feat: new cockpit
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
80
backend/routers/instruments_watchlist.py
Normal file
80
backend/routers/instruments_watchlist.py
Normal 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}
|
||||
Reference in New Issue
Block a user