feat: strategy builder

This commit is contained in:
OpenSquared
2026-07-19 00:14:03 +02:00
parent 0d437922c9
commit 85f48bb374
3 changed files with 40 additions and 12 deletions

View File

@@ -97,9 +97,9 @@ function GreeksTile({ label, now, scenario }: { label: string; now: number; scen
// ── Scenario panel ────────────────────────────────────────────────────────────
function ScenarioPanel({
symbol, setSymbol, horizonDays, setHorizonDays, scenario, setScenario, watchlistTickers,
symbol, setSymbol, onCommitSymbol, horizonDays, setHorizonDays, scenario, setScenario, watchlistTickers,
}: {
symbol: string; setSymbol: (v: string) => void
symbol: string; setSymbol: (v: string) => void; onCommitSymbol: (v?: string) => void
horizonDays: number; setHorizonDays: (v: number) => void
scenario: StrategyScenario; setScenario: (v: StrategyScenario) => void
watchlistTickers: string[]
@@ -128,7 +128,15 @@ function ScenarioPanel({
<label className="stat-label block mb-1">Symbole</label>
<input
value={symbol}
onChange={(e) => setSymbol(e.target.value.toUpperCase())}
onChange={(e) => {
const v = e.target.value.toUpperCase()
setSymbol(v)
// Native datalist pick lands here as a single onChange with the full value —
// commit immediately rather than waiting for a blur that may not follow.
if (watchlistTickers.includes(v)) onCommitSymbol(v)
}}
onBlur={() => onCommitSymbol()}
onKeyDown={(e) => e.key === 'Enter' && onCommitSymbol()}
className="w-full bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white"
placeholder="SPY, QQQ, GLD…"
list="strategy-builder-watchlist"
@@ -573,10 +581,12 @@ export default function StrategyBuilder() {
symbol: '', horizon_days: 8, spot_shock_pct: 0, iv_level_shift: 0, skew_tilt: 0, term_shift: 0, manual_grid: [],
})
useEffect(() => {
const t = setTimeout(() => setDebouncedSymbol(symbol.trim()), 500)
return () => clearTimeout(t)
}, [symbol])
// Chain lookup only commits on blur/Enter/datalist-pick, never mid-keystroke — typing
// "EUU" while aiming for "EUU:XCME" must never flash a "ticker not found" error. Takes
// an optional explicit value so the datalist-pick path (which calls this synchronously
// right after setSymbol in the same onChange) doesn't read a stale pre-update closure.
const commitSymbol = (v?: string) => setDebouncedSymbol((v ?? symbol).trim())
const [legs, setLegs] = useState<StrategyLeg[]>([])
const [constraints, setConstraints] = useState<OptimizeConstraints>({
max_legs: 4, delta_threshold: 0.15, max_loss_cap: null, objective: 'net_pnl', top_n: 20,
@@ -691,7 +701,7 @@ export default function StrategyBuilder() {
</div>
)}
<ScenarioPanel symbol={symbol} setSymbol={setSymbol} horizonDays={horizonDays} setHorizonDays={setHorizonDays}
<ScenarioPanel symbol={symbol} setSymbol={setSymbol} onCommitSymbol={commitSymbol} horizonDays={horizonDays} setHorizonDays={setHorizonDays}
scenario={scenario} setScenario={setScenario} watchlistTickers={watchlistTickers} />
<ScenarioLibrary symbol={debouncedSymbol} scenario={scenario} onLoad={handleLoadScenario} />