310 lines
10 KiB
Python
310 lines
10 KiB
Python
"""Runtime configuration storage.
|
|
|
|
rules.yaml is the read-only baseline. Anything that can change online lives
|
|
here so code deploys cannot overwrite production state.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
from datetime import datetime
|
|
|
|
from app.db.schema import get_conn, init_db
|
|
|
|
|
|
STRATEGY_TABLE = "strategy_runtime_config"
|
|
SYSTEM_TABLE = "system_config"
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now().isoformat()
|
|
|
|
|
|
def _loads(value, fallback=None):
|
|
try:
|
|
if isinstance(value, str) and value.strip():
|
|
return json.loads(value)
|
|
if isinstance(value, (dict, list)):
|
|
return value
|
|
except Exception:
|
|
pass
|
|
return copy.deepcopy(fallback if fallback is not None else {})
|
|
|
|
|
|
def _dumps(value) -> str:
|
|
return json.dumps(value if value is not None else {}, ensure_ascii=False, sort_keys=True, default=str)
|
|
|
|
|
|
def _table(kind: str) -> str:
|
|
return SYSTEM_TABLE if kind == "system" else STRATEGY_TABLE
|
|
|
|
|
|
def _ensure():
|
|
init_db()
|
|
|
|
|
|
def get_config(kind: str, key: str, default=None):
|
|
_ensure()
|
|
table = _table(kind)
|
|
conn = get_conn()
|
|
try:
|
|
row = conn.execute(f"SELECT config_json FROM {table} WHERE config_key=%s", (key,)).fetchone()
|
|
finally:
|
|
conn.close()
|
|
if not row:
|
|
return copy.deepcopy(default)
|
|
return _loads(row["config_json"], default if default is not None else {})
|
|
|
|
|
|
def set_config(kind: str, key: str, value, *, description: str = "", source: str = "manual", updated_by: str = "", is_secret: bool = False):
|
|
_ensure()
|
|
table = _table(kind)
|
|
now = _now()
|
|
conn = get_conn()
|
|
try:
|
|
if table == SYSTEM_TABLE:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO system_config (config_key, config_json, description, source, is_secret, updated_by, created_at, updated_at)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
|
ON CONFLICT(config_key) DO UPDATE SET
|
|
config_json=excluded.config_json,
|
|
description=COALESCE(NULLIF(excluded.description, ''), system_config.description),
|
|
source=excluded.source,
|
|
is_secret=excluded.is_secret,
|
|
updated_by=excluded.updated_by,
|
|
updated_at=excluded.updated_at
|
|
""",
|
|
(key, _dumps(value), description, source, int(bool(is_secret)), updated_by, now, now),
|
|
)
|
|
else:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO strategy_runtime_config (config_key, config_json, description, source, updated_by, created_at, updated_at)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s)
|
|
ON CONFLICT(config_key) DO UPDATE SET
|
|
config_json=excluded.config_json,
|
|
description=COALESCE(NULLIF(excluded.description, ''), strategy_runtime_config.description),
|
|
source=excluded.source,
|
|
updated_by=excluded.updated_by,
|
|
updated_at=excluded.updated_at
|
|
""",
|
|
(key, _dumps(value), description, source, updated_by, now, now),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return get_config(kind, key)
|
|
|
|
|
|
def delete_config(kind: str, key: str) -> bool:
|
|
_ensure()
|
|
table = _table(kind)
|
|
conn = get_conn()
|
|
try:
|
|
cur = conn.execute(f"DELETE FROM {table} WHERE config_key=%s", (key,))
|
|
conn.commit()
|
|
return getattr(cur, "rowcount", 0) > 0
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def list_configs(kind: str):
|
|
_ensure()
|
|
table = _table(kind)
|
|
conn = get_conn()
|
|
try:
|
|
rows = conn.execute(f"SELECT * FROM {table} ORDER BY config_key").fetchall()
|
|
finally:
|
|
conn.close()
|
|
items = []
|
|
for row in rows:
|
|
item = dict(row)
|
|
item["config"] = _loads(item.pop("config_json", "{}"), {})
|
|
item["kind"] = kind
|
|
if item.get("is_secret"):
|
|
item["config"] = {"masked": True}
|
|
items.append(item)
|
|
return items
|
|
|
|
|
|
def deep_merge(base, override):
|
|
if not isinstance(base, dict) or not isinstance(override, dict):
|
|
return copy.deepcopy(override)
|
|
merged = copy.deepcopy(base)
|
|
for key, value in override.items():
|
|
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
merged[key] = deep_merge(merged[key], value)
|
|
else:
|
|
merged[key] = copy.deepcopy(value)
|
|
return merged
|
|
|
|
|
|
def get_strategy_override():
|
|
return get_config("strategy", "rules_override", default={})
|
|
|
|
|
|
def set_strategy_override(value, updated_by="", source="manual"):
|
|
return set_config("strategy", "rules_override", value, description="Runtime strategy override layered on top of rules.yaml", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_strategy_meta(default=None):
|
|
return get_config("strategy", "meta", default=default)
|
|
|
|
|
|
def set_strategy_meta(value, updated_by="", source="runtime"):
|
|
return set_config("strategy", "meta", value, description="Runtime strategy iteration metadata", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_learned_rules_config(default=None):
|
|
return get_config("strategy", "learned_rules", default=default)
|
|
|
|
|
|
def set_learned_rules_config(value, updated_by="", source="runtime"):
|
|
return set_config("strategy", "learned_rules", value, description="Runtime learned rules released by review gate", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_event_sources(default=None):
|
|
return get_config("system", "event_driven.sources", default=default)
|
|
|
|
|
|
def set_event_sources(value, updated_by="", source="manual"):
|
|
return set_config("system", "event_driven.sources", value, description="Runtime news/event sources", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_event_driven_config(default=None):
|
|
return get_config("system", "event_driven", default=default)
|
|
|
|
|
|
def set_event_driven_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "event_driven", value, description="Runtime event/news driven screening settings", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_sentiment_config(default=None):
|
|
return get_config("system", "sentiment", default=default)
|
|
|
|
|
|
def set_sentiment_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "sentiment", value, description="Runtime sentiment monitoring settings", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_monitoring_config(default=None):
|
|
return get_config("system", "monitoring", default=default)
|
|
|
|
|
|
def set_monitoring_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "monitoring", value, description="Runtime monitoring and audit settings", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_llm_config(default=None):
|
|
return get_config("system", "llm", default=default)
|
|
|
|
|
|
def set_llm_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "llm", value, description="LLM provider and module switches; API key stays in env", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_paper_trading_config(default=None):
|
|
return get_config("system", "paper_trading", default=default)
|
|
|
|
|
|
def set_paper_trading_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "paper_trading", value, description="Paper trading account and execution model", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_live_trading_config(default=None):
|
|
return get_config("system", "live_trading", default=default)
|
|
|
|
|
|
def set_live_trading_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "live_trading", value, description="Live trading exchange, account and risk settings; API secrets stay in env", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_price_streamer_config(default=None):
|
|
return get_config("system", "price_streamer", default=default)
|
|
|
|
|
|
def set_price_streamer_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "price_streamer", value, description="Realtime websocket price streamer settings", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_notification_config(default=None):
|
|
return get_config("system", "notification", default=default)
|
|
|
|
|
|
def set_notification_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "notification", value, description="Notification channel switches and env pointers", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_email_config(default=None):
|
|
return get_config("system", "email", default=default)
|
|
|
|
|
|
def set_email_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "email", value, description="SMTP email settings; password stays in env", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_bootstrap_admin_config(default=None):
|
|
return get_config("system", "bootstrap_admin", default=default)
|
|
|
|
|
|
def set_bootstrap_admin_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "bootstrap_admin", value, description="Default admin bootstrap env pointers", source=source, updated_by=updated_by)
|
|
|
|
|
|
def get_scheduler_config(default=None):
|
|
return get_config("system", "scheduler", default=default)
|
|
|
|
|
|
def set_scheduler_config(value, updated_by="", source="manual"):
|
|
return set_config("system", "scheduler", value, description="Scheduler runtime process settings", source=source, updated_by=updated_by)
|
|
|
|
|
|
def seed_system_defaults(defaults: dict[str, tuple[object, str]]):
|
|
seeded = []
|
|
for key, item in (defaults or {}).items():
|
|
value, description = item
|
|
if get_config("system", key, default=None) is None:
|
|
set_config("system", key, value, description=description, source="seed_default")
|
|
seeded.append(key)
|
|
return seeded
|
|
|
|
|
|
__all__ = [
|
|
"deep_merge",
|
|
"delete_config",
|
|
"get_config",
|
|
"get_event_driven_config",
|
|
"get_event_sources",
|
|
"get_bootstrap_admin_config",
|
|
"get_email_config",
|
|
"get_llm_config",
|
|
"get_monitoring_config",
|
|
"get_notification_config",
|
|
"get_paper_trading_config",
|
|
"get_price_streamer_config",
|
|
"get_scheduler_config",
|
|
"get_sentiment_config",
|
|
"get_learned_rules_config",
|
|
"get_strategy_meta",
|
|
"get_strategy_override",
|
|
"list_configs",
|
|
"set_config",
|
|
"set_event_driven_config",
|
|
"set_event_sources",
|
|
"set_bootstrap_admin_config",
|
|
"set_email_config",
|
|
"set_llm_config",
|
|
"set_monitoring_config",
|
|
"set_notification_config",
|
|
"set_paper_trading_config",
|
|
"set_price_streamer_config",
|
|
"set_scheduler_config",
|
|
"set_sentiment_config",
|
|
"seed_system_defaults",
|
|
"set_learned_rules_config",
|
|
"set_strategy_meta",
|
|
"set_strategy_override",
|
|
]
|