feat: AI Desks — configurable agent system for news/technical/eco processing
- New ai_desks table with CRUD (get_all/by_type/upsert/delete) - ai_desks router: REST API + GET /signal-catalog (7 extensible signals) - News Desk: semantic dedup via AI (±N days window, system_prompt hint) - Technical Desk: 4 signal detectors driven by desk config (ma_cross, rsi_extreme, bb_squeeze, new_52w_extreme) - 3 more signals in catalog ready to enable: price_gap, volume_spike, macd_crossover - market_event_detector.py loads desk configs at runtime, falls back to legacy params - AIDesks.tsx: full editor UI with signal toggles, param sliders, instrument multi-select - Sidebar: Bot icon + /ai-desks route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -961,6 +961,87 @@ def init_db():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── AI Desks ──────────────────────────────────────────────────────────────
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS ai_desks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
active INTEGER DEFAULT 1,
|
||||
system_prompt TEXT DEFAULT '',
|
||||
instruments TEXT DEFAULT '[]',
|
||||
config TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)""")
|
||||
|
||||
# Seed default desks (idempotent)
|
||||
_AI_DESK_DEFAULTS = [
|
||||
{
|
||||
"name": "News Desk — Géopolitique",
|
||||
"type": "news",
|
||||
"active": 1,
|
||||
"system_prompt": (
|
||||
"Tu es un analyste géopolitique et macro senior. Tu évalues si une news représente "
|
||||
"un événement marché STRUCTURANT qui mérite un enregistrement permanent.\n"
|
||||
"Sois exigeant : préfère ignorer une news douteuse plutôt qu'enregistrer du bruit.\n"
|
||||
"Points d'attention :\n"
|
||||
"- Al Jazeera, RT et certains médias régionaux publient souvent plusieurs articles "
|
||||
"redondants sur le même fait — vérifie toujours si un événement similaire existe déjà.\n"
|
||||
"- Une rumeur ou spéculation sans source officielle ne qualifie pas.\n"
|
||||
"- Privilégie les faits avérés avec impact macro ou géopolitique mesurable."
|
||||
),
|
||||
"instruments": json.dumps(["SPY","GLD","USO","TLT","VXX","EURUSD=X","BTC-USD","XOM"]),
|
||||
"config": json.dumps({
|
||||
"min_impact": 0.55,
|
||||
"lookback_hours": 48,
|
||||
"max_evaluate": 15,
|
||||
"dedup_enabled": True,
|
||||
"dedup_lookback_days": 2,
|
||||
"dedup_categories": ["geopolitical","fundamental","report"],
|
||||
}),
|
||||
},
|
||||
{
|
||||
"name": "Technical Desk",
|
||||
"type": "technical",
|
||||
"active": 1,
|
||||
"system_prompt": (
|
||||
"Tu détectes des signaux techniques structurants sur les marchés financiers. "
|
||||
"Concentre-toi sur les signaux qui ont une signification macro claire."
|
||||
),
|
||||
"instruments": json.dumps([
|
||||
"SPY","QQQ","IWM","EEM","GLD","USO","TLT",
|
||||
"EURUSD=X","VXX","BTC-USD","NVDA","XOM","HYG"
|
||||
]),
|
||||
"config": json.dumps({
|
||||
"lookback_days": 7,
|
||||
"signals": {
|
||||
"ma_cross": {"enabled": True, "pairs": [["MA50","MA200"],["MA50","MA100"]]},
|
||||
"rsi_extreme": {"enabled": True, "period": 14, "oversold": 30, "overbought": 70},
|
||||
"bb_squeeze": {"enabled": True, "period": 20, "std": 2.0, "width_threshold": 0.05},
|
||||
"new_52w_extreme":{"enabled": True, "buffer_pct": 0.5},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
"name": "Eco Desk — FRED",
|
||||
"type": "eco",
|
||||
"active": 1,
|
||||
"system_prompt": "Tu analyses les surprises économiques des données macro US (FRED).",
|
||||
"instruments": json.dumps(["SPY","TLT","GLD","EURUSD=X","USO","HYG"]),
|
||||
"config": json.dumps({"z_threshold": 1.5, "days": 7}),
|
||||
},
|
||||
]
|
||||
for _desk in _AI_DESK_DEFAULTS:
|
||||
try:
|
||||
c.execute(
|
||||
"INSERT OR IGNORE INTO ai_desks (name, type, active, system_prompt, instruments, config) "
|
||||
"VALUES (?,?,?,?,?,?)",
|
||||
(_desk["name"], _desk["type"], _desk["active"],
|
||||
_desk["system_prompt"], _desk["instruments"], _desk["config"])
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -4850,3 +4931,93 @@ def get_weekly_impact_sources(days: int = 7, min_score: float = 0.3) -> List[Dic
|
||||
return result
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── AI Desks ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_all_ai_desks() -> List[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
try:
|
||||
rows = conn.execute("SELECT * FROM ai_desks ORDER BY type, name").fetchall()
|
||||
result = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
for f in ("instruments", "config"):
|
||||
try:
|
||||
d[f] = json.loads(d.get(f) or "[]" if f == "instruments" else "{}")
|
||||
except Exception:
|
||||
d[f] = [] if f == "instruments" else {}
|
||||
result.append(d)
|
||||
return result
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_ai_desk_by_type(desk_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Return the first active desk of given type, or None."""
|
||||
desks = get_all_ai_desks()
|
||||
return next((d for d in desks if d["type"] == desk_type and d.get("active")), None)
|
||||
|
||||
|
||||
def upsert_ai_desk(desk: Dict[str, Any]) -> int:
|
||||
conn = get_conn()
|
||||
try:
|
||||
instr = desk.get("instruments", [])
|
||||
cfg = desk.get("config", {})
|
||||
if isinstance(instr, list):
|
||||
instr = json.dumps(instr)
|
||||
if isinstance(cfg, dict):
|
||||
cfg = json.dumps(cfg)
|
||||
conn.execute("""
|
||||
INSERT INTO ai_desks (name, type, active, system_prompt, instruments, config, updated_at)
|
||||
VALUES (?,?,?,?,?,?, datetime('now'))
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
type=excluded.type, active=excluded.active,
|
||||
system_prompt=excluded.system_prompt,
|
||||
instruments=excluded.instruments, config=excluded.config,
|
||||
updated_at=datetime('now')
|
||||
""", (desk["name"], desk["type"], int(desk.get("active", 1)),
|
||||
desk.get("system_prompt", ""), instr, cfg))
|
||||
row = conn.execute("SELECT id FROM ai_desks WHERE name=?", (desk["name"],)).fetchone()
|
||||
conn.commit()
|
||||
return row["id"] if row else -1
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_ai_desk(name: str) -> bool:
|
||||
conn = get_conn()
|
||||
try:
|
||||
conn.execute("DELETE FROM ai_desks WHERE name=?", (name,))
|
||||
conn.commit()
|
||||
return True
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_market_events_near_date(date_str: str, days: int = 2,
|
||||
categories: Optional[List[str]] = None) -> List[Dict[str, Any]]:
|
||||
"""Fetch market_events within ±days of date_str, optionally filtered by category."""
|
||||
conn = get_conn()
|
||||
try:
|
||||
from datetime import datetime, timedelta
|
||||
dt = datetime.fromisoformat(date_str[:10])
|
||||
d_from = (dt - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
d_to = (dt + timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
if categories:
|
||||
placeholders = ",".join("?" * len(categories))
|
||||
rows = conn.execute(
|
||||
f"SELECT id, name, start_date, category, description FROM market_events "
|
||||
f"WHERE start_date BETWEEN ? AND ? AND category IN ({placeholders}) "
|
||||
f"ORDER BY start_date DESC LIMIT 30",
|
||||
[d_from, d_to] + list(categories)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT id, name, start_date, category, description FROM market_events "
|
||||
"WHERE start_date BETWEEN ? AND ? ORDER BY start_date DESC LIMIT 30",
|
||||
(d_from, d_to)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user