Multi-symbol strategy baskets + symbol picker

The store now keeps one basket per symbol (v2 schema, auto-migrates v1).
Adding a leg from a different symbol no longer wipes the basket — it creates
that symbol's basket and makes it active. The Strategy page shows a symbol
picker at the top (when >1 symbol is saved) to switch between them; switching
auto-fetches that symbol's live marks. "Clear all" clears only the current
symbol's legs; new StrategyStore.clearAll() wipes everything.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ojy
2026-05-13 04:47:16 +00:00
parent e0cbd798b6
commit 703c305cf1
2 changed files with 195 additions and 85 deletions

View File

@@ -55,13 +55,20 @@
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col-auto" x-show="symbols.length > 1" x-cloak>
<label class="form-label text-secondary mb-1" for="symPicker">Strategy</label>
<select id="symPicker" class="form-select form-select-sm fw-bold" style="min-width:7rem" :value="symbol" @change="switchSymbol($event.target.value)" aria-label="Pick strategy symbol">
<template x-for="s in symbols" :key="s"><option :value="s" x-text="s"></option></template>
</select>
</div>
<div class="col">
<h2 class="page-title">Strategy P/L Analyzer</h2>
<div class="text-secondary mt-1" x-show="legs.length > 0" x-cloak>
<span class="badge bg-purple-lt fs-6 me-2" x-text="strategyName"></span>
<span x-text="symbol"></span> ·
<span x-show="symbols.length <= 1" x-text="symbol"></span><span x-show="symbols.length <= 1"> · </span>
<span x-text="legs.length + ' leg' + (legs.length===1?'':'s')"></span> ·
Spot <strong class="mono" x-text="spot > 0 ? '$'+spot.toFixed(2) : '—'"></strong>
<span x-show="symbols.length > 1" class="ms-2 text-secondary" x-text="'(' + symbols.length + ' symbols saved)'"></span>
</div>
</div>
<div class="col-auto" x-show="legs.length > 0" x-cloak>
@@ -291,7 +298,7 @@
function strategyApp() {
return {
// state
symbol: '', spot: 0, legs: [],
symbol: '', symbols: [], spot: 0, legs: [],
dteOffset: 0,
refreshing: false, showManual: false, toast: '',
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
@@ -308,10 +315,19 @@
reload() {
const st = StrategyStore.load();
this.symbol = st.symbol || '';
this.symbols = st.symbols || [];
this.spot = st.spotSnapshot || 0;
this.legs = st.legs || [];
if (this.dteOffset > this.maxDTE) this.dteOffset = this.maxDTE;
if (this.legs.length > 0) this.$nextTick(() => this.renderChart());
if (this.legs.length > 0) this.$nextTick(() => this.renderChart()); else if (this.chart) this.chart.updateSeries([{name:'P/L',data:[]},{name:'P/L',data:[]}]);
},
switchSymbol(sym) {
if (!sym || sym === this.symbol) return;
StrategyStore.setActive(sym);
this.dteOffset = 0;
this.reload();
if (this.legs.length > 0) this.reloadMarket(false);
},
// ── derived ───────────────────────────────────────────
@@ -421,10 +437,11 @@
this.reload();
},
clearAll() {
if (!confirm('Clear all legs?')) return;
if (!confirm('Clear all ' + (this.symbol || '') + ' legs?')) return;
StrategyStore.clear();
this.reload();
this.dteOffset = 0;
if (this.chart) { this.chart.destroy(); this.chart = null; }
this.reload(); // re-renders the chart if another symbol's basket is now active
},
addManualLeg() {
const m = this.manual;