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:
ojy
2026-05-13 06:54:40 +00:00
parent 58f898b47d
commit c1520d7962
4 changed files with 422 additions and 1 deletions

View File

@@ -76,6 +76,10 @@
<button class="btn btn-outline-primary btn-sm me-1" @click="reloadMarket()" :disabled="refreshing" title="Re-fetch spot, marks &amp; 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);