feat: instrument picker + Pattern Lab instrument scan mode
- instruments.ts: 90 IB-options-tradable instruments in 12 categories
(US Indices, Europe, Asia, EM, Sectors, Forex, Bonds, Metals, Energy,
Agriculture, Crypto, Volatility) — EUR/CHF, Cotton, etc. all included
- PatternExplorer: replace text input in Instrument Lens with categorised
grid picker (category pill filters + search + custom ticker fallback)
- PatternLab: add Instrument Scan tab alongside Event Presets
- Pick any instrument from the shared categorised picker
- Set period (start/end date) + horizon per pattern
- AI scans the full period: identifies 4-6 key pattern instances each with
their own entry date, expected move, strategy
- 'Evaluate outcomes' fetches actual price at T+horizon per pattern
- 'Save pattern' promotes any instance to the Pattern Library
- backend/services/pattern_lab.py: run_instrument_scan() + evaluate_instrument_outcomes()
(per-pattern analysis_date vs shared date in event mode)
- backend/routers/pattern_lab.py: POST /instrument-scan + POST /evaluate-instrument/{id}
- useApi.ts: useInstrumentScan + useEvaluateInstrumentScan hooks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,14 @@ class SavePatternRequest(BaseModel):
|
||||
asset_class: Optional[str] = "indices"
|
||||
|
||||
|
||||
class InstrumentScanRequest(BaseModel):
|
||||
ticker: str
|
||||
instrument_name: str
|
||||
start_date: str # YYYY-MM-DD
|
||||
end_date: str # YYYY-MM-DD
|
||||
horizon_days: int = 90
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _get_run(run_id: str) -> dict:
|
||||
@@ -165,6 +173,82 @@ def delete_run(run_id: str):
|
||||
return {"deleted": run_id}
|
||||
|
||||
|
||||
@router.post("/instrument-scan")
|
||||
async def instrument_scan(req: InstrumentScanRequest):
|
||||
"""Scan an instrument's historical price action to extract pattern instances."""
|
||||
from services.pattern_lab import run_instrument_scan
|
||||
|
||||
openai_key = get_config("openai_api_key") or ""
|
||||
if not openai_key:
|
||||
raise HTTPException(400, "OpenAI API key not configured")
|
||||
|
||||
run_id = f"LAB_INS_{uuid.uuid4().hex[:8].upper()}"
|
||||
now = datetime.utcnow().isoformat()
|
||||
|
||||
conn = get_conn()
|
||||
conn.execute("""INSERT INTO backtest_lab_runs
|
||||
(id, preset_id, theme, analysis_date, horizon_days, assets, theme_hint, status, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,'running',?)""", (
|
||||
run_id,
|
||||
"instrument_scan",
|
||||
f"Instrument Scan: {req.instrument_name}",
|
||||
req.start_date,
|
||||
req.horizon_days,
|
||||
json.dumps([req.ticker]),
|
||||
json.dumps({"type": "instrument", "ticker": req.ticker,
|
||||
"instrument_name": req.instrument_name,
|
||||
"start_date": req.start_date, "end_date": req.end_date}),
|
||||
now,
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
ai_result = await run_instrument_scan(
|
||||
req.ticker, req.instrument_name,
|
||||
req.start_date, req.end_date,
|
||||
req.horizon_days, openai_key,
|
||||
)
|
||||
|
||||
conn = get_conn()
|
||||
conn.execute("""UPDATE backtest_lab_runs SET
|
||||
ai_result=?, status='done'
|
||||
WHERE id=?""", (json.dumps(ai_result), run_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {"run_id": run_id, "status": "done", "ai_result": ai_result}
|
||||
|
||||
except Exception as e:
|
||||
conn = get_conn()
|
||||
conn.execute("UPDATE backtest_lab_runs SET status='error', error_msg=? WHERE id=?",
|
||||
(str(e), run_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
raise HTTPException(500, f"Instrument scan failed: {e}")
|
||||
|
||||
|
||||
@router.post("/evaluate-instrument/{run_id}")
|
||||
def evaluate_instrument_run(run_id: str):
|
||||
"""Evaluate outcomes for an instrument scan (per-pattern dates)."""
|
||||
from services.pattern_lab import evaluate_instrument_outcomes
|
||||
|
||||
run = _get_run(run_id)
|
||||
if run["status"] not in ("done", "evaluated"):
|
||||
raise HTTPException(400, f"Run status is '{run['status']}', must be 'done' first")
|
||||
|
||||
outcomes = evaluate_instrument_outcomes(run)
|
||||
|
||||
conn = get_conn()
|
||||
conn.execute("""UPDATE backtest_lab_runs SET
|
||||
outcome=?, status='evaluated', evaluated_at=datetime('now')
|
||||
WHERE id=?""", (json.dumps(outcomes), run_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {"run_id": run_id, "outcomes": outcomes}
|
||||
|
||||
|
||||
@router.post("/save-pattern")
|
||||
def save_pattern_from_run(req: SavePatternRequest):
|
||||
"""Promote one AI-suggested pattern (with its outcome) to the pattern library."""
|
||||
|
||||
Reference in New Issue
Block a user