store.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  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, timeout=30)
  200. conn.row_factory = sqlite3.Row
  201. conn.execute("pragma foreign_keys = on")
  202. conn.execute("pragma journal_mode = wal")
  203. conn.execute("pragma synchronous = normal")
  204. conn.execute("pragma busy_timeout = 30000")
  205. return conn
  206. def init_db() -> None:
  207. with _connect() as conn:
  208. for stmt in SCHEMA_STATEMENTS:
  209. if stmt.lstrip().lower().startswith("create index"):
  210. continue
  211. conn.execute(stmt)
  212. conn.execute(
  213. """
  214. create table if not exists state (
  215. key text primary key,
  216. value text not null,
  217. updated_at text not null default current_timestamp
  218. )
  219. """
  220. )
  221. concern_columns = {row[1] for row in conn.execute("pragma table_info(concerns)").fetchall()}
  222. for column in ("base_currency", "quote_currency", "decision_profile_id"):
  223. if column not in concern_columns:
  224. conn.execute(f"alter table concerns add column {column} text")
  225. for stmt in SCHEMA_STATEMENTS:
  226. if not stmt.lstrip().lower().startswith("create index"):
  227. continue
  228. conn.execute(stmt)
  229. def get_state() -> dict[str, Any]:
  230. init_db()
  231. with _connect() as conn:
  232. row = conn.execute("select value from state where key = ?", ("snapshot",)).fetchone()
  233. if not row:
  234. return {
  235. "status": "stub",
  236. "thinking": "Hermes is scaffolded and waiting for integrations.",
  237. "layers": ["concerns", "decision_profiles", "strategy_groups", "strategy_assignments", "cycles", "observations", "states", "narratives", "decisions", "actions", "coverage_gaps"],
  238. }
  239. return json.loads(row["value"])
  240. def put_state(payload: dict[str, Any]) -> None:
  241. init_db()
  242. with _connect() as conn:
  243. conn.execute(
  244. "insert into state(key, value, updated_at) values(?, ?, current_timestamp) on conflict(key) do update set value=excluded.value, updated_at=current_timestamp",
  245. ("snapshot", json.dumps(payload)),
  246. )
  247. 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:
  248. init_db()
  249. now = _now()
  250. with _connect() as conn:
  251. conn.execute(
  252. """
  253. insert into concerns(id, account_id, market_symbol, base_currency, quote_currency, strategy_id, decision_profile_id, source, status, notes, created_at, updated_at)
  254. values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  255. on conflict(id) do update set
  256. account_id=excluded.account_id,
  257. market_symbol=excluded.market_symbol,
  258. base_currency=excluded.base_currency,
  259. quote_currency=excluded.quote_currency,
  260. strategy_id=excluded.strategy_id,
  261. decision_profile_id=excluded.decision_profile_id,
  262. source=excluded.source,
  263. status=excluded.status,
  264. notes=excluded.notes,
  265. updated_at=excluded.updated_at
  266. """,
  267. (id, account_id, market_symbol, base_currency, quote_currency, strategy_id, decision_profile_id, source, status, notes, now, now),
  268. )
  269. def list_concerns() -> list[dict[str, Any]]:
  270. init_db()
  271. with _connect() as conn:
  272. rows = conn.execute("select * from concerns order by updated_at desc").fetchall()
  273. return [dict(r) for r in rows]
  274. def upsert_decision_profile(*, id: str, name: str, config: dict[str, Any], description: str | None = None, status: str = "active") -> None:
  275. init_db()
  276. now = _now()
  277. with _connect() as conn:
  278. conn.execute(
  279. """
  280. insert into decision_profiles(id, name, description, config_json, status, created_at, updated_at)
  281. values(?, ?, ?, ?, ?, ?, ?)
  282. on conflict(id) do update set
  283. name=excluded.name,
  284. description=excluded.description,
  285. config_json=excluded.config_json,
  286. status=excluded.status,
  287. updated_at=excluded.updated_at
  288. """,
  289. (id, name, description, json.dumps(config, ensure_ascii=False), status, now, now),
  290. )
  291. def list_decision_profiles() -> list[dict[str, Any]]:
  292. init_db()
  293. with _connect() as conn:
  294. rows = conn.execute("select * from decision_profiles order by updated_at desc").fetchall()
  295. return [dict(r) for r in rows]
  296. def get_decision_profile(*, profile_id: str) -> dict[str, Any] | None:
  297. init_db()
  298. profile_id = str(profile_id or "").strip()
  299. if not profile_id:
  300. return None
  301. with _connect() as conn:
  302. row = conn.execute("select * from decision_profiles where id = ?", (profile_id,)).fetchone()
  303. return dict(row) if row else None
  304. 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:
  305. init_db()
  306. now = _now()
  307. with _connect() as conn:
  308. conn.execute(
  309. """
  310. insert into strategy_groups(id, concern_id, name, strategy_family, decision_profile_id, notes, status, created_at, updated_at)
  311. values(?, ?, ?, ?, ?, ?, ?, ?, ?)
  312. on conflict(id) do update set
  313. concern_id=excluded.concern_id,
  314. name=excluded.name,
  315. strategy_family=excluded.strategy_family,
  316. decision_profile_id=excluded.decision_profile_id,
  317. notes=excluded.notes,
  318. status=excluded.status,
  319. updated_at=excluded.updated_at
  320. """,
  321. (id, concern_id, name, strategy_family, decision_profile_id, notes, status, now, now),
  322. )
  323. def list_strategy_groups(*, concern_id: str | None = None) -> list[dict[str, Any]]:
  324. init_db()
  325. with _connect() as conn:
  326. if concern_id:
  327. rows = conn.execute("select * from strategy_groups where concern_id = ? order by updated_at desc", (concern_id,)).fetchall()
  328. else:
  329. rows = conn.execute("select * from strategy_groups order by updated_at desc").fetchall()
  330. return [dict(r) for r in rows]
  331. 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:
  332. init_db()
  333. now = _now()
  334. with _connect() as conn:
  335. conn.execute(
  336. """
  337. insert into strategy_assignments(id, strategy_group_id, strategy_id, strategy_type, role, status, notes, created_at, updated_at)
  338. values(?, ?, ?, ?, ?, ?, ?, ?, ?)
  339. on conflict(id) do update set
  340. strategy_group_id=excluded.strategy_group_id,
  341. strategy_id=excluded.strategy_id,
  342. strategy_type=excluded.strategy_type,
  343. role=excluded.role,
  344. status=excluded.status,
  345. notes=excluded.notes,
  346. updated_at=excluded.updated_at
  347. """,
  348. (id, strategy_group_id, strategy_id, strategy_type, role, status, notes, now, now),
  349. )
  350. def list_strategy_assignments(*, strategy_group_id: str | None = None) -> list[dict[str, Any]]:
  351. init_db()
  352. with _connect() as conn:
  353. if strategy_group_id:
  354. rows = conn.execute("select * from strategy_assignments where strategy_group_id = ? order by updated_at desc", (strategy_group_id,)).fetchall()
  355. else:
  356. rows = conn.execute("select * from strategy_assignments order by updated_at desc").fetchall()
  357. return [dict(r) for r in rows]
  358. def delete_concern(*, concern_id: str) -> dict[str, int]:
  359. init_db()
  360. concern_id = str(concern_id or "").strip()
  361. if not concern_id:
  362. 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}
  363. 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}
  364. with _connect() as conn:
  365. decision_ids = [row[0] for row in conn.execute("select id from decisions where concern_id = ?", (concern_id,)).fetchall()]
  366. group_ids = [row[0] for row in conn.execute("select id from strategy_groups where concern_id = ?", (concern_id,)).fetchall()]
  367. deleted["actions"] = conn.execute(
  368. f"delete from actions where decision_id in ({','.join('?' for _ in decision_ids)})",
  369. decision_ids,
  370. ).rowcount if decision_ids else 0
  371. deleted["strategy_assignments"] = conn.execute(
  372. f"delete from strategy_assignments where strategy_group_id in ({','.join('?' for _ in group_ids)})",
  373. group_ids,
  374. ).rowcount if group_ids else 0
  375. deleted["strategy_groups"] = conn.execute("delete from strategy_groups where concern_id = ?", (concern_id,)).rowcount or 0
  376. deleted["observations"] = conn.execute("delete from observations where concern_id = ?", (concern_id,)).rowcount or 0
  377. deleted["states"] = conn.execute("delete from states where concern_id = ?", (concern_id,)).rowcount or 0
  378. deleted["narratives"] = conn.execute("delete from narratives where concern_id = ?", (concern_id,)).rowcount or 0
  379. deleted["coverage_gaps"] = conn.execute("delete from coverage_gaps where concern_id = ?", (concern_id,)).rowcount or 0
  380. deleted["regime_samples"] = conn.execute("delete from regime_samples where concern_id = ?", (concern_id,)).rowcount or 0
  381. deleted["decisions"] = conn.execute("delete from decisions where concern_id = ?", (concern_id,)).rowcount or 0
  382. deleted["concerns"] = conn.execute("delete from concerns where id = ?", (concern_id,)).rowcount or 0
  383. return deleted
  384. def prune_older_than(days: int) -> dict[str, int]:
  385. init_db()
  386. cutoff = datetime.now(timezone.utc).timestamp() - (days * 86400)
  387. cutoff_iso = datetime.fromtimestamp(cutoff, tz=timezone.utc).isoformat()
  388. with _connect() as conn:
  389. deleted = {}
  390. for table in ("actions", "decisions", "narratives", "states", "observations", "coverage_gaps", "cycles"):
  391. if table == "actions":
  392. where = "executed_at is not null and executed_at < ?"
  393. elif table in {"decisions", "narratives", "states", "observations", "coverage_gaps", "cycles"}:
  394. where = "created_at < ?" if table != "cycles" else "started_at < ?"
  395. else:
  396. continue
  397. cur = conn.execute(f"delete from {table} where {where}", (cutoff_iso,))
  398. deleted[table] = cur.rowcount if cur.rowcount is not None else 0
  399. return deleted
  400. def sync_concerns_from_strategies(strategies: list[dict[str, Any]]) -> list[dict[str, Any]]:
  401. seen: set[str] = set()
  402. synced: list[dict[str, Any]] = []
  403. existing = {str(c.get("id") or ""): c for c in list_concerns()}
  404. for s in strategies:
  405. account_id = str(s.get("account_id") or "").strip() or None
  406. market_symbol = str(s.get("market_symbol") or "").strip() or None
  407. base_currency = str(s.get("base_currency") or "").strip() or None
  408. quote_currency = str(s.get("counter_currency") or s.get("quote_currency") or "").strip() or None
  409. strategy_id = str(s.get("id") or s.get("name") or s.get("strategy_type") or "").strip() or None
  410. if not account_id or not market_symbol:
  411. continue
  412. concern_id = f"{account_id}:{market_symbol}"
  413. if concern_id in seen:
  414. continue
  415. seen.add(concern_id)
  416. current = existing.get(concern_id, {})
  417. upsert_concern(
  418. id=concern_id,
  419. account_id=account_id,
  420. market_symbol=market_symbol,
  421. base_currency=base_currency,
  422. quote_currency=quote_currency,
  423. strategy_id=strategy_id,
  424. decision_profile_id=str(current.get("decision_profile_id") or "").strip() or None,
  425. source=str(current.get("source") or "trader_inventory"),
  426. status=str(current.get("status") or "active"),
  427. notes=str(current.get("notes") or "mirrored from trader strategy inventory").strip() or None,
  428. )
  429. 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})
  430. return synced
  431. def table_counts() -> dict[str, int]:
  432. init_db()
  433. tables = ["concerns", "decision_profiles", "strategy_groups", "strategy_assignments", "cycles", "observations", "states", "narratives", "decisions", "actions", "coverage_gaps", "regime_samples"]
  434. out: dict[str, int] = {}
  435. with _connect() as conn:
  436. for table in tables:
  437. out[table] = int(conn.execute(f"select count(*) as n from {table}").fetchone()["n"])
  438. return out
  439. def latest_cycle() -> dict[str, Any] | None:
  440. init_db()
  441. with _connect() as conn:
  442. row = conn.execute("select * from cycles order by started_at desc limit 1").fetchone()
  443. return dict(row) if row else None
  444. def latest_cycles(limit: int = 20) -> list[dict[str, Any]]:
  445. init_db()
  446. with _connect() as conn:
  447. rows = conn.execute("select * from cycles order by started_at desc limit ?", (limit,)).fetchall()
  448. return [dict(r) for r in rows]
  449. def upsert_cycle(*, id: str, started_at: str, finished_at: str | None, status: str, trigger: str, notes: str | None = None) -> None:
  450. init_db()
  451. with _connect() as conn:
  452. conn.execute(
  453. """
  454. insert into cycles(id, started_at, finished_at, status, trigger, notes)
  455. values(?, ?, ?, ?, ?, ?)
  456. on conflict(id) do update set
  457. started_at=excluded.started_at,
  458. finished_at=excluded.finished_at,
  459. status=excluded.status,
  460. trigger=excluded.trigger,
  461. notes=excluded.notes
  462. """,
  463. (id, started_at, finished_at, status, trigger, notes),
  464. )
  465. def upsert_regime_sample(*, id: str, cycle_id: str, concern_id: str, timeframe: str, regime_json: str, captured_at: str) -> None:
  466. init_db()
  467. last_error: Exception | None = None
  468. for delay_ms in (0, 75, 150, 300):
  469. try:
  470. with _connect() as conn:
  471. conn.execute(
  472. """
  473. insert into regime_samples(id, cycle_id, concern_id, timeframe, regime_json, captured_at)
  474. values(?, ?, ?, ?, ?, ?)
  475. on conflict(id) do update set
  476. cycle_id=excluded.cycle_id,
  477. concern_id=excluded.concern_id,
  478. timeframe=excluded.timeframe,
  479. regime_json=excluded.regime_json,
  480. captured_at=excluded.captured_at
  481. """,
  482. (id, cycle_id, concern_id, timeframe, regime_json, captured_at),
  483. )
  484. return
  485. except sqlite3.OperationalError as exc:
  486. last_error = exc
  487. if "database is locked" not in str(exc).lower():
  488. raise
  489. if delay_ms:
  490. import time
  491. time.sleep(delay_ms / 1000.0)
  492. if last_error:
  493. raise last_error
  494. 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:
  495. init_db()
  496. observed_at = observed_at or _now()
  497. with _connect() as conn:
  498. conn.execute(
  499. """
  500. insert into observations(id, cycle_id, concern_id, source, kind, payload_json, observed_at)
  501. values(?, ?, ?, ?, ?, ?, ?)
  502. on conflict(id) do update set
  503. cycle_id=excluded.cycle_id,
  504. concern_id=excluded.concern_id,
  505. source=excluded.source,
  506. kind=excluded.kind,
  507. payload_json=excluded.payload_json,
  508. observed_at=excluded.observed_at
  509. """,
  510. (id, cycle_id, concern_id, source, kind, payload_json, observed_at),
  511. )
  512. 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:
  513. init_db()
  514. created_at = created_at or _now()
  515. with _connect() as conn:
  516. conn.execute(
  517. """
  518. 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)
  519. values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  520. on conflict(id) do update set
  521. cycle_id=excluded.cycle_id,
  522. concern_id=excluded.concern_id,
  523. market_regime=excluded.market_regime,
  524. volatility_state=excluded.volatility_state,
  525. liquidity_state=excluded.liquidity_state,
  526. sentiment_pressure=excluded.sentiment_pressure,
  527. event_risk=excluded.event_risk,
  528. execution_quality=excluded.execution_quality,
  529. confidence=excluded.confidence,
  530. payload_json=excluded.payload_json,
  531. created_at=excluded.created_at
  532. """,
  533. (id, cycle_id, concern_id, market_regime, volatility_state, liquidity_state, sentiment_pressure, event_risk, execution_quality, confidence, payload_json, created_at),
  534. )
  535. 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:
  536. init_db()
  537. created_at = created_at or _now()
  538. with _connect() as conn:
  539. conn.execute(
  540. """
  541. insert into narratives(id, cycle_id, concern_id, summary, key_drivers_json, risk_flags_json, uncertainties_json, confidence, created_at)
  542. values(?, ?, ?, ?, ?, ?, ?, ?, ?)
  543. on conflict(id) do update set
  544. cycle_id=excluded.cycle_id,
  545. concern_id=excluded.concern_id,
  546. summary=excluded.summary,
  547. key_drivers_json=excluded.key_drivers_json,
  548. risk_flags_json=excluded.risk_flags_json,
  549. uncertainties_json=excluded.uncertainties_json,
  550. confidence=excluded.confidence,
  551. created_at=excluded.created_at
  552. """,
  553. (id, cycle_id, concern_id, summary, key_drivers_json, risk_flags_json, uncertainties_json, confidence, created_at),
  554. )
  555. 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:
  556. init_db()
  557. created_at = created_at or _now()
  558. with _connect() as conn:
  559. conn.execute(
  560. """
  561. insert into decisions(id, cycle_id, concern_id, mode, action, target_strategy, target_policy_json, reason_summary, confidence, requires_action, created_at)
  562. values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  563. on conflict(id) do update set
  564. cycle_id=excluded.cycle_id,
  565. concern_id=excluded.concern_id,
  566. mode=excluded.mode,
  567. action=excluded.action,
  568. target_strategy=excluded.target_strategy,
  569. target_policy_json=excluded.target_policy_json,
  570. reason_summary=excluded.reason_summary,
  571. confidence=excluded.confidence,
  572. requires_action=excluded.requires_action,
  573. created_at=excluded.created_at
  574. """,
  575. (id, cycle_id, concern_id, mode, action, target_strategy, target_policy_json, reason_summary, confidence, 1 if requires_action else 0, created_at),
  576. )
  577. def latest_states(limit: int = 20) -> list[dict[str, Any]]:
  578. init_db()
  579. with _connect() as conn:
  580. rows = conn.execute("select * from states order by created_at desc limit ?", (limit,)).fetchall()
  581. return [dict(r) for r in rows]
  582. def recent_states_for_concern(*, concern_id: str, since_seconds: int, limit: int = 24) -> list[dict[str, Any]]:
  583. init_db()
  584. cutoff = datetime.now(timezone.utc).timestamp() - max(1, since_seconds)
  585. cutoff_iso = datetime.fromtimestamp(cutoff, tz=timezone.utc).isoformat()
  586. with _connect() as conn:
  587. rows = conn.execute(
  588. "select * from states where concern_id = ? and created_at >= ? order by created_at asc limit ?",
  589. (concern_id, cutoff_iso, limit),
  590. ).fetchall()
  591. return [dict(r) for r in rows]
  592. def latest_decisions(limit: int = 20) -> list[dict[str, Any]]:
  593. init_db()
  594. with _connect() as conn:
  595. rows = conn.execute("select * from decisions order by created_at desc limit ?", (limit,)).fetchall()
  596. return [dict(r) for r in rows]
  597. def latest_narratives(limit: int = 20) -> list[dict[str, Any]]:
  598. init_db()
  599. with _connect() as conn:
  600. rows = conn.execute("select * from narratives order by created_at desc limit ?", (limit,)).fetchall()
  601. return [dict(r) for r in rows]
  602. def latest_regime_samples(limit: int = 20) -> 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]
  607. def latest_observations(limit: int = 20, source: str | None = None) -> list[dict[str, Any]]:
  608. init_db()
  609. with _connect() as conn:
  610. if source:
  611. rows = conn.execute(
  612. "select * from observations where source = ? order by observed_at desc limit ?",
  613. (source, limit),
  614. ).fetchall()
  615. else:
  616. rows = conn.execute("select * from observations order by observed_at desc limit ?", (limit,)).fetchall()
  617. return [dict(r) for r in rows]
  618. def recent_regime_samples(limit: int = 200) -> list[dict[str, Any]]:
  619. init_db()
  620. with _connect() as conn:
  621. rows = conn.execute("select * from regime_samples order by captured_at desc limit ?", (limit,)).fetchall()
  622. return [dict(r) for r in rows]