feat: new cockpit

This commit is contained in:
OpenSquared
2026-07-14 11:21:43 +02:00
parent 9778c74ae3
commit ad07c8d886
32 changed files with 871 additions and 551 deletions

View File

@@ -268,7 +268,7 @@ def _gpt4o_recommend(event: dict, templates: list) -> dict:
]
system_prompt = (
"You are a macro market analyst and causal graph specialist for the GeoOptions platform. "
"You are a macro market analyst and causal graph specialist for the OpenFin platform. "
"Given a market event and a library of causal graph templates, recommend the most appropriate template "
"and suggest any coefficient adjustments. Respond with a JSON object only.\n"
"IMPORTANT: Only recommend a template if it is structurally compatible with the event type "
@@ -1973,7 +1973,7 @@ def ai_to_grammar(template_id: int, body: AiToGrammarRequest):
)
system_prompt = (
"You are a causal graph patch generator for the GeoOptions macro-finance platform.\n"
"You are a causal graph patch generator for the OpenFin macro-finance platform.\n"
"Convert the user's natural language modification request into graph patch operations.\n"
"Return ONLY the grammar lines — no explanation, no markdown, no fences.\n\n"
+ GRAMMAR_SPEC
@@ -2036,7 +2036,7 @@ def ai_modify_template(template_id: int, body: AiModifyRequest):
gj = body.current_graph if body.current_graph.get("nodes") else t["graph_json"]
system_prompt = (
"You are a causal graph editor for the GeoOptions macro-finance platform.\n"
"You are a causal graph editor for the OpenFin macro-finance platform.\n"
"You receive a causal graph in JSON and a modification request in natural language.\n"
"Return ONLY the complete modified graph_json as a valid JSON object — no explanation, no markdown.\n\n"
"Graph format:\n"

View File

@@ -439,6 +439,7 @@ def get_calendar_events(
"""
from services.database import get_conn
from datetime import datetime, date as date_type, timedelta
import json as _json
conn = get_conn()
try:
inst_upper = instrument.upper()
@@ -453,6 +454,23 @@ def get_calendar_events(
currencies = _INSTRUMENT_CURRENCIES.get(inst_upper, ["USD"])
impact_filter = [i.strip() for i in impacts.split(",")] if impacts else ["high", "medium"]
# Build event_category → node_label map from instrument graph
event_node_map: dict[str, str] = {}
try:
graph_row = conn.execute(
"SELECT graph_json FROM instrument_models WHERE instrument=?", [inst_upper]
).fetchone()
if graph_row:
gdef = _json.loads(graph_row["graph_json"])
event_node_map = {
n.get("event_category", ""): n.get("label", n["id"])
for n in gdef.get("nodes", [])
if n.get("node_type") == "input_event" and n.get("event_category")
}
event_node_map.pop("", None)
except Exception:
pass
# Auto-sync live si ff_calendar vide pour cette periode
ph = ",".join("?" * len(currencies))
n_existing = conn.execute(
@@ -500,6 +518,7 @@ def get_calendar_events(
"forecast_value": r.get("forecast_value"),
"previous_value": r.get("previous_value"),
"category": category,
"routed_node": event_node_map.get(category, "direct"),
"is_future": is_future,
"estimated_pips": est_pips,
"has_surprise": has_surprise,
@@ -738,12 +757,24 @@ def get_macro_guidance(instrument: str) -> List[Dict[str, Any]]:
except Exception:
pass
# Unit-aware conversion (same logic as get_macro_sync_items)
unit = node.get("unit", "")
def _uc(v):
if v is None: return None
if unit == "K" and abs(v) >= 1_000: return round(v / 1_000, 1)
if unit == "pts" and v > 10: return round(v - 50.0, 1)
return round(v, 4)
next_fv = _uc(next_event.get("forecast") if next_event else None)
if next_event:
next_event = {**next_event, "forecast": next_fv}
result.append({
"node_id": node["id"],
"node_label": node.get("label", node["id"]),
"macro_key": macro_key,
"unit": node.get("unit", ""),
"current_value": round(current_v, 4) if current_v is not None else None,
"unit": unit,
"current_value": _uc(current_v),
"next_event": next_event,
})

View File

@@ -0,0 +1,80 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List
router = APIRouter(prefix="/api/watchlist", tags=["watchlist"])
# Intentionally duplicated from market_data.py to keep this system fully decoupled
# from the other instrument-list mechanisms (market_watchlist, INSTRUMENT_MODELS,
# instruments.json, options_vol watchlist-tickers).
_QUOTE_TYPE_TO_ASSET_CLASS = {
"CURRENCY": "forex",
"FUTURE": "energy",
"INDEX": "indices",
"ETF": "etfs",
"EQUITY": "equities",
"MUTUALFUND": "etfs",
}
@router.get("/")
def list_watchlist():
from services.database import get_instruments_watchlist
return get_instruments_watchlist()
@router.get("/quotes")
def watchlist_quotes():
from services.database import get_instruments_watchlist
from services.data_fetcher import get_quote
items = []
for row in get_instruments_watchlist():
q = get_quote(row["ticker"]) or {}
items.append({
**row,
"price": q.get("price"),
"change_pct": q.get("change_pct"),
})
return {"items": items}
@router.post("/{ticker}")
def add_ticker(ticker: str):
from services.data_fetcher import get_quote
from services.database import get_instruments_watchlist, add_instrument_watchlist
import yfinance as yf
ticker = ticker.strip().upper()
if any(row["ticker"] == ticker for row in get_instruments_watchlist()):
raise HTTPException(409, f"'{ticker}' is already in the watchlist")
q = get_quote(ticker)
if not q or not q.get("price"):
raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance")
name = ticker
asset_class = "unknown"
try:
info = yf.Ticker(ticker).fast_info
name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker
quote_type = getattr(info, "quote_type", "") or ""
asset_class = _QUOTE_TYPE_TO_ASSET_CLASS.get(quote_type.upper(), "unknown")
except Exception:
pass
add_instrument_watchlist(ticker, name, asset_class)
return {"ticker": ticker, "name": name, "asset_class": asset_class, "price": q["price"]}
@router.delete("/{ticker}")
def remove_ticker(ticker: str):
from services.database import remove_instrument_watchlist
remove_instrument_watchlist(ticker.strip().upper())
return {"removed": ticker.strip().upper()}
class ReorderBody(BaseModel):
tickers: List[str]
@router.put("/reorder")
def reorder(body: ReorderBody):
from services.database import reorder_instruments_watchlist
reorder_instruments_watchlist(body.tickers)
return {"ok": True}