feat: instrument model
This commit is contained in:
@@ -37,6 +37,7 @@ class VirtualEvent(BaseModel):
|
||||
class WhatIfBody(BaseModel):
|
||||
period: str = "1y"
|
||||
virtual_events: List[VirtualEvent] = []
|
||||
start_date: Optional[str] = None
|
||||
|
||||
|
||||
class CalibrateBody(BaseModel):
|
||||
@@ -192,10 +193,26 @@ def get_price_history(
|
||||
conn.close()
|
||||
|
||||
|
||||
def _ph_period(start_date: Optional[str], fallback_period: str) -> str:
|
||||
"""Compute the price-history period string needed to cover start_date → today."""
|
||||
if not start_date:
|
||||
return fallback_period
|
||||
try:
|
||||
from datetime import datetime as _dt
|
||||
days = (_dt.utcnow().date() - _dt.fromisoformat(start_date[:10]).date()).days + 10
|
||||
for p, d in [("5d", 7), ("1mo", 35), ("3mo", 95), ("6mo", 190), ("1y", 370), ("2y", 740)]:
|
||||
if days <= d:
|
||||
return p
|
||||
return "2y"
|
||||
except Exception:
|
||||
return fallback_period
|
||||
|
||||
|
||||
@router.get("/{instrument}/timeline")
|
||||
def get_instrument_timeline(
|
||||
instrument: str,
|
||||
period: str = Query("1y", description="5d|1mo|3mo|6mo|1y|2y"),
|
||||
period: str = Query("1y", description="5d|1mo|3mo|6mo|1y|2y"),
|
||||
start_date: Optional[str] = Query(None, description="Date de début ISO (override period)"),
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Simulation jour par jour de tous les nœuds du modèle sur la période."""
|
||||
from services.database import get_conn
|
||||
@@ -203,9 +220,8 @@ def get_instrument_timeline(
|
||||
from services.price_history import get_price_history
|
||||
conn = get_conn()
|
||||
try:
|
||||
# Pré-peupler le cache prix pour que l'auto-anchor ait les données disponibles
|
||||
get_price_history(conn, instrument.upper(), period)
|
||||
data = simulate_timeline(conn, instrument.upper(), period)
|
||||
get_price_history(conn, instrument.upper(), _ph_period(start_date, period))
|
||||
data = simulate_timeline(conn, instrument.upper(), period, start_date=start_date)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail=f"Modèle introuvable pour {instrument.upper()}")
|
||||
return data
|
||||
@@ -224,10 +240,12 @@ def timeline_whatif(
|
||||
from services.price_history import get_price_history
|
||||
conn = get_conn()
|
||||
try:
|
||||
# Garantir que le cache prix est disponible pour l'auto-anchor
|
||||
get_price_history(conn, instrument.upper(), body.period)
|
||||
get_price_history(conn, instrument.upper(), _ph_period(body.start_date, body.period))
|
||||
ve_list = [ve.dict() for ve in body.virtual_events]
|
||||
data = simulate_timeline(conn, instrument.upper(), body.period, virtual_events=ve_list)
|
||||
data = simulate_timeline(
|
||||
conn, instrument.upper(), body.period,
|
||||
virtual_events=ve_list, start_date=body.start_date
|
||||
)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail=f"Modèle introuvable pour {instrument.upper()}")
|
||||
return data
|
||||
|
||||
@@ -1015,12 +1015,14 @@ def get_model_state(conn, instrument: str, at_date: Optional[str] = None) -> Opt
|
||||
def simulate_timeline(
|
||||
conn, instrument: str, period: str = "1y",
|
||||
virtual_events: Optional[list] = None,
|
||||
start_date: Optional[str] = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Simulate all node values day by day over the period.
|
||||
Returns [{date, nodes: {id: value}, net_pips}].
|
||||
Uses lifecycle (rise/plateau/decay) for event contributions.
|
||||
Manual overrides are static (applied uniformly across the period).
|
||||
start_date overrides the period-based date_from when provided.
|
||||
"""
|
||||
inst_upper = instrument.upper()
|
||||
row = conn.execute(
|
||||
@@ -1033,9 +1035,15 @@ def simulate_timeline(
|
||||
output_id = graph_def["output_node"]
|
||||
|
||||
period_days = {"5d":7,"1mo":35,"3mo":95,"6mo":190,"1y":370,"2y":740}
|
||||
lookback = period_days.get(period, 370)
|
||||
today = datetime.utcnow().date()
|
||||
date_from = today - timedelta(days=lookback)
|
||||
lookback = period_days.get(period, 370)
|
||||
today = datetime.utcnow().date()
|
||||
if start_date:
|
||||
try:
|
||||
date_from = date_type.fromisoformat(start_date[:10])
|
||||
except ValueError:
|
||||
date_from = today - timedelta(days=lookback)
|
||||
else:
|
||||
date_from = today - timedelta(days=lookback)
|
||||
|
||||
# Load overrides (static)
|
||||
overrides = {r["node_id"]: dict(r) for r in conn.execute(
|
||||
@@ -1107,6 +1115,10 @@ def simulate_timeline(
|
||||
for ev in events:
|
||||
if ev["ev_date"] > cur:
|
||||
continue
|
||||
if ev["ev_date"] < date_from:
|
||||
# Events avant la fenêtre ne portent pas de lifecycle dans la simu
|
||||
# (leur impact est absorbé dans l'auto-anchor du prix de départ)
|
||||
continue
|
||||
days = (cur - ev["ev_date"]).days
|
||||
df = _lifecycle(days, ev["rise"], ev["plateau"], ev["absorption"], ev["dtype"])
|
||||
if df < 0.01:
|
||||
|
||||
Reference in New Issue
Block a user