DeepSeek V4-Flash news enrichment: LLM sentiment, surprise direction, summary
Three display/warning-only upgrades to the news engine, all behind the
existing keyword path as fallback — signals are never gated on LLM
output (event_risk_caps_signals / sentiment_affects_signals stay False
per the 2026-07 audit; backtester use_news=False untouched):
- Headline sentiment: one batched call per 600s cache window (capped at
25 new headlines, sha1-cached so repeats are never re-sent) overrides
the keyword scores per currency with the gold-inverse doctrine encoded
in the prompt.
- Release surprises: get_event_risk_level's description now says which
way the print pulls GOLD ("— gold bearish: strong jobs lift yields"),
fixing the documented numeric-only gap; risk level and windows
unchanged.
- Market summary: 1-2 sentence macro blurb on SignalReport.news_summary,
rendered in Telegram alerts (📰) and the CLI (🧠).
New llm_client.py: requests-only OpenAI-compatible client (no SDK), one
attempt per call, UTC-day request cap, singleton so the cap survives
per-scan NewsEngine reconstruction. Config: deepseek block in
config.json + DEEPSEEK_API_KEY env; absent key = fully inert.
Live-measured API behavior baked in: V4-Flash is a hybrid thinking model
— requests send thinking:disabled (reasoning otherwise blew the timeout
and could exhaust max_tokens before the answer); prompts are static
module constants so DeepSeek's prefix cache prices repeat calls at the
cache-hit rate; scores use compact [s, xau] pairs; timeout 12s.
Tests 273 → 286 (new Module 19 + deepseek config tests).
Claude-Session: https://claude.ai/code/session_01Nsd8HcrV67GbCZXxZEn1Ma
This commit is contained in:
+37
-1
@@ -85,11 +85,15 @@ _VALID_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
||||
# AUTOSCAN_CONFIRMATIONS entries in telegram_bot.py).
|
||||
_SCANNABLE_TFS = ("M5", "M15", "H1", "H4")
|
||||
|
||||
_KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level", "oanda"}
|
||||
_KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level",
|
||||
"oanda", "deepseek"}
|
||||
_KNOWN_OANDA = {
|
||||
"account_id", "api_token", "base_url", "lot_size",
|
||||
"quality_gate", "max_hold_bars",
|
||||
}
|
||||
_KNOWN_DEEPSEEK = {
|
||||
"api_key", "model", "enabled", "timeout_seconds", "daily_request_cap",
|
||||
}
|
||||
_KNOWN_TELEGRAM = {"token", "chat_id", "channel_id", "alert_min_quality"}
|
||||
_KNOWN_SCANNING = {"pairs", "timeframes"}
|
||||
_KNOWN_PAIR_KEYS = {"blocked_hours_utc"}
|
||||
@@ -113,6 +117,7 @@ class AppConfig:
|
||||
Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4])
|
||||
pair_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
oanda_settings: Dict[str, Any] = field(default_factory=dict)
|
||||
deepseek_settings: Dict[str, Any] = field(default_factory=dict)
|
||||
log_level: str = "INFO"
|
||||
|
||||
|
||||
@@ -248,6 +253,29 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig:
|
||||
"config.json: oanda.max_hold_bars must be a positive integer")
|
||||
app.oanda_settings = dict(oanda)
|
||||
|
||||
deepseek = raw.get("deepseek", {})
|
||||
_check_keys(deepseek, _KNOWN_DEEPSEEK, '"deepseek"')
|
||||
if "enabled" in deepseek and not isinstance(deepseek["enabled"], bool):
|
||||
raise AppConfigError(
|
||||
"config.json: deepseek.enabled must be true or false")
|
||||
if "timeout_seconds" in deepseek:
|
||||
ts = deepseek["timeout_seconds"]
|
||||
if isinstance(ts, bool) or not isinstance(ts, (int, float)) or ts <= 0:
|
||||
raise AppConfigError(
|
||||
"config.json: deepseek.timeout_seconds must be a positive "
|
||||
"number")
|
||||
if "daily_request_cap" in deepseek:
|
||||
cap = deepseek["daily_request_cap"]
|
||||
if isinstance(cap, bool) or not isinstance(cap, int) or cap < 1:
|
||||
raise AppConfigError(
|
||||
"config.json: deepseek.daily_request_cap must be a positive "
|
||||
"integer")
|
||||
if "model" in deepseek:
|
||||
if not isinstance(deepseek["model"], str) or not deepseek["model"]:
|
||||
raise AppConfigError(
|
||||
"config.json: deepseek.model must be a non-empty string")
|
||||
app.deepseek_settings = dict(deepseek)
|
||||
|
||||
level = raw.get("log_level", app.log_level)
|
||||
if level not in _VALID_LOG_LEVELS:
|
||||
raise AppConfigError(
|
||||
@@ -339,6 +367,14 @@ def oanda_base_url(app: Optional[AppConfig] = None) -> str:
|
||||
app.oanda_settings.get("base_url", "https://api-fxpractice.oanda.com"))
|
||||
|
||||
|
||||
# ── DeepSeek resolution helper (env → config.json → "") ──────────────
|
||||
|
||||
def deepseek_api_key(app: Optional[AppConfig] = None) -> str:
|
||||
app = app or get_app_config()
|
||||
return os.environ.get("DEEPSEEK_API_KEY") or str(
|
||||
app.deepseek_settings.get("api_key", ""))
|
||||
|
||||
|
||||
# ── PlatformConfig overlay ────────────────────────────────────────────
|
||||
|
||||
def apply_pair_overrides(
|
||||
|
||||
Reference in New Issue
Block a user