alphax/app/db/runtime_config_db.py
2026-05-16 23:54:43 +08:00

312 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_onchain_config(default=None):
return get_config("system", "onchain", default=default)
def set_onchain_config(value, updated_by="", source="manual"):
return set_config("system", "onchain", value, description="On-chain provider and signal thresholds; API keys stay 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_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_onchain_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_onchain_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",
]