Tracked orders — persist strategy positions in SQLite
Backend:
- New orders table (symbol, name, legs_json, entry_cost, created_at,
closed_at, status, note) sharing the snapshots.db
- Endpoints:
POST /api/orders save a position
GET /api/orders?symbol= list (most recent first)
GET /api/orders/:id single
PATCH /api/orders/:id { status:'open'|'closed', note? }
DELETE /api/orders/:id remove
Strategy page:
- "Save to Tracker" button posts the currently-active legs (with name,
net cost, side, qty, entry, IV, lock) as an order
Tracker page:
- New "Tracked Orders" section above the IV history charts: lists each
saved position for the current symbol with strategy name, leg summary,
entered cost, current value, P/L $ and %, opened date, status, and
Close/Reopen + Remove actions. Live P/L uses /api/chain mids for each
unique leg expiry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,10 @@
|
||||
<button class="btn btn-outline-primary btn-sm me-1" @click="reloadMarket()" :disabled="refreshing" title="Re-fetch spot, marks & IVs; refresh entry price on unlocked legs">
|
||||
<span x-show="refreshing" class="spinner-border spinner-border-sm me-1" role="status"></span>Reload
|
||||
</button>
|
||||
<button class="btn btn-success btn-sm me-1" @click="saveOrder()" :disabled="savingOrder || activeLegs.length === 0"
|
||||
title="Save this position to the Tracker (uses currently-checked legs)">
|
||||
<span x-show="savingOrder" class="spinner-border spinner-border-sm me-1" role="status"></span>Save to Tracker
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" @click="clearAll()">Clear all</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,7 +345,7 @@
|
||||
dteOffset: 0,
|
||||
xZoom: 1, xPan: 0, lastHalfPct: 0,
|
||||
expiryLocked: false, masterExpiry: '',
|
||||
refreshing: false, showManual: false, toast: '',
|
||||
refreshing: false, savingOrder: false, showManual: false, toast: '',
|
||||
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
||||
chart: null, _renderTimer: null,
|
||||
_chainCache: {}, // "SYMBOL@EXPIRY" -> { "strike@type": optionRow }
|
||||
@@ -744,6 +748,36 @@
|
||||
},
|
||||
flash(msg) { this.toast = msg; setTimeout(()=>{ this.toast=''; }, 2500); },
|
||||
|
||||
// POST the current active basket to /api/orders for tracking
|
||||
async saveOrder() {
|
||||
const legs = this.activeLegs;
|
||||
if (legs.length === 0 || !this.symbol) return;
|
||||
this.savingOrder = true;
|
||||
try {
|
||||
const body = {
|
||||
symbol: this.symbol,
|
||||
name: this.strategyName,
|
||||
entryCost: this.netCost,
|
||||
legs: legs.map(l => ({
|
||||
symbol: l.symbol, expiry: l.expiry, type: l.type, strike: l.strike,
|
||||
side: l.side, qty: l.qty, entryPrice: l.entryPrice, iv: l.iv, locked: !!l.locked,
|
||||
})),
|
||||
};
|
||||
const r = await fetch('/api/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!r.ok || !d.ok) throw new Error(d.error || ('HTTP ' + r.status));
|
||||
this.flash('Saved to Tracker · order #' + d.data.id + ' (' + this.strategyName + ')');
|
||||
} catch (e) {
|
||||
this.flash('Save failed: ' + e.message);
|
||||
} finally {
|
||||
this.savingOrder = false;
|
||||
}
|
||||
},
|
||||
|
||||
scheduleRender() {
|
||||
clearTimeout(this._renderTimer);
|
||||
this._renderTimer = setTimeout(() => this.renderChart(), 40);
|
||||
|
||||
Reference in New Issue
Block a user