feat: strategy builder
This commit is contained in:
@@ -18,6 +18,24 @@ from services.vol_surface import Surface, ScenarioSurface
|
||||
DEFAULT_SPREAD_PCT = 0.05 # fallback relative bid/ask spread when no live quote is found
|
||||
|
||||
|
||||
def to_native(obj: Any) -> Any:
|
||||
"""Recursively converts numpy scalars (bool_, int64, float64, ...) to native Python
|
||||
types. Comparisons/aggregations over numpy-typed inputs (e.g. bid/ask sourced from a
|
||||
DB row that came back as a numpy type, or scipy's norm.cdf) can leave a stray
|
||||
numpy.bool_/numpy.float64 buried in a nested result — FastAPI's default JSON encoder
|
||||
doesn't know those types and fails ('X object is not iterable', then a secondary
|
||||
'vars() argument must have __dict__ attribute' from its own fallback). Applied once
|
||||
at the API boundary (price_combo/payoff_curves/optimizer results) rather than chasing
|
||||
the exact field through the whole pricing pipeline."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: to_native(v) for k, v in obj.items()}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [to_native(v) for v in obj]
|
||||
if isinstance(obj, np.generic):
|
||||
return obj.item()
|
||||
return obj
|
||||
|
||||
|
||||
def _sign(leg: Dict[str, Any]) -> int:
|
||||
return 1 if leg.get("position", "long") == "long" else -1
|
||||
|
||||
@@ -131,7 +149,7 @@ def price_combo(
|
||||
delta_now = greeks_at(legs, spot_now, 0, surface_now, r)["delta"]
|
||||
delta_scenario = greeks_at(legs, spot_scenario, horizon_days, surface_scenario, r)["delta"]
|
||||
|
||||
return {
|
||||
return to_native({
|
||||
"entry_cost": round(entry_ref, 2),
|
||||
"entry_cost_mid": round(entry_ref_mid, 2),
|
||||
"scenario_value": round(scenario_exec, 2),
|
||||
@@ -145,7 +163,7 @@ def price_combo(
|
||||
"greeks_scenario": greeks_at(legs, spot_scenario, horizon_days, surface_scenario, r),
|
||||
"net_delta_now": delta_now,
|
||||
"net_delta_scenario": delta_scenario,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def check_bounded_risk(legs: List[Dict[str, Any]], entry_ref: float, surface: Any, spot: float) -> Dict[str, Any]:
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from services.option_chain import get_chain_slice
|
||||
from services.vol_surface import Surface, ScenarioSurface, build_surface, apply_scenario
|
||||
from services.strategy_engine import price_combo, expected_pnl_scenario
|
||||
from services.strategy_engine import price_combo, expected_pnl_scenario, to_native
|
||||
from services.strategy_templates import generate_all, strikes_for
|
||||
|
||||
MAX_SEEDS_FOR_RESIDUAL_SEARCH = 40
|
||||
@@ -168,4 +168,4 @@ def optimize(
|
||||
scored.extend(refined)
|
||||
|
||||
scored.sort(key=lambda c: c["score"], reverse=True)
|
||||
return _dedup_top_n(scored, top_n)
|
||||
return to_native(_dedup_top_n(scored, top_n))
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user