store.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. from __future__ import annotations
  2. import json
  3. import sqlite3
  4. from datetime import datetime, timezone
  5. from pathlib import Path
  6. from typing import Any
  7. ROOT = Path(__file__).resolve().parents[2]
  8. DATA_DIR = ROOT / "data"
  9. DB_PATH = DATA_DIR / "hermes_mcp.sqlite3"
  10. SCHEMA_STATEMENTS = [
  11. """
  12. create table if not exists concerns (
  13. id text primary key,
  14. account_id text,
  15. market_symbol text,
  16. base_currency text,
  17. quote_currency text,
  18. strategy_id text,
  19. decision_profile_id text,
  20. source text not null,
  21. status text not null default 'active',
  22. notes text,
  23. created_at text not null,
  24. updated_at text not null
  25. )
  26. """,
  27. """
  28. create table if not exists decision_profiles (
  29. id text primary key,
  30. name text not null,
  31. description text,
  32. config_json text not null,
  33. status text not null default 'active',
  34. created_at text not null,
  35. updated_at text not null
  36. )
  37. """,
  38. """
  39. create table if not exists strategy_groups (
  40. id text primary key,
  41. concern_id text not null,
  42. name text not null,
  43. strategy_family text,
  44. decision_profile_id text,
  45. notes text,
  46. status text not null default 'active',
  47. created_at text not null,
  48. updated_at text not null,
  49. foreign key(concern_id) references concerns(id) on delete cascade,
  50. foreign key(decision_profile_id) references decision_profiles(id)
  51. )
  52. """,
  53. """
  54. create table if not exists strategy_assignments (
  55. id text primary key,
  56. strategy_group_id text not null,
  57. strategy_id text not null,
  58. strategy_type text,
  59. role text,
  60. status text not null default 'active',
  61. notes text,
  62. created_at text not null,
  63. updated_at text not null,
  64. foreign key(strategy_group_id) references strategy_groups(id) on delete cascade
  65. )
  66. """,
  67. """
  68. create table if not exists cycles (
  69. id text primary key,
  70. started_at text not null,
  71. finished_at text,
  72. status text not null default 'running',
  73. trigger text not null,
  74. notes text
  75. )
  76. """,
  77. """
  78. create table if not exists observations (
  79. id text primary key,
  80. cycle_id text not null,
  81. concern_id text,
  82. source text not null,
  83. kind text not null,
  84. payload_json text not null,
  85. observed_at text not null,
  86. foreign key(cycle_id) references cycles(id),
  87. foreign key(concern_id) references concerns(id)
  88. )
  89. """,
  90. """
  91. create table if not exists states (
  92. id text primary key,
  93. cycle_id text not null,
  94. concern_id text not null,
  95. market_regime text,
  96. volatility_state text,
  97. liquidity_state text,
  98. sentiment_pressure text,
  99. event_risk text,
  100. execution_quality text,
  101. confidence real,
  102. payload_json text not null,
  103. created_at text not null,
  104. foreign key(cycle_id) references cycles(id),
  105. foreign key(concern_id) references concerns(id)
  106. )
  107. """,
  108. """
  109. create table if not exists narratives (
  110. id text primary key,
  111. cycle_id text not null,
  112. concern_id text not null,
  113. summary text not null,
  114. key_drivers_json text not null,
  115. risk_flags_json text not null,
  116. uncertainties_json text not null,
  117. confidence real,
  118. created_at text not null,
  119. foreign key(cycle_id) references cycles(id),
  120. foreign key(concern_id) references concerns(id)
  121. )
  122. """,
  123. """
  124. create table if not exists decisions (
  125. id text primary key,
  126. cycle_id text not null,
  127. concern_id text not null,
  128. mode text not null,
  129. action text not null,
  130. target_strategy text,
  131. target_policy_json text,
  132. reason_summary text,
  133. confidence real,
  134. requires_action integer not null default 0,
  135. created_at text not null,
  136. foreign key(cycle_id) references cycles(id),
  137. foreign key(concern_id) references concerns(id)
  138. )
  139. """,
  140. """
  141. create table if not exists actions (
  142. id text primary key,
  143. decision_id text not null,
  144. target text not null,
  145. command text not null,
  146. request_json text not null,
  147. response_json text,
  148. status text not null default 'pending',
  149. executed_at text,
  150. foreign key(decision_id) references decisions(id)
  151. )
  152. """,
  153. """
  154. create table if not exists coverage_gaps (
  155. id text primary key,
  156. cycle_id text not null,
  157. concern_id text,
  158. gap_type text not null,
  159. summary text not null,
  160. recommendation_json text not null,
  161. status text not null default 'open',
  162. created_at text not null,
  163. foreign key(cycle_id) references cycles(id),
  164. foreign key(concern_id) references concerns(id)
  165. )
  166. """,
  167. """
  168. create table if not exists regime_samples (
  169. id text primary key,
  170. cycle_id text not null,
  171. concern_id text not null,
  172. timeframe text not null,
  173. regime_json text not null,
  174. captured_at text not null,
  175. foreign key(cycle_id) references cycles(id),
  176. foreign key(concern_id) references concerns(id)
  177. )
  178. """,
  179. "create index if not exists idx_observations_cycle on observations(cycle_id)",
  180. "create index if not exists idx_observations_concern on observations(concern_id)",
  181. "create index if not exists idx_concerns_profile on concerns(decision_profile_id)",
  182. "create index if not exists idx_decision_profiles_status on decision_profiles(status)",
  183. "create index if not exists idx_strategy_groups_concern on strategy_groups(concern_id)",
  184. "create index if not exists idx_strategy_groups_profile on strategy_groups(decision_profile_id)",
  185. "create index if not exists idx_strategy_assignments_group on strategy_assignments(strategy_group_id)",
  186. "create index if not exists idx_strategy_assignments_strategy on strategy_assignments(strategy_id)",
  187. "create index if not exists idx_states_cycle on states(cycle_id)",
  188. "create index if not exists idx_states_concern on states(concern_id)",
  189. "create index if not exists idx_narratives_cycle on narratives(cycle_id)",
  190. "create index if not exists idx_decisions_cycle on decisions(cycle_id)",
  191. "create index if not exists idx_actions_decision on actions(decision_id)",
  192. "create index if not exists idx_gaps_cycle on coverage_gaps(cycle_id)",
  193. "create index if not exists idx_regime_samples_concern on regime_samples(concern_id)",
  194. ]
  195. def _now() -> str:
  196. return datetime.now(timezone.utc).isoformat()
  197. def _connect() -> sqlite3.Connection:
  198. DATA_DIR.mkdir(parents=True, exist_ok=True)
  199. conn = sqlite3.connect(DB_PATH)
  200. conn.row_factory = sqlite3.Row
  201. conn.execute("pragma foreign_keys = on")
  202. return conn
  203. def init_db() -> None:
  204. with _connect() as conn:
  205. for stmt in SCHEMA_STATEMENTS:
  206. if stmt.lstrip().lower().startswith("create index"):
  207. continue
  208. conn.execute(stmt)
  209. conn.execute(
  210. """
  211. create table if not exists state (
  212. key text primary key,
  213. value text not null,
  214. updated_at text not null default current_timestamp
  215. )
  216. """
  217. )
  218. concern_columns = {row[1] for row in conn.execute("pragma table_info(concerns)").fetchall()}
  219. for column in ("base_currency", "quote_currency", "decision_profile_id"):
  220. if column not in concern_columns:
  221. conn.execute(f"alter table concerns add column {column} text")
  222. for stmt in SCHEMA_STATEMENTS:
  223. if not stmt.lstrip().lower().startswith("create index"):
  224. continue
  225. conn.execute(stmt)
  226. def get_state() -> dict[str, Any]:
  227. init_db()
  228. with _connect() as conn:
  229. row = conn.execute("select value from state where key = ?", ("snapshot",)).fetchone()
  230. if not row:
  231. return {
  232. "status": "stub",
  233. "thinking": "Hermes is scaffolded and waiting for integrations.",
  234. "layers": ["concerns", "decision_profiles", "strategy_groups", "strategy_assignments", "cycles", "observations", "states", "narratives", "decisions", "actions", "coverage_gaps"],
  235. }
  236. return json.loads(row["value"])
  237. def put_state(payload: dict[str, Any]) -> None:
  238. init_db()
  239. with _connect() as conn:
  240. conn.execute(
  241. "insert into state(key, value, updated_at) values(?, ?, current_timestamp) on conflict(key) do update set value=excluded.value, updated_at=current_timestamp",
  242. ("snapshot", json.dumps(payload)),
  243. )
  244. def upsert_concern(*, id: str, account_id: str | None, market_symbol: str | None, base_currency: str | None = None, quote_currency: str | None = None, strategy_id: str | None, decision_profile_id: str | None = None, source: str, status: str = "active", notes: str | None = None) -> None:
  245. init_db()
  246. now = _now()
  247. with _connect() as conn:
  248. conn.execute(
  249. """
  250. insert into concerns(id, account_id, market_symbol, base_currency, quote_currency, strategy_id, decision_profile_id, source, status, notes, created_at, updated_at)
  251. values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  252. on conflict(id) do update set
  253. account_id=excluded.account_id,
  254. market_symbol=excluded.market_symbol,
  255. base_currency=excluded.base_currency,
  256. quote_currency=excluded.quote_currency,
  257. strategy_id=excluded.strategy_id,
  258. decision_profile_id=excluded.decision_profile_id,
  259. source=excluded.source,
  260. status=excluded.status,
  261. notes=excluded.notes,
  262. updated_at=excluded.updated_at
  263. """,
  264. (id, account_id, market_symbol, base_currency, quote_currency, strategy_id, decision_profile_id, source, status, notes, now, now),
  265. )
  266. def list_concerns() -> list[dict[str, Any]]:
  267. init_db()
  268. with _connect() as conn:
  269. rows = conn.execute("select * from concerns order by updated_at desc").fetchall()
  270. return [dict(r) for r in rows]
  271. def upsert_decision_profile(*, id: str, name: str, config: dict[str, Any], description: str | None = None, status: str = "active") -> None:
  272. init_db()
  273. now = _now()
  274. with _connect() as conn:
  275. conn.execute(
  276. """
  277. insert into decision_profiles(id, name, description, config_json, status, created_at, updated_at)
  278. values(?, ?, ?, ?, ?, ?, ?)
  279. on conflict(id) do update set
  280. name=excluded.name,
  281. description=excluded.description,
  282. config_json=excluded.config_json,
  283. status=excluded.status,
  284. updated_at=excluded.updated_at
  285. """,
  286. (id, name, description, json.dumps(config, ensure_ascii=False), status, now, now),
  287. )
  288. def list_decision_profiles() -> list[dict[str, Any]]:
  289. init_db()
  290. with _connect() as conn:
  291. rows = conn.execute("select * from decision_profiles order by updated_at desc").fetchall()
  292. return [dict(r) for r in rows]
  293. def get_decision_profile(*, profile_id: str) -> dict[str, Any] | None:
  294. init_db()
  295. profile_id = str(profile_id or "").strip()
  296. if not profile_id:
  297. return None
  298. with _connect() as conn:
  299. row = conn.execute("select * from decision_profiles where id = ?", (profile_id,)).fetchone()
  300. return dict(row) if row else None
  301. def upsert_strategy_group(*, id: str, concern_id: str, name: str, strategy_family: str | None = None, decision_profile_id: str | None = None, notes: str | None = None, status: str = "active") -> None:
  302. init_db()
  303. now = _now()
  304. with _connect() as conn:
  305. conn.execute(
  306. """
  307. insert into strategy_groups(id, concern_id, name, strategy_family, decision_profile_id, notes, status, created_at, updated_at)
  308. values(?, ?, ?, ?, ?, ?, ?, ?, ?)
  309. on conflict(id) do update set
  310. concern_id=excluded.concern_id,
  311. name=excluded.name,
  312. strategy_family=excluded.strategy_family,
  313. decision_profile_id=excluded.decision_profile_id,
  314. notes=excluded.notes,
  315. status=excluded.status,
  316. updated_at=excluded.updated_at
  317. """,
  318. (id, concern_id, name, strategy_family, decision_profile_id, notes, status, now, now),
  319. )
  320. def list_strategy_groups(*, concern_id: str | None = None) -> list[dict[str, Any]]:
  321. init_db()
  322. with _connect() as conn:
  323. if concern_id:
  324. rows = conn.execute("select * from strategy_groups where concern_id = ? order by updated_at desc", (concern_id,)).fetchall()
  325. else:
  326. rows = conn.execute("select * from strategy_groups order by updated_at desc").fetchall()
  327. return [dict(r) for r in rows]
  328. def upsert_strategy_assignment(*, id: str, strategy_group_id: str, strategy_id: str, strategy_type: str | None = None, role: str | None = None, status: str = "active", notes: str | None = None) -> None:
  329. init_db()
  330. now = _now()
  331. with _connect() as conn:
  332. conn.execute(
  333. """
  334. insert into strategy_assignments(id, strategy_group_id, strategy_id, strategy_type, role, status, notes, created_at, updated_at)
  335. values(?, ?, ?, ?, ?, ?, ?, ?, ?)
  336. on conflict(id) do update set
  337. strategy_group_id=excluded.strategy_group_id,
  338. strategy_id=excluded.strategy_id,
  339. strategy_type=excluded.strategy_type,
  340. role=excluded.role,
  341. status=excluded.status,
  342. notes=excluded.notes,
  343. updated_at=excluded.updated_at
  344. """,
  345. (id, strategy_group_id, strategy_id, strategy_type, role, status, notes, now, now),
  346. )
  347. def list_strategy_assignments(*, strategy_group_id: str | None = None) -> list[dict[str, Any]]:
  348. init_db()
  349. with _connect() as conn:
  350. if strategy_group_id:
  351. rows = conn.execute("select * from strategy_assignments where strategy_group_id = ? order by updated_at desc", (strategy_group_id,)).fetchall()
  352. else:
  353. rows = conn.execute("select * from strategy_assignments order by updated_at desc").fetchall()
  354. return [dict(r) for r in rows]
  355. def delete_concern(*, concern_id: str) -> dict[str, int]:
  356. init_db()
  357. concern_id = str(concern_id or "").strip()
  358. if not concern_id:
  359. return {"concerns": 0, "strategy_groups": 0, "strategy_assignments": 0, "observations": 0, "states": 0, "narratives": 0, "decisions": 0, "actions": 0, "coverage_gaps": 0, "regime_samples": 0}
  360. deleted = {"concerns": 0, "strategy_groups": 0, "strategy_assignments": 0, "observations": 0, "states": 0, "narratives": 0, "decisions": 0, "actions": 0, "coverage_gaps": 0, "regime_samples": 0}
  361. with _connect() as conn:
  362. decision_ids = [row[0] for row in conn.execute("select id from decisions where concern_id = ?", (concern_id,)).fetchall()]
  363. group_ids = [row[0] for row in conn.execute("select id from strategy_groups where concern_id = ?", (concern_id,)).fetchall()]
  364. deleted["actions"] = conn.execute(
  365. f"delete from actions where decision_id in ({','.join('?' for _ in decision_ids)})",
  366. decision_ids,
  367. ).rowcount if decision_ids else 0
  368. deleted["strategy_assignments"] = conn.execute(
  369. f"delete from strategy_assignments where strategy_group_id in ({','.join('?' for _ in group_ids)})",
  370. group_ids,
  371. ).rowcount if group_ids else 0
  372. deleted["strategy_groups"] = conn.execute("delete from strategy_groups where concern_id = ?", (concern_id,)).rowcount or 0
  373. deleted["observations"] = conn.execute("delete from observations where concern_id = ?", (concern_id,)).rowcount or 0
  374. deleted["states"] = conn.execute("delete from states where concern_id = ?", (concern_id,)).rowcount or 0
  375. deleted["narratives"] = conn.execute("delete from narratives where concern_id = ?", (concern_id,)).rowcount or 0
  376. deleted["coverage_gaps"] = conn.execute("delete from coverage_gaps where concern_id = ?", (concern_id,)).rowcount or 0
  377. deleted["regime_samples"] = conn.execute("delete from regime_samples where concern_id = ?", (concern_id,)).rowcount or 0
  378. deleted["decisions"] = conn.execute("delete from decisions where concern_id = ?", (concern_id,)).rowcount or 0
  379. deleted["concerns"] = conn.execute("delete from concerns where id = ?", (concern_id,)).rowcount or 0
  380. return deleted
  381. def prune_older_than(days: int) -> dict[str, int]:
  382. init_db()
  383. cutoff = datetime.now(timezone.utc).timestamp() - (days * 86400)
  384. cutoff_iso = datetime.fromtimestamp(cutoff, tz=timezone.utc).isoformat()
  385. with _connect() as conn:
  386. deleted = {}
  387. for table in ("actions", "decisions", "narratives", "states", "observations", "coverage_gaps", "cycles"):
  388. if table == "actions":
  389. where = "executed_at is not null and executed_at < ?"
  390. elif table in {"decisions", "narratives", "states", "observations", "coverage_gaps", "cycles"}:
  391. where = "created_at < ?" if table != "cycles" else "started_at < ?"
  392. else:
  393. continue
  394. cur = conn.execute(f"delete from {table} where {where}", (cutoff_iso,))
  395. deleted[table] = cur.rowcount if cur.rowcount is not None else 0
  396. return deleted
  397. def sync_concerns_from_strategies(strategies: list[dict[str, Any]]) -> list[dict[str, Any]]:
  398. seen: set[str] = set()
  399. synced: list[dict[str, Any]] = []
  400. existing = {str(c.get("id") or ""): c for c in list_concerns()}
  401. for s in strategies:
  402. account_id = str(s.get("account_id") or "").strip() or None
  403. market_symbol = str(s.get("market_symbol") or "").strip() or None
  404. base_currency = str(s.get("base_currency") or "").strip() or None
  405. quote_currency = str(s.get("counter_currency") or s.get("quote_currency") or "").strip() or None
  406. strategy_id = str(s.get("id") or s.get("name") or s.get("strategy_type") or "").strip() or None
  407. if not account_id or not market_symbol:
  408. continue
  409. concern_id = f"{account_id}:{market_symbol}"
  410. if concern_id in seen:
  411. continue
  412. seen.add(concern_id)
  413. current = existing.get(concern_id, {})
  414. upsert_concern(
  415. id=concern_id,
  416. account_id=account_id,
  417. market_symbol=market_symbol,
  418. base_currency=base_currency,
  419. quote_currency=quote_currency,
  420. strategy_id=strategy_id,
  421. decision_profile_id=str(current.get("decision_profile_id") or "").strip() or None,
  422. source=str(current.get("source") or "trader_inventory"),
  423. status=str(current.get("status") or "active"),
  424. notes=str(current.get("notes") or "mirrored from trader strategy inventory").strip() or None,
  425. )
  426. synced.append({"id": concern_id, "account_id": account_id, "market_symbol": market_symbol, "base_currency": base_currency, "quote_currency": quote_currency, "strategy_id": strategy_id})
  427. return synced
  428. def table_counts() -> dict[str, int]:
  429. init_db()
  430. tables = ["concerns", "decision_profiles", "strategy_groups", "strategy_assignments", "cycles", "observations", "states", "narratives", "decisions", "actions", "coverage_gaps", "regime_samples"]
  431. out: dict[str, int] = {}
  432. with _connect() as conn:
  433. for table in tables:
  434. out[table] = int(conn.execute(f"select count(*) as n from {table}").fetchone()["n"])
  435. return out
  436. def latest_cycle() -> dict[str, Any] | None:
  437. init_db()
  438. with _connect() as conn:
  439. row = conn.execute("select * from cycles order by started_at desc limit 1").fetchone()
  440. return dict(row) if row else None
  441. def latest_cycles(limit: int = 20) -> list[dict[str, Any]]:
  442. init_db()
  443. with _connect() as conn:
  444. rows = conn.execute("select * from cycles order by started_at desc limit ?", (limit,)).fetchall()
  445. return [dict(r) for r in rows]
  446. def upsert_cycle(*, id: str, started_at: str, finished_at: str | None, status: str, trigger: str, notes: str | None = None) -> None:
  447. init_db()
  448. with _connect() as conn:
  449. conn.execute(
  450. """
  451. insert into cycles(id, started_at, finished_at, status, trigger, notes)
  452. values(?, ?, ?, ?, ?, ?)
  453. on conflict(id) do update set
  454. started_at=excluded.started_at,
  455. finished_at=excluded.finished_at,
  456. status=excluded.status,
  457. trigger=excluded.trigger,
  458. notes=excluded.notes
  459. """,
  460. (id, started_at, finished_at, status, trigger, notes),
  461. )
  462. def upsert_regime_sample(*, id: str, cycle_id: str, concern_id: str, timeframe: str, regime_json: str, captured_at: str) -> None:
  463. init_db()
  464. with _connect() as conn:
  465. conn.execute(
  466. """
  467. insert into regime_samples(id, cycle_id, concern_id, timeframe, regime_json, captured_at)
  468. values(?, ?, ?, ?, ?, ?)
  469. on conflict(id) do update set
  470. cycle_id=excluded.cycle_id,
  471. concern_id=excluded.concern_id,
  472. timeframe=excluded.timeframe,
  473. regime_json=excluded.regime_json,
  474. captured_at=excluded.captured_at
  475. """,
  476. (id, cycle_id, concern_id, timeframe, regime_json, captured_at),
  477. )
  478. def upsert_observation(*, id: str, cycle_id: str, concern_id: str | None, source: str, kind: str, payload_json: str, observed_at: str | None = None) -> None:
  479. init_db()
  480. observed_at = observed_at or _now()
  481. with _connect() as conn:
  482. conn.execute(
  483. """
  484. insert into observations(id, cycle_id, concern_id, source, kind, payload_json, observed_at)
  485. values(?, ?, ?, ?, ?, ?, ?)
  486. on conflict(id) do update set
  487. cycle_id=excluded.cycle_id,
  488. concern_id=excluded.concern_id,
  489. source=excluded.source,
  490. kind=excluded.kind,
  491. payload_json=excluded.payload_json,
  492. observed_at=excluded.observed_at
  493. """,
  494. (id, cycle_id, concern_id, source, kind, payload_json, observed_at),
  495. )
  496. def upsert_state(*, id: str, cycle_id: str, concern_id: str, market_regime: str | None, volatility_state: str | None, liquidity_state: str | None, sentiment_pressure: str | None, event_risk: str | None, execution_quality: str | None, confidence: float | None, payload_json: str, created_at: str | None = None) -> None:
  497. init_db()
  498. created_at = created_at or _now()
  499. with _connect() as conn:
  500. conn.execute(
  501. """
  502. insert into states(id, cycle_id, concern_id, market_regime, volatility_state, liquidity_state, sentiment_pressure, event_risk, execution_quality, confidence, payload_json, created_at)
  503. values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  504. on conflict(id) do update set
  505. cycle_id=excluded.cycle_id,
  506. concern_id=excluded.concern_id,
  507. market_regime=excluded.market_regime,
  508. volatility_state=excluded.volatility_state,
  509. liquidity_state=excluded.liquidity_state,
  510. sentiment_pressure=excluded.sentiment_pressure,
  511. event_risk=excluded.event_risk,
  512. execution_quality=excluded.execution_quality,
  513. confidence=excluded.confidence,
  514. payload_json=excluded.payload_json,
  515. created_at=excluded.created_at
  516. """,
  517. (id, cycle_id, concern_id, market_regime, volatility_state, liquidity_state, sentiment_pressure, event_risk, execution_quality, confidence, payload_json, created_at),
  518. )
  519. def upsert_narrative(*, id: str, cycle_id: str, concern_id: str, summary: str, key_drivers_json: str, risk_flags_json: str, uncertainties_json: str, confidence: float | None, created_at: str | None = None) -> None:
  520. init_db()
  521. created_at = created_at or _now()
  522. with _connect() as conn:
  523. conn.execute(
  524. """
  525. insert into narratives(id, cycle_id, concern_id, summary, key_drivers_json, risk_flags_json, uncertainties_json, confidence, created_at)
  526. values(?, ?, ?, ?, ?, ?, ?, ?, ?)
  527. on conflict(id) do update set
  528. cycle_id=excluded.cycle_id,
  529. concern_id=excluded.concern_id,
  530. summary=excluded.summary,
  531. key_drivers_json=excluded.key_drivers_json,
  532. risk_flags_json=excluded.risk_flags_json,
  533. uncertainties_json=excluded.uncertainties_json,
  534. confidence=excluded.confidence,
  535. created_at=excluded.created_at
  536. """,
  537. (id, cycle_id, concern_id, summary, key_drivers_json, risk_flags_json, uncertainties_json, confidence, created_at),
  538. )
  539. def upsert_decision(*, id: str, cycle_id: str, concern_id: str, mode: str, action: str, target_strategy: str | None, target_policy_json: str | None, reason_summary: str | None, confidence: float | None, requires_action: bool, created_at: str | None = None) -> None:
  540. init_db()
  541. created_at = created_at or _now()
  542. with _connect() as conn:
  543. conn.execute(
  544. """
  545. insert into decisions(id, cycle_id, concern_id, mode, action, target_strategy, target_policy_json, reason_summary, confidence, requires_action, created_at)
  546. values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  547. on conflict(id) do update set
  548. cycle_id=excluded.cycle_id,
  549. concern_id=excluded.concern_id,
  550. mode=excluded.mode,
  551. action=excluded.action,
  552. target_strategy=excluded.target_strategy,
  553. target_policy_json=excluded.target_policy_json,
  554. reason_summary=excluded.reason_summary,
  555. confidence=excluded.confidence,
  556. requires_action=excluded.requires_action,
  557. created_at=excluded.created_at
  558. """,
  559. (id, cycle_id, concern_id, mode, action, target_strategy, target_policy_json, reason_summary, confidence, 1 if requires_action else 0, created_at),
  560. )
  561. def latest_states(limit: int = 20) -> list[dict[str, Any]]:
  562. init_db()
  563. with _connect() as conn:
  564. rows = conn.execute("select * from states order by created_at desc limit ?", (limit,)).fetchall()
  565. return [dict(r) for r in rows]
  566. def recent_states_for_concern(*, concern_id: str, since_seconds: int, limit: int = 24) -> list[dict[str, Any]]:
  567. init_db()
  568. cutoff = datetime.now(timezone.utc).timestamp() - max(1, since_seconds)
  569. cutoff_iso = datetime.fromtimestamp(cutoff, tz=timezone.utc).isoformat()
  570. with _connect() as conn:
  571. rows = conn.execute(
  572. "select * from states where concern_id = ? and created_at >= ? order by created_at asc limit ?",
  573. (concern_id, cutoff_iso, limit),
  574. ).fetchall()
  575. return [dict(r) for r in rows]
  576. def latest_decisions(limit: int = 20) -> list[dict[str, Any]]:
  577. init_db()
  578. with _connect() as conn:
  579. rows = conn.execute("select * from decisions order by created_at desc limit ?", (limit,)).fetchall()
  580. return [dict(r) for r in rows]
  581. def latest_narratives(limit: int = 20) -> list[dict[str, Any]]:
  582. init_db()
  583. with _connect() as conn:
  584. rows = conn.execute("select * from narratives order by created_at desc limit ?", (limit,)).fetchall()
  585. return [dict(r) for r in rows]
  586. def latest_regime_samples(limit: int = 20) -> list[dict[str, Any]]:
  587. init_db()
  588. with _connect() as conn:
  589. rows = conn.execute("select * from regime_samples order by captured_at desc limit ?", (limit,)).fetchall()
  590. return [dict(r) for r in rows]
  591. def latest_observations(limit: int = 20, source: str | None = None) -> list[dict[str, Any]]:
  592. init_db()
  593. with _connect() as conn:
  594. if source:
  595. rows = conn.execute(
  596. "select * from observations where source = ? order by observed_at desc limit ?",
  597. (source, limit),
  598. ).fetchall()
  599. else:
  600. rows = conn.execute("select * from observations order by observed_at desc limit ?", (limit,)).fetchall()
  601. return [dict(r) for r in rows]
  602. def recent_regime_samples(limit: int = 200) -> list[dict[str, Any]]:
  603. init_db()
  604. with _connect() as conn:
  605. rows = conn.execute("select * from regime_samples order by captured_at desc limit ?", (limit,)).fetchall()
  606. return [dict(r) for r in rows]