Files
OpenFin/backend/services/database.py
OpenSquared d256b65d30 Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o
Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM,
Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA.
Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:29:59 +02:00

1406 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
SQLite persistence layer for portfolio positions, custom patterns, and config.
"""
import sqlite3
import json
import os
from datetime import datetime
from typing import List, Dict, Any, Optional
DB_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "geooptions.db")
def get_conn() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = get_conn()
c = conn.cursor()
c.execute("""CREATE TABLE IF NOT EXISTS portfolio (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
underlying TEXT NOT NULL,
strategy TEXT NOT NULL,
asset_class TEXT,
entry_date TEXT NOT NULL,
expiry_date TEXT,
expiry_days INTEGER,
legs TEXT NOT NULL,
capital_invested REAL NOT NULL,
entry_underlying_price REAL,
geo_trigger TEXT,
rationale TEXT,
status TEXT DEFAULT 'open',
close_date TEXT,
close_value REAL,
notes TEXT,
ib_fees_entry REAL DEFAULT 0,
ib_fees_exit REAL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)""")
c.execute("""CREATE TABLE IF NOT EXISTS custom_patterns (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
triggers TEXT,
keywords TEXT,
historical_instances TEXT,
suggested_trades TEXT,
asset_class TEXT,
expected_move_pct REAL,
probability REAL,
horizon_days INTEGER,
ai_quality_score INTEGER,
ai_evaluation TEXT,
source TEXT DEFAULT 'custom',
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)""")
# Migration: add source column if not present
try:
c.execute("ALTER TABLE custom_patterns ADD COLUMN source TEXT DEFAULT 'custom'")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now'))
)""")
c.execute("""CREATE TABLE IF NOT EXISTS pattern_score_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
pattern_id TEXT NOT NULL,
score INTEGER,
confidence INTEGER,
summary TEXT,
scored_at TEXT NOT NULL
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_psh_pattern ON pattern_score_history(pattern_id, scored_at DESC)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS cycle_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL UNIQUE,
started_at TEXT NOT NULL,
completed_at TEXT,
trigger TEXT DEFAULT 'auto',
patterns_suggested INTEGER DEFAULT 0,
patterns_added INTEGER DEFAULT 0,
patterns_scored INTEGER DEFAULT 0,
geo_score INTEGER,
dominant_regime TEXT,
commentary TEXT,
status TEXT DEFAULT 'running'
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_cr_started ON cycle_runs(started_at DESC)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS macro_regime_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
dominant TEXT NOT NULL,
scores_json TEXT NOT NULL,
reasons_json TEXT NOT NULL,
gauges_summary_json TEXT NOT NULL DEFAULT '{}'
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_mrh_ts ON macro_regime_history(timestamp DESC)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS geo_alert_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
run_id TEXT NOT NULL,
geo_score INTEGER NOT NULL,
top_patterns_json TEXT NOT NULL DEFAULT '[]',
news_count INTEGER DEFAULT 0
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_gah_ts ON geo_alert_history(timestamp DESC)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS trade_entry_prices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
pattern_id TEXT NOT NULL,
pattern_name TEXT,
underlying TEXT NOT NULL,
strategy TEXT,
entry_price REAL,
entry_date TEXT NOT NULL,
score_at_entry INTEGER DEFAULT 0,
latest_score INTEGER,
expected_move_pct REAL,
horizon_days INTEGER DEFAULT 30,
ev_at_entry REAL,
ev_net REAL,
trade_score REAL,
matched_profile TEXT,
last_seen_at TEXT
)""")
for col, definition in [
("latest_score", "INTEGER"),
("ev_at_entry", "REAL"),
("ev_net", "REAL"),
("trade_score", "REAL"),
("matched_profile", "TEXT"),
("last_seen_at", "TEXT"),
]:
try:
c.execute(f"ALTER TABLE trade_entry_prices ADD COLUMN {col} {definition}")
except Exception:
pass
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_tep_date ON trade_entry_prices(entry_date DESC)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS risk_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
min_score INTEGER NOT NULL DEFAULT 0,
min_gain_pct REAL NOT NULL DEFAULT 0,
color TEXT DEFAULT '#3b82f6',
enabled INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)""")
# Default risk profiles (seed only if table is empty)
existing_profiles = c.execute("SELECT COUNT(*) FROM risk_profiles").fetchone()[0]
if existing_profiles == 0:
default_profiles = [
("Conservateur", 50, 100.0, "#22c55e", 1, 0),
("Équilibré", 30, 250.0, "#3b82f6", 1, 1),
("Agressif", 15, 600.0, "#ef4444", 1, 2),
]
c.executemany(
"INSERT INTO risk_profiles (name, min_score, min_gain_pct, color, enabled, sort_order) VALUES (?,?,?,?,?,?)",
default_profiles
)
# Default config
defaults = {
"openai_api_key": "",
"newsapi_key": "",
"eia_api_key": "",
"fred_api_key": "",
"sources": json.dumps({
"reuters_world": {"enabled": True, "url": "https://feeds.reuters.com/reuters/worldNews", "name": "Reuters World"},
"reuters_business": {"enabled": True, "url": "https://feeds.reuters.com/reuters/businessNews", "name": "Reuters Business"},
"reuters_energy": {"enabled": True, "url": "https://feeds.reuters.com/reuters/USenergyNews", "name": "Reuters Commodities"},
"ap_top": {"enabled": True, "url": "https://feeds.apnews.com/rss/apf-topnews", "name": "AP Top News"},
"aljazeera": {"enabled": True, "url": "https://www.aljazeera.com/xml/rss/all.xml", "name": "Al Jazeera"},
"ft": {"enabled": False, "url": "https://www.ft.com/rss/home", "name": "Financial Times"},
"bloomberg": {"enabled": False, "url": "https://feeds.bloomberg.com/markets/news.rss", "name": "Bloomberg Markets"},
"newsapi": {"enabled": False, "url": "", "name": "NewsAPI (clé requise)", "requires_key": "newsapi_key"},
"gdelt": {"enabled": False, "url": "https://api.gdeltproject.org/api/v2/doc/doc?query=geopolitics&mode=artlist&format=json", "name": "GDELT Project (gratuit)"},
"eia": {"enabled": False, "url": "", "name": "EIA Energy (clé requise)", "requires_key": "eia_api_key"},
"fred": {"enabled": False, "url": "", "name": "FRED Macro Fed (clé requise)", "requires_key": "fred_api_key"},
"usda": {"enabled": False, "url": "https://apps.fas.usda.gov/psdonline/api/psd/crops", "name": "USDA Agriculture (gratuit)"},
"who": {"enabled": False, "url": "https://www.who.int/rss-feeds/news-english.xml", "name": "WHO Santé (gratuit)"},
"emdat": {"enabled": False, "url": "", "name": "EM-DAT Catastrophes (inscription requise)"},
"twitter_trump": {"enabled": False, "url": "", "name": "X/Twitter Trump (API payante)"},
}),
"ai_enabled": "false",
"ai_auto_rescore": "false",
"auto_cycle_enabled": "false",
"auto_cycle_hours": "3",
"auto_cycle_similarity_threshold": "0.30",
"min_ev_threshold": "0.0",
"min_score_threshold": "0",
}
for k, v in defaults.items():
c.execute("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)", (k, v))
c.execute("""CREATE TABLE IF NOT EXISTS ai_reasoning_traces (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
trace_type TEXT NOT NULL,
pattern_id TEXT,
input_context_json TEXT DEFAULT '{}',
output_json TEXT DEFAULT '{}',
reasoning_summary TEXT,
geo_score INTEGER,
macro_dominant TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_art_run ON ai_reasoning_traces(run_id)")
c.execute("CREATE INDEX IF NOT EXISTS idx_art_pattern ON ai_reasoning_traces(pattern_id, trace_type)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS ai_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_type TEXT NOT NULL DEFAULT 'portfolio',
days INTEGER NOT NULL DEFAULT 30,
stats_json TEXT DEFAULT '{}',
winners_json TEXT DEFAULT '[]',
losers_json TEXT DEFAULT '[]',
report_json TEXT DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_reports_type_date ON ai_reports(report_type, created_at)")
except Exception:
pass
c.execute("""CREATE TABLE IF NOT EXISTS knowledge_base (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
confidence INTEGER DEFAULT 50,
confirmation_count INTEGER DEFAULT 1,
status TEXT DEFAULT 'active',
tags TEXT DEFAULT '',
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
last_confirmed_at TEXT NOT NULL DEFAULT (datetime('now'))
)""")
c.execute("""CREATE TABLE IF NOT EXISTS reasoning_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version INTEGER NOT NULL DEFAULT 1,
narrative TEXT NOT NULL,
synthesis_json TEXT DEFAULT '{}',
sources_count INTEGER DEFAULT 0,
reports_used INTEGER DEFAULT 0,
trades_analyzed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)""")
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category, status)")
c.execute("CREATE INDEX IF NOT EXISTS idx_rs_version ON reasoning_state(version DESC)")
except Exception:
pass
conn.commit()
conn.close()
# ── Config ────────────────────────────────────────────────────────────────────
def get_config(key: str) -> Optional[str]:
conn = get_conn()
row = conn.execute("SELECT value FROM config WHERE key=?", (key,)).fetchone()
conn.close()
return row["value"] if row else None
def set_config(key: str, value: str):
conn = get_conn()
conn.execute(
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, datetime('now'))",
(key, value)
)
conn.commit()
conn.close()
if key == "openai_api_key":
os.environ["OPENAI_API_KEY"] = value
def get_all_config() -> Dict[str, str]:
conn = get_conn()
rows = conn.execute("SELECT key, value FROM config").fetchall()
conn.close()
result = {r["key"]: r["value"] for r in rows}
if "openai_api_key" in result and result["openai_api_key"]:
result["openai_api_key_set"] = True
result["openai_api_key"] = "***"
return result
def get_sources() -> Dict[str, Any]:
raw = get_config("sources")
return json.loads(raw) if raw else {}
def update_sources(sources: Dict[str, Any]):
set_config("sources", json.dumps(sources))
def save_pattern_scores(scores: List[Dict[str, Any]], meta: Dict[str, Any] = None) -> str:
"""Persist last AI scores and append a history snapshot. Returns run_id."""
from datetime import datetime as _dt
run_id = _dt.utcnow().isoformat()
data = {"scores": scores, "meta": meta or {}, "scored_at": run_id, "run_id": run_id}
set_config("last_pattern_scores", json.dumps(data))
conn = get_conn()
for sp in scores:
pid = sp.get("pattern_id", "")
if pid:
conn.execute(
"INSERT INTO pattern_score_history (run_id, pattern_id, score, confidence, summary, scored_at) VALUES (?,?,?,?,?,?)",
(run_id, pid, sp.get("score"), sp.get("confidence"), sp.get("summary", ""), run_id),
)
# Keep only the last 30 runs
conn.execute("""DELETE FROM pattern_score_history WHERE run_id NOT IN (
SELECT DISTINCT run_id FROM pattern_score_history ORDER BY scored_at DESC LIMIT 30
)""")
conn.commit()
conn.close()
return run_id
def get_pattern_scores() -> Dict[str, Any]:
"""Return last persisted AI scores."""
raw = get_config("last_pattern_scores")
if raw:
try:
return json.loads(raw)
except Exception:
pass
return {"scores": [], "meta": {}, "scored_at": None}
def get_score_deltas() -> Dict[str, int]:
"""Compute score change per pattern between the two most recent scoring runs."""
conn = get_conn()
runs = conn.execute(
"SELECT DISTINCT run_id FROM pattern_score_history ORDER BY scored_at DESC LIMIT 2"
).fetchall()
if len(runs) < 2:
conn.close()
return {}
latest_run, prev_run = runs[0]["run_id"], runs[1]["run_id"]
latest = {r["pattern_id"]: r["score"] for r in
conn.execute("SELECT pattern_id, score FROM pattern_score_history WHERE run_id=?", (latest_run,)).fetchall()}
prev = {r["pattern_id"]: r["score"] for r in
conn.execute("SELECT pattern_id, score FROM pattern_score_history WHERE run_id=?", (prev_run,)).fetchall()}
conn.close()
return {
pid: score - prev[pid]
for pid, score in latest.items()
if pid in prev and score is not None and prev[pid] is not None
}
def get_score_history(pattern_id: str, limit: int = 10) -> List[Dict[str, Any]]:
"""Return the last N score snapshots for a given pattern."""
conn = get_conn()
rows = conn.execute(
"SELECT score, confidence, summary, scored_at FROM pattern_score_history WHERE pattern_id=? ORDER BY scored_at DESC LIMIT ?",
(pattern_id, limit),
).fetchall()
conn.close()
return [dict(r) for r in rows]
def compute_pattern_similarity(patterns: List[Dict[str, Any]], threshold: float = 0.25) -> List[Dict[str, Any]]:
"""Return pairs of patterns with Jaccard keyword similarity above threshold."""
results = []
for i, p1 in enumerate(patterns):
kw1 = set(kw.lower() for kw in (p1.get("keywords") or []))
for j, p2 in enumerate(patterns):
if i >= j:
continue
kw2 = set(kw.lower() for kw in (p2.get("keywords") or []))
if not kw1 or not kw2:
continue
common = kw1 & kw2
union = kw1 | kw2
sim = len(common) / len(union) if union else 0.0
if sim >= threshold:
results.append({
"id_a": p1.get("id"), "name_a": p1.get("name"),
"id_b": p2.get("id"), "name_b": p2.get("name"),
"similarity": round(sim, 2),
"common_keywords": sorted(common)[:8],
})
return sorted(results, key=lambda x: -x["similarity"])
def get_analysis_config() -> Dict[str, Any]:
"""Return the AI analysis config: template, top_n, category_filter."""
raw = get_config("analysis_config")
if raw:
try:
return json.loads(raw)
except Exception:
pass
return {"top_n": 10, "category_filter": "all", "template": None}
def save_analysis_config(cfg: Dict[str, Any]):
set_config("analysis_config", json.dumps(cfg))
# ── Portfolio ─────────────────────────────────────────────────────────────────
IB_OPTIONS_FEE_PER_CONTRACT = 0.65
IB_MIN_FEE = 1.0
def compute_ib_fees(num_contracts: int) -> float:
return max(IB_MIN_FEE, num_contracts * IB_OPTIONS_FEE_PER_CONTRACT)
def add_position(pos: Dict[str, Any]) -> str:
import uuid
pos_id = pos.get("id") or f"POS-{uuid.uuid4().hex[:8].upper()}"
legs = pos.get("legs", [])
num_contracts = sum(abs(leg.get("quantity", 1)) for leg in legs)
ib_fees = compute_ib_fees(num_contracts)
conn = get_conn()
conn.execute("""INSERT INTO portfolio (
id, title, underlying, strategy, asset_class, entry_date, expiry_date,
expiry_days, legs, capital_invested, entry_underlying_price,
geo_trigger, rationale, status, notes, ib_fees_entry
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,'open',?,?)""", (
pos_id,
pos.get("title", pos.get("underlying", "")),
pos.get("underlying", ""),
pos.get("strategy", ""),
pos.get("asset_class", ""),
pos.get("entry_date", datetime.utcnow().isoformat()[:10]),
pos.get("expiry_date", ""),
pos.get("expiry_days", 90),
json.dumps(legs),
pos.get("capital_invested", 1000.0),
pos.get("entry_underlying_price"),
pos.get("geo_trigger", ""),
pos.get("rationale", ""),
pos.get("notes", ""),
ib_fees,
))
conn.commit()
conn.close()
return pos_id
def get_positions(status: str = "open") -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM portfolio WHERE status=? ORDER BY created_at DESC",
(status,)
).fetchall()
conn.close()
result = []
for r in rows:
d = dict(r)
d["legs"] = json.loads(d.get("legs", "[]"))
result.append(d)
return result
def close_position(pos_id: str, close_value: float) -> Dict[str, Any]:
conn = get_conn()
pos = conn.execute("SELECT * FROM portfolio WHERE id=?", (pos_id,)).fetchone()
if not pos:
conn.close()
return {"error": "Position non trouvée"}
legs = json.loads(pos["legs"])
num_contracts = sum(abs(leg.get("quantity", 1)) for leg in legs)
ib_exit = compute_ib_fees(num_contracts)
conn.execute("""UPDATE portfolio SET status='closed', close_date=?, close_value=?,
ib_fees_exit=? WHERE id=?""",
(datetime.utcnow().isoformat()[:10], close_value, ib_exit, pos_id))
conn.commit()
pnl = close_value - pos["capital_invested"] - pos["ib_fees_entry"] - ib_exit
conn.close()
return {"id": pos_id, "close_value": close_value, "ib_fees_exit": ib_exit, "pnl": pnl}
def update_position_notes(pos_id: str, notes: str):
conn = get_conn()
conn.execute("UPDATE portfolio SET notes=? WHERE id=?", (notes, pos_id))
conn.commit()
conn.close()
# ── Custom Patterns ────────────────────────────────────────────────────────────
def seed_builtin_patterns(builtin_patterns: List[Dict[str, Any]]):
"""Seed built-in patterns into DB (idempotent — skips existing IDs)."""
conn = get_conn()
existing = {r[0] for r in conn.execute("SELECT id FROM custom_patterns").fetchall()}
for p in builtin_patterns:
if p["id"] not in existing:
conn.execute("""INSERT INTO custom_patterns (
id, name, description, triggers, keywords, historical_instances,
suggested_trades, asset_class, expected_move_pct, probability,
horizon_days, source, is_active, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,'builtin',1,datetime('now'))""", (
p["id"],
p.get("name", ""),
p.get("description", ""),
json.dumps(p.get("triggers", [])),
json.dumps(p.get("keywords", [])),
json.dumps(p.get("historical_instances", [])),
json.dumps(p.get("suggested_trades", [])),
p.get("asset_class", "indices"),
p.get("expected_move_pct", 0),
p.get("probability", 0.5),
p.get("horizon_days", 30),
))
conn.commit()
conn.close()
def save_custom_pattern(pattern: Dict[str, Any]) -> str:
import uuid
pat_id = pattern.get("id") or f"P_USER_{uuid.uuid4().hex[:6].upper()}"
source = pattern.get("source", "custom")
conn = get_conn()
conn.execute("""INSERT OR REPLACE INTO custom_patterns (
id, name, description, triggers, keywords, historical_instances,
suggested_trades, asset_class, expected_move_pct, probability,
horizon_days, ai_quality_score, ai_evaluation, source, is_active, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,datetime('now'))""", (
pat_id,
pattern.get("name", ""),
pattern.get("description", ""),
json.dumps(pattern.get("triggers", [])),
json.dumps(pattern.get("keywords", [])),
json.dumps(pattern.get("historical_instances", [])),
json.dumps(pattern.get("suggested_trades", [])),
pattern.get("asset_class", "indices"),
pattern.get("expected_move_pct", 0),
pattern.get("probability", 0.5),
pattern.get("horizon_days", 30),
pattern.get("ai_quality_score"),
json.dumps(pattern.get("ai_evaluation", {})),
source,
))
conn.commit()
conn.close()
return pat_id
def get_custom_patterns() -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM custom_patterns WHERE is_active=1 ORDER BY source DESC, created_at DESC"
).fetchall()
conn.close()
result = []
for r in rows:
d = dict(r)
for f in ["triggers", "keywords", "historical_instances", "suggested_trades", "ai_evaluation"]:
d[f] = json.loads(d.get(f) or "[]")
result.append(d)
return result
def toggle_pattern_active(pat_id: str) -> bool:
"""Toggle is_active for a pattern. Returns the new state."""
conn = get_conn()
row = conn.execute("SELECT is_active FROM custom_patterns WHERE id=?", (pat_id,)).fetchone()
if not row:
conn.close()
return False
new_state = 0 if row["is_active"] else 1
conn.execute("UPDATE custom_patterns SET is_active=? WHERE id=?", (new_state, pat_id))
conn.commit()
conn.close()
return bool(new_state)
def delete_custom_pattern(pat_id: str):
conn = get_conn()
conn.execute("UPDATE custom_patterns SET is_active=0 WHERE id=?", (pat_id,))
conn.commit()
conn.close()
# ── Risk Profiles ─────────────────────────────────────────────────────────────
def get_risk_profiles(enabled_only: bool = False) -> List[Dict[str, Any]]:
conn = get_conn()
q = "SELECT * FROM risk_profiles"
if enabled_only:
q += " WHERE enabled=1"
q += " ORDER BY sort_order ASC, id ASC"
rows = conn.execute(q).fetchall()
conn.close()
return [dict(r) for r in rows]
def upsert_risk_profile(profile: Dict[str, Any]) -> int:
conn = get_conn()
pid = profile.get("id")
if pid:
conn.execute("""UPDATE risk_profiles
SET name=?, min_score=?, min_gain_pct=?, color=?, enabled=?, sort_order=?
WHERE id=?""", (
profile["name"], int(profile["min_score"]), float(profile["min_gain_pct"]),
profile.get("color", "#3b82f6"), 1 if profile.get("enabled", True) else 0,
int(profile.get("sort_order", 0)), pid,
))
else:
cur = conn.execute("""INSERT INTO risk_profiles (name, min_score, min_gain_pct, color, enabled, sort_order)
VALUES (?,?,?,?,?,?)""", (
profile["name"], int(profile["min_score"]), float(profile["min_gain_pct"]),
profile.get("color", "#3b82f6"), 1 if profile.get("enabled", True) else 0,
int(profile.get("sort_order", 0)),
))
pid = cur.lastrowid
conn.commit()
conn.close()
return pid
def delete_risk_profile(profile_id: int):
conn = get_conn()
conn.execute("DELETE FROM risk_profiles WHERE id=?", (profile_id,))
conn.commit()
conn.close()
def _compute_trade_score(score: int, gain_pct: float) -> tuple[float, float, float]:
"""
Returns (ev_gross, ev_net, trade_score) for a (score, gain_pct) pair.
- ev_gross = p × G (raw expected multiple)
- ev_net = p × G - (1-p) (net EV assuming total loss if wrong)
- trade_score = p×G / (p×G + (1-p)) × 100 (normalized 0-100)
"""
p = max(0.0, min(1.0, score / 100))
G = abs(gain_pct) / 100
ev_gross = round(p * G, 4)
ev_net = round(p * G - (1 - p), 4)
denom = p * G + (1 - p)
trade_score = round((p * G / denom * 100) if denom > 0 else 0.0, 1)
return ev_gross, ev_net, trade_score
def _matches_profile(score: int, gain_pct: float, profiles: List[Dict[str, Any]]) -> Optional[str]:
"""Return the name of the first enabled profile this trade satisfies, or None."""
for prof in profiles:
if not prof.get("enabled", True):
continue
if score >= prof["min_score"] and gain_pct >= prof["min_gain_pct"]:
return prof["name"]
return None
# ── Journal de Bord ────────────────────────────────────────────────────────────
def log_macro_regime(dominant: str, scores: Dict[str, Any], reasons: Dict[str, Any], gauges_summary: Dict[str, Any]):
"""Append a macro regime snapshot. Keeps last 90 days."""
conn = get_conn()
conn.execute("""INSERT INTO macro_regime_history
(timestamp, dominant, scores_json, reasons_json, gauges_summary_json)
VALUES (datetime('now'), ?, ?, ?, ?)""",
(dominant, json.dumps(scores), json.dumps(reasons), json.dumps(gauges_summary)))
conn.execute("""DELETE FROM macro_regime_history WHERE timestamp < datetime('now', '-90 days')""")
conn.commit()
conn.close()
def get_macro_regime_history(days: int = 15) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM macro_regime_history WHERE timestamp >= datetime('now', ?) ORDER BY timestamp DESC",
(f"-{days} days",)
).fetchall()
conn.close()
result = []
for r in rows:
d = dict(r)
d["scores"] = json.loads(d.pop("scores_json", "{}"))
d["reasons"] = json.loads(d.pop("reasons_json", "{}"))
d["gauges_summary"] = json.loads(d.pop("gauges_summary_json", "{}"))
result.append(d)
return result
def log_geo_alert(geo_score: int, top_patterns: List[Dict[str, Any]], news_count: int, run_id: str):
"""Append a geo alert snapshot tied to a scoring run."""
conn = get_conn()
conn.execute("""INSERT INTO geo_alert_history
(timestamp, run_id, geo_score, top_patterns_json, news_count)
VALUES (datetime('now'), ?, ?, ?, ?)""",
(run_id, geo_score, json.dumps(top_patterns[:10]), news_count))
conn.execute("DELETE FROM geo_alert_history WHERE timestamp < datetime('now', '-90 days')")
conn.commit()
conn.close()
def get_geo_alert_history(days: int = 30) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM geo_alert_history WHERE timestamp >= datetime('now', ?) ORDER BY timestamp DESC",
(f"-{days} days",)
).fetchall()
conn.close()
result = []
for r in rows:
d = dict(r)
d["top_patterns"] = json.loads(d.pop("top_patterns_json", "[]"))
result.append(d)
return result
def _normalize_yf_ticker(ticker: str) -> str:
"""Normalize ticker for yfinance.
- USD/KRW → USDKRW=X (slash-format forex pairs from GPT-4o)
- USDKRW → USDKRW=X (bare 6-char alphabetic forex pairs)
- CL=F, SPY, etc. → unchanged
"""
t = ticker.upper().strip()
if '/' in t:
parts = t.split('/')
if len(parts) == 2 and all(p.isalpha() and len(p) >= 2 for p in parts):
return parts[0] + parts[1] + '=X'
return t
if len(t) == 6 and t.isalpha():
return t + "=X"
return t
def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes: Dict[str, Any]):
"""
For each scored pattern's trade_rankings, record entry price if the trade
passes at least one enabled risk profile (min_score + min_gain_pct pair).
Deduplicates: one row per (pattern_id, underlying, strategy).
Falls back to yfinance for tickers not found in the quotes snapshot.
"""
import logging as _logging
_log = _logging.getLogger(__name__)
profiles = get_risk_profiles(enabled_only=True)
_log.info(f"[TradeLog] run_id={run_id} scored_patterns={len(scored_patterns)} profiles={len(profiles)}")
# Load original patterns as fallback for expected_move_pct
# (GPT-4o scored output doesn't include this field)
_orig_patterns = {p.get("id", ""): p for p in get_custom_patterns()}
# Build price map from pre-fetched quotes
price_map: Dict[str, float] = {}
for asset_class, items in quotes.items():
if isinstance(items, list):
for item in items:
if item.get("ticker") and item.get("price") is not None:
price_map[item["ticker"].upper()] = float(item["price"])
elif isinstance(items, dict):
for ticker, item in items.items():
if isinstance(item, dict) and item.get("price") is not None:
price_map[ticker.upper()] = float(item["price"])
# Collect tickers NOT already in price_map for yfinance fallback
tickers_to_fetch: set = set()
for sp in scored_patterns:
for trade in sp.get("trade_rankings") or sp.get("suggested_trades", []):
t = (trade.get("underlying") or trade.get("ticker", "")).upper()
if t and t not in price_map:
tickers_to_fetch.add(t)
if tickers_to_fetch:
_log.info(f"[TradeLog] yfinance fallback for {len(tickers_to_fetch)} tickers: {sorted(tickers_to_fetch)}")
try:
import yfinance as yf
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
def _fetch(ticker: str):
normalized = _normalize_yf_ticker(ticker)
try:
# Use yf.download() — fresh HTTP request, no in-process Ticker cache
for kwargs in [
{"period": "1d", "interval": "5m"},
{"period": "5d", "interval": "1d"},
]:
df = yf.download(normalized, progress=False, auto_adjust=True, **kwargs)
if df.empty:
continue
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
if "Close" not in df.columns:
continue
close = df["Close"].dropna()
if close.empty:
continue
price = float(close.iloc[-1])
if price > 0:
_log.debug(f"[TradeLog] {ticker}{normalized} = {price}")
return ticker, price
except Exception as e:
_log.warning(f"[TradeLog] Failed to fetch '{ticker}' (normalized='{normalized}'): {e}")
return ticker, None
with ThreadPoolExecutor(max_workers=min(len(tickers_to_fetch), 10)) as ex:
for fut in as_completed({ex.submit(_fetch, t): t for t in tickers_to_fetch}, timeout=20):
try:
tk, price = fut.result()
if price is not None:
price_map[tk] = price
except Exception:
pass
except Exception as e:
_log.error(f"[TradeLog] yfinance fallback failed: {e}")
conn = get_conn()
today = datetime.utcnow().isoformat()[:10]
now_ts = datetime.utcnow().isoformat()
inserted_count = 0
updated_count = 0
skipped_no_profile = 0
for sp in scored_patterns:
pid = sp.get("pattern_id", "")
pattern_name = sp.get("geo_trigger") or sp.get("pattern_name") or pid
base_score = int(sp.get("score") or 0)
_orig = _orig_patterns.get(pid, {})
for trade in sp.get("trade_rankings") or sp.get("suggested_trades", []):
underlying = trade.get("underlying") or trade.get("ticker", "")
if not underlying:
continue
strategy = trade.get("strategy") or trade.get("trade_type", "")
delta = int(trade.get("score_delta") or 0)
eff_score = max(0, min(100, base_score + delta))
# Fallback chain: trade field → scored sp field → auto_cycle enrichment → original DB pattern
exp_move = abs(float(
trade.get("expected_move_pct") or
sp.get("expected_move_pct") or
_orig.get("expected_move_pct") or
0
))
if exp_move == 0:
_log.warning(f"[TradeLog] Pattern '{pattern_name}' trade {underlying} has expected_move_pct=0 — all profiles with min_gain_pct>0 will fail")
# Check if this trade passes any enabled risk profile
matched = _matches_profile(eff_score, exp_move, profiles)
if matched is None:
skipped_no_profile += 1
_log.debug(f"[TradeLog] SKIP {underlying} score={eff_score} gain={exp_move:.0f}% — no profile match")
continue
ev_gross, ev_net, trade_score = _compute_trade_score(eff_score, exp_move)
ticker_key = underlying.upper()
entry_price = price_map.get(ticker_key)
horizon = int(trade.get("horizon_days") or sp.get("horizon_days") or 30)
existing_row = conn.execute(
"SELECT id FROM trade_entry_prices WHERE pattern_id=? AND underlying=? AND strategy=?",
(pid, ticker_key, strategy)
).fetchone()
if existing_row:
conn.execute(
"UPDATE trade_entry_prices SET latest_score=?, trade_score=?, last_seen_at=? WHERE id=?",
(eff_score, trade_score, now_ts, existing_row["id"])
)
updated_count += 1
else:
conn.execute("""INSERT INTO trade_entry_prices
(run_id, pattern_id, pattern_name, underlying, strategy,
entry_price, entry_date, score_at_entry, latest_score,
expected_move_pct, horizon_days, ev_at_entry, ev_net,
trade_score, matched_profile, last_seen_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (
run_id, pid, pattern_name, ticker_key, strategy,
entry_price, today, eff_score, eff_score,
exp_move, horizon, ev_gross, ev_net,
trade_score, matched, now_ts,
))
inserted_count += 1
_log.info(f"[TradeLog] NEW trade: pattern='{pattern_name}' {underlying} {strategy} score={eff_score} gain={exp_move:.0f}% profile='{matched}' price={entry_price}")
_log.info(f"[TradeLog] Done — inserted={inserted_count} updated={updated_count} skipped_no_profile={skipped_no_profile}")
conn.execute("DELETE FROM trade_entry_prices WHERE entry_date < date('now', '-90 days')")
conn.commit()
conn.close()
def reset_journal_history():
"""Truncate all journal history tables for a clean slate."""
conn = get_conn()
conn.execute("DELETE FROM trade_entry_prices")
conn.execute("DELETE FROM macro_regime_history")
conn.execute("DELETE FROM geo_alert_history")
conn.execute("DELETE FROM cycle_runs")
conn.commit()
conn.close()
def add_cycle_run(run_id: str, trigger: str = "auto") -> None:
conn = get_conn()
conn.execute(
"INSERT OR IGNORE INTO cycle_runs (run_id, started_at, trigger, status) VALUES (?, datetime('now'), ?, 'running')",
(run_id, trigger)
)
conn.commit()
conn.close()
def update_cycle_run(run_id: str, **fields) -> None:
if not fields:
return
allowed = {"completed_at", "patterns_suggested", "patterns_added", "patterns_scored",
"geo_score", "dominant_regime", "commentary", "status"}
sets = ", ".join(f"{k}=?" for k in fields if k in allowed)
vals = [v for k, v in fields.items() if k in allowed]
if not sets:
return
conn = get_conn()
conn.execute(f"UPDATE cycle_runs SET {sets} WHERE run_id=?", vals + [run_id])
conn.commit()
conn.close()
def get_cycle_runs(limit: int = 30) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM cycle_runs ORDER BY started_at DESC LIMIT ?", (limit,)
).fetchall()
conn.close()
return [dict(r) for r in rows]
def get_cycle_run(run_id: str) -> Optional[Dict[str, Any]]:
conn = get_conn()
row = conn.execute("SELECT * FROM cycle_runs WHERE run_id=?", (run_id,)).fetchone()
conn.close()
return dict(row) if row else None
def get_trade_entry_prices(days: int = 30) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"""SELECT * FROM trade_entry_prices
WHERE entry_date >= date('now', ?)
ORDER BY entry_date DESC, score_at_entry DESC""",
(f"-{days} days",)
).fetchall()
conn.close()
return [dict(r) for r in rows]
def get_trade_entry_by_id(trade_id: int) -> Optional[Dict[str, Any]]:
conn = get_conn()
row = conn.execute("SELECT * FROM trade_entry_prices WHERE id=?", (trade_id,)).fetchone()
conn.close()
return dict(row) if row else None
# ── AI Reasoning Traces ───────────────────────────────────────────────────────
def save_reasoning_trace(
run_id: str,
trace_type: str,
pattern_id: str = None,
input_context: Dict[str, Any] = None,
output: Dict[str, Any] = None,
reasoning_summary: str = None,
geo_score: int = None,
macro_dominant: str = None,
) -> int:
"""Persist one AI reasoning step. Returns the new row id."""
conn = get_conn()
cur = conn.execute(
"""INSERT INTO ai_reasoning_traces
(run_id, trace_type, pattern_id, input_context_json, output_json,
reasoning_summary, geo_score, macro_dominant)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(
run_id, trace_type, pattern_id,
json.dumps(input_context or {}, ensure_ascii=False, default=str),
json.dumps(output or {}, ensure_ascii=False, default=str),
reasoning_summary, geo_score, macro_dominant,
),
)
row_id = cur.lastrowid
conn.commit()
conn.close()
return row_id
def _parse_trace(row) -> Dict[str, Any]:
"""Deserialize a reasoning trace row."""
d = dict(row)
for field in ("input_context_json", "output_json"):
try:
d[field.replace("_json", "")] = json.loads(d.get(field) or "{}")
except Exception:
d[field.replace("_json", "")] = {}
return d
def get_scoring_trace(run_id: str, pattern_id: str) -> Optional[Dict[str, Any]]:
"""Return the scoring trace for a given (run_id, pattern_id) pair."""
conn = get_conn()
row = conn.execute(
"SELECT * FROM ai_reasoning_traces WHERE run_id=? AND pattern_id=? AND trace_type='scoring'",
(run_id, pattern_id),
).fetchone()
conn.close()
return _parse_trace(row) if row else None
def get_suggestion_trace(pattern_id: str) -> Optional[Dict[str, Any]]:
"""Return the original suggestion trace for a pattern (first one ever)."""
conn = get_conn()
row = conn.execute(
"""SELECT * FROM ai_reasoning_traces
WHERE pattern_id=? AND trace_type='suggestion'
ORDER BY created_at ASC LIMIT 1""",
(pattern_id,),
).fetchone()
conn.close()
return _parse_trace(row) if row else None
def get_pattern_scoring_history(pattern_id: str, limit: int = 10) -> List[Dict[str, Any]]:
"""All scoring traces for a pattern across cycles — for trend analysis."""
conn = get_conn()
rows = conn.execute(
"""SELECT * FROM ai_reasoning_traces
WHERE pattern_id=? AND trace_type='scoring'
ORDER BY created_at DESC LIMIT ?""",
(pattern_id, limit),
).fetchall()
conn.close()
return [_parse_trace(r) for r in rows]
_BEARISH_KEYWORDS = {"bear", "put", "short", "sell", "vente", "baissier"}
def _fetch_live_prices(tickers: List[str], timeout: int = 20) -> Dict[str, Optional[float]]:
"""
Fetch current prices for a list of tickers using yf.download().
Shared by journal MTM and portfolio report so both get consistent live data.
"""
result: Dict[str, Optional[float]] = {t: None for t in tickers}
if not tickers:
return result
try:
import yfinance as yf
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
def _one(ticker: str) -> tuple:
for kwargs in [
{"period": "1d", "interval": "5m"},
{"period": "5d", "interval": "1d"},
]:
try:
df = yf.download(ticker, progress=False, auto_adjust=True, **kwargs)
if df.empty:
continue
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
if "Close" not in df.columns:
continue
close = df["Close"].dropna()
if close.empty:
continue
price = float(close.iloc[-1])
if price > 0:
return ticker, price
except Exception:
continue
return ticker, None
with ThreadPoolExecutor(max_workers=min(len(tickers), 10)) as ex:
futs = {ex.submit(_one, t): t for t in tickers}
from concurrent.futures import as_completed as _ac
for fut in _ac(futs, timeout=timeout):
try:
tk, price = fut.result()
result[tk] = price
except Exception:
pass
except Exception:
pass
return result
def get_mtm_trades_with_traces(days: int = 90, limit_movers: int = 5) -> Dict[str, Any]:
"""
Return all MTM trades with live prices and reasoning traces for the
top winners and losers (by pnl_pct). Used by the AI portfolio report.
"""
from datetime import timedelta
cutoff_date = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d")
conn = get_conn()
rows = conn.execute(
"""SELECT * FROM trade_entry_prices
WHERE entry_date >= ?
ORDER BY entry_date DESC""",
(cutoff_date,),
).fetchall()
conn.close()
all_trades = [dict(r) for r in rows]
# Fetch live prices for all unique tickers
tickers = list({(t.get("underlying") or "").upper() for t in all_trades if t.get("underlying")})
live_prices = _fetch_live_prices(tickers, timeout=25)
# Enrich trades with live price + pnl_pct
def _with_pnl(trade: Dict) -> Dict:
ticker = (trade.get("underlying") or "").upper()
entry = trade.get("entry_price")
current = live_prices.get(ticker)
pnl_pct = None
if entry and current and entry > 0:
raw = (current - entry) / entry * 100
strategy = trade.get("strategy", "").lower()
bearish = any(k in strategy for k in _BEARISH_KEYWORDS)
pnl_pct = round(-raw if bearish else raw, 2)
return {**trade, "current_price": current, "pnl_pct": pnl_pct}
enriched = [_with_pnl(t) for t in all_trades]
priced = [t for t in enriched if t.get("pnl_pct") is not None]
winners = sorted(priced, key=lambda t: t.get("pnl_pct", 0), reverse=True)[:limit_movers]
losers = sorted(priced, key=lambda t: t.get("pnl_pct", 0))[:limit_movers]
def _with_traces(trade: Dict) -> Dict:
pid = trade.get("pattern_id", "")
run_id = trade.get("run_id", "")
sc = get_scoring_trace(run_id, pid) if run_id and pid else None
sg = get_suggestion_trace(pid) if pid else None
history = get_pattern_scoring_history(pid, limit=5) if pid else []
return {
**trade,
"scoring_context": sc,
"suggestion_context": sg,
"score_history_count": len(history),
"score_trend": [h["output"].get("score") for h in reversed(history)] if history else [],
}
return {
"total_trades": len(all_trades),
"priced_count": len(priced),
"avg_pnl_pct": (sum(t.get("pnl_pct", 0) for t in priced) / len(priced)) if priced else None,
"winners": [_with_traces(t) for t in winners],
"losers": [_with_traces(t) for t in losers],
"all_trades": enriched,
}
def save_ai_report(
days: int,
stats: Dict[str, Any],
winners: List[Dict],
losers: List[Dict],
report: Dict[str, Any],
report_type: str = "portfolio",
) -> int:
conn = get_conn()
cur = conn.execute(
"""INSERT INTO ai_reports
(report_type, days, stats_json, winners_json, losers_json, report_json)
VALUES (?, ?, ?, ?, ?, ?)""",
(
report_type, days,
json.dumps(stats, ensure_ascii=False, default=str),
json.dumps(winners, ensure_ascii=False, default=str),
json.dumps(losers, ensure_ascii=False, default=str),
json.dumps(report, ensure_ascii=False, default=str),
),
)
row_id = cur.lastrowid
conn.commit()
conn.close()
return row_id
def _parse_report(row) -> Dict[str, Any]:
d = dict(row)
for field in ("stats_json", "winners_json", "losers_json", "report_json"):
key = field.replace("_json", "")
try:
d[key] = json.loads(d.get(field) or "{}")
except Exception:
d[key] = {}
return d
def get_latest_portfolio_lessons() -> Optional[Dict[str, Any]]:
"""
Return the key lessons from the most recent portfolio report.
Used by auto_cycle to inject feedback into the next AI cycle's prompts.
Returns None if no report exists yet.
"""
conn = get_conn()
row = conn.execute(
"""SELECT report_json, stats_json, created_at, days
FROM ai_reports WHERE report_type='portfolio'
ORDER BY created_at DESC LIMIT 1"""
).fetchone()
conn.close()
if not row:
return None
try:
report = json.loads(row["report_json"] or "{}")
stats = json.loads(row["stats_json"] or "{}")
except Exception:
return None
if not report:
return None
return {
"created_at": row["created_at"],
"days": row["days"],
"stats": stats,
"headline": report.get("headline", ""),
"winners_analysis": report.get("winners_analysis", ""),
"losers_analysis": report.get("losers_analysis", ""),
"key_lessons": report.get("key_lessons", []),
"blind_spots": report.get("blind_spots", ""),
"next_cycle_priorities": report.get("next_cycle_priorities", ""),
"risk_watch": report.get("risk_watch", ""),
}
def list_ai_reports(report_type: str = "portfolio", limit: int = 20) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"""SELECT id, report_type, days, stats_json, report_json, created_at
FROM ai_reports
WHERE report_type=?
ORDER BY created_at DESC LIMIT ?""",
(report_type, limit),
).fetchall()
conn.close()
result = []
for row in rows:
d = dict(row)
for field in ("stats_json", "report_json"):
key = field.replace("_json", "")
try:
d[key] = json.loads(d.get(field) or "{}")
except Exception:
d[key] = {}
result.append(d)
return result
def get_ai_report(report_id: int) -> Optional[Dict[str, Any]]:
conn = get_conn()
row = conn.execute("SELECT * FROM ai_reports WHERE id=?", (report_id,)).fetchone()
conn.close()
return _parse_report(row) if row else None
# ── Knowledge Base ─────────────────────────────────────────────────────────────
def save_kb_entry(category: str, title: str, content: str, confidence: int = 50,
tags: str = "", existing_id: Optional[int] = None) -> int:
conn = get_conn()
now = datetime.utcnow().isoformat()
if existing_id:
conn.execute("""UPDATE knowledge_base SET content=?, confidence=?, tags=?,
last_confirmed_at=?, confirmation_count=confirmation_count+1
WHERE id=?""", (content, confidence, tags, now, existing_id))
conn.commit()
conn.close()
return existing_id
cur = conn.execute("""INSERT INTO knowledge_base
(category, title, content, confidence, confirmation_count, status, tags, first_seen_at, last_confirmed_at)
VALUES (?,?,?,?,1,'active',?,?,?)""",
(category, title, content, confidence, tags, now, now))
new_id = cur.lastrowid
conn.commit()
conn.close()
return new_id
def get_kb_entries(status: str = "active") -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM knowledge_base WHERE status=? ORDER BY confidence DESC, last_confirmed_at DESC",
(status,)
).fetchall()
conn.close()
return [dict(r) for r in rows]
def get_all_kb_entries() -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT * FROM knowledge_base ORDER BY confidence DESC, last_confirmed_at DESC"
).fetchall()
conn.close()
return [dict(r) for r in rows]
def update_kb_entry_status(entry_id: int, status: str):
conn = get_conn()
conn.execute("UPDATE knowledge_base SET status=? WHERE id=?", (status, entry_id))
conn.commit()
conn.close()
# ── Reasoning State ────────────────────────────────────────────────────────────
def save_reasoning_state(narrative: str, synthesis: Dict[str, Any],
sources_count: int = 0, reports_used: int = 0,
trades_analyzed: int = 0) -> int:
conn = get_conn()
cur_row = conn.execute(
"SELECT COALESCE(MAX(version), 0) as v FROM reasoning_state"
).fetchone()
next_version = (cur_row["v"] if cur_row else 0) + 1
cur = conn.execute("""INSERT INTO reasoning_state
(version, narrative, synthesis_json, sources_count, reports_used, trades_analyzed, created_at)
VALUES (?,?,?,?,?,?,datetime('now'))""",
(next_version, narrative, json.dumps(synthesis), sources_count, reports_used, trades_analyzed))
new_id = cur.lastrowid
conn.commit()
conn.close()
return new_id
def get_latest_reasoning_state() -> Optional[Dict[str, Any]]:
conn = get_conn()
row = conn.execute(
"SELECT * FROM reasoning_state ORDER BY version DESC LIMIT 1"
).fetchone()
conn.close()
if not row:
return None
d = dict(row)
try:
d["synthesis"] = json.loads(d.get("synthesis_json") or "{}")
except Exception:
d["synthesis"] = {}
return d
def get_reasoning_history(limit: int = 10) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"SELECT id, version, sources_count, reports_used, trades_analyzed, created_at FROM reasoning_state ORDER BY version DESC LIMIT ?",
(limit,)
).fetchall()
conn.close()
return [dict(r) for r in rows]
def get_reasoning_state_by_id(state_id: int) -> Optional[Dict[str, Any]]:
conn = get_conn()
row = conn.execute("SELECT * FROM reasoning_state WHERE id=?", (state_id,)).fetchone()
conn.close()
if not row:
return None
d = dict(row)
try:
d["synthesis"] = json.loads(d.get("synthesis_json") or "{}")
except Exception:
d["synthesis"] = {}
return d