1
This commit is contained in:
parent
40dc2e07fe
commit
8ddcb08c79
@ -38,6 +38,7 @@ ALPHAX_LLM_MAX_TOKENS=900
|
|||||||
ALPHAX_LLM_RECOMMENDATIONS_ENABLED=1
|
ALPHAX_LLM_RECOMMENDATIONS_ENABLED=1
|
||||||
ALPHAX_LLM_SENTIMENT_ENABLED=1
|
ALPHAX_LLM_SENTIMENT_ENABLED=1
|
||||||
ALPHAX_LLM_REVIEW_ENABLED=1
|
ALPHAX_LLM_REVIEW_ENABLED=1
|
||||||
|
ALPHAX_LLM_SENTIMENT_BATCH_MAX_EVENTS=12
|
||||||
|
|
||||||
# 策略交易挂单门控。wait_pullback 只是候选,必须通过这些条件才会创建挂单。
|
# 策略交易挂单门控。wait_pullback 只是候选,必须通过这些条件才会创建挂单。
|
||||||
ALPHAX_PAPER_TRADING_MODE=intraday_trading
|
ALPHAX_PAPER_TRADING_MODE=intraday_trading
|
||||||
|
|||||||
@ -63,6 +63,8 @@ def default_llm_config():
|
|||||||
"model": _env_str("ALPHAX_LLM_MODEL", "gpt-4o-mini"),
|
"model": _env_str("ALPHAX_LLM_MODEL", "gpt-4o-mini"),
|
||||||
"timeout": _env_int("ALPHAX_LLM_TIMEOUT", 20),
|
"timeout": _env_int("ALPHAX_LLM_TIMEOUT", 20),
|
||||||
"max_tokens": _env_int("ALPHAX_LLM_MAX_TOKENS", 900),
|
"max_tokens": _env_int("ALPHAX_LLM_MAX_TOKENS", 900),
|
||||||
|
"reasoning_effort": _env_str("ALPHAX_LLM_REASONING_EFFORT", ""),
|
||||||
|
"thinking_enabled": _env_bool("ALPHAX_LLM_THINKING_ENABLED", False),
|
||||||
"modules": {
|
"modules": {
|
||||||
"recommendations": _env_bool("ALPHAX_LLM_RECOMMENDATIONS_ENABLED", True),
|
"recommendations": _env_bool("ALPHAX_LLM_RECOMMENDATIONS_ENABLED", True),
|
||||||
"sentiment": _env_bool("ALPHAX_LLM_SENTIMENT_ENABLED", True),
|
"sentiment": _env_bool("ALPHAX_LLM_SENTIMENT_ENABLED", True),
|
||||||
@ -82,6 +84,8 @@ def _llm_env_overrides():
|
|||||||
"ALPHAX_LLM_MODEL": ("model", lambda: _env_str("ALPHAX_LLM_MODEL", "gpt-4o-mini")),
|
"ALPHAX_LLM_MODEL": ("model", lambda: _env_str("ALPHAX_LLM_MODEL", "gpt-4o-mini")),
|
||||||
"ALPHAX_LLM_TIMEOUT": ("timeout", lambda: _env_int("ALPHAX_LLM_TIMEOUT", 20)),
|
"ALPHAX_LLM_TIMEOUT": ("timeout", lambda: _env_int("ALPHAX_LLM_TIMEOUT", 20)),
|
||||||
"ALPHAX_LLM_MAX_TOKENS": ("max_tokens", lambda: _env_int("ALPHAX_LLM_MAX_TOKENS", 900)),
|
"ALPHAX_LLM_MAX_TOKENS": ("max_tokens", lambda: _env_int("ALPHAX_LLM_MAX_TOKENS", 900)),
|
||||||
|
"ALPHAX_LLM_REASONING_EFFORT": ("reasoning_effort", lambda: _env_str("ALPHAX_LLM_REASONING_EFFORT", "")),
|
||||||
|
"ALPHAX_LLM_THINKING_ENABLED": ("thinking_enabled", lambda: _env_bool("ALPHAX_LLM_THINKING_ENABLED", False)),
|
||||||
}
|
}
|
||||||
for env_name, (key, loader) in checks.items():
|
for env_name, (key, loader) in checks.items():
|
||||||
if _env_present(env_name):
|
if _env_present(env_name):
|
||||||
|
|||||||
@ -21,7 +21,7 @@ from app.db import chat_assistant_db
|
|||||||
from app.db.analytics import get_pipeline_runs
|
from app.db.analytics import get_pipeline_runs
|
||||||
from app.db.llm_insights import compute_input_hash, repair_mojibake_json, repair_mojibake_text
|
from app.db.llm_insights import compute_input_hash, repair_mojibake_json, repair_mojibake_text
|
||||||
from app.db.schema import get_conn
|
from app.db.schema import get_conn
|
||||||
from app.services.llm_insights import _message_content_text, _parse_json_object_text, get_llm_params
|
from app.services.llm_insights import _chat_completion_body, _message_content_text, _parse_json_object_text, get_llm_params
|
||||||
from app.services.market_overview import get_crypto_market_overview
|
from app.services.market_overview import get_crypto_market_overview
|
||||||
|
|
||||||
|
|
||||||
@ -598,19 +598,20 @@ def _call_chat_llm(message: str, context: dict, history=None) -> dict:
|
|||||||
}
|
}
|
||||||
system_prompt = "你是 AlphaX Agent 的 Crypto 研究助手。你只能基于提供的结构化数据回答,不能编造数据。"
|
system_prompt = "你是 AlphaX Agent 的 Crypto 研究助手。你只能基于提供的结构化数据回答,不能编造数据。"
|
||||||
try:
|
try:
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": _compact_json(payload)},
|
||||||
|
]
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
f"{base_url}/chat/completions",
|
f"{base_url}/chat/completions",
|
||||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||||
json={
|
json=_chat_completion_body(
|
||||||
"model": model,
|
params,
|
||||||
"messages": [
|
messages,
|
||||||
{"role": "system", "content": system_prompt},
|
int(params.get("max_tokens") or 900),
|
||||||
{"role": "user", "content": _compact_json(payload)},
|
response_format=True,
|
||||||
],
|
temperature=0.15,
|
||||||
"temperature": 0.15,
|
),
|
||||||
"max_tokens": int(params.get("max_tokens") or 900),
|
|
||||||
"response_format": {"type": "json_object"},
|
|
||||||
},
|
|
||||||
timeout=int(params.get("timeout") or 20),
|
timeout=int(params.get("timeout") or 20),
|
||||||
)
|
)
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
|
|||||||
@ -26,6 +26,13 @@ def _env_bool(name, default=False):
|
|||||||
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name, default=0):
|
||||||
|
try:
|
||||||
|
return int(os.getenv(name, default))
|
||||||
|
except Exception:
|
||||||
|
return int(default or 0)
|
||||||
|
|
||||||
|
|
||||||
def get_llm_params():
|
def get_llm_params():
|
||||||
"""Runtime LLM config. This is system config, not strategy config."""
|
"""Runtime LLM config. This is system config, not strategy config."""
|
||||||
cfg = llm_config()
|
cfg = llm_config()
|
||||||
@ -36,6 +43,8 @@ def get_llm_params():
|
|||||||
"model": str(cfg.get("model") or "gpt-4o-mini").strip(),
|
"model": str(cfg.get("model") or "gpt-4o-mini").strip(),
|
||||||
"timeout": int(cfg.get("timeout") or 20),
|
"timeout": int(cfg.get("timeout") or 20),
|
||||||
"max_tokens": int(cfg.get("max_tokens") or 900),
|
"max_tokens": int(cfg.get("max_tokens") or 900),
|
||||||
|
"reasoning_effort": str(cfg.get("reasoning_effort") or "").strip(),
|
||||||
|
"thinking_enabled": bool(cfg.get("thinking_enabled", False)),
|
||||||
"modules": cfg.get("modules") or {},
|
"modules": cfg.get("modules") or {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,10 +100,11 @@ def _message_content_text(message: dict) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _parse_json_object_text(content: str) -> dict:
|
def _parse_json_object_text(content: str, *, detail=None) -> dict:
|
||||||
text = str(content or "").strip()
|
text = str(content or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
raise ValueError("llm_empty_content")
|
suffix = f":{_dump_json(detail)}" if detail else ""
|
||||||
|
raise ValueError(f"llm_empty_content{suffix}")
|
||||||
if text.startswith("```"):
|
if text.startswith("```"):
|
||||||
text = text.strip("`").strip()
|
text = text.strip("`").strip()
|
||||||
if text.lower().startswith("json"):
|
if text.lower().startswith("json"):
|
||||||
@ -114,15 +124,28 @@ def _parse_json_object_text(content: str) -> dict:
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
def _chat_completion_request(base_url, api_key, model, messages, max_tokens, timeout, *, response_format=True):
|
def _chat_completion_body(params, messages, max_tokens, *, response_format=True, temperature=0.2):
|
||||||
|
model = str(params.get("model") or "").strip()
|
||||||
body = {
|
body = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"temperature": 0.2,
|
"temperature": temperature,
|
||||||
"max_tokens": max_tokens,
|
"max_tokens": max_tokens,
|
||||||
}
|
}
|
||||||
if response_format:
|
if response_format:
|
||||||
body["response_format"] = {"type": "json_object"}
|
body["response_format"] = {"type": "json_object"}
|
||||||
|
reasoning_effort = str(params.get("reasoning_effort") or "").strip()
|
||||||
|
if reasoning_effort:
|
||||||
|
body["reasoning_effort"] = reasoning_effort
|
||||||
|
if bool(params.get("thinking_enabled", False)):
|
||||||
|
body["thinking"] = {"type": "enabled"}
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def _chat_completion_request(base_url, api_key, model, messages, max_tokens, timeout, *, response_format=True, params=None, temperature=0.2):
|
||||||
|
request_params = dict(params or {})
|
||||||
|
request_params.setdefault("model", model)
|
||||||
|
body = _chat_completion_body(request_params, messages, max_tokens, response_format=response_format, temperature=temperature)
|
||||||
return requests.post(
|
return requests.post(
|
||||||
f"{base_url}/chat/completions",
|
f"{base_url}/chat/completions",
|
||||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||||
@ -135,9 +158,15 @@ def _parse_llm_response(resp, model):
|
|||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
return {"status": "failed", "error": f"http_{resp.status_code}", "raw": resp.text[:1000], "model": model}
|
return {"status": "failed", "error": f"http_{resp.status_code}", "raw": resp.text[:1000], "model": model}
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
message = (((data.get("choices") or [{}])[0]).get("message") or {})
|
choice = (data.get("choices") or [{}])[0]
|
||||||
|
message = (choice.get("message") or {})
|
||||||
content = _message_content_text(message)
|
content = _message_content_text(message)
|
||||||
parsed = _parse_json_object_text(content)
|
detail = {
|
||||||
|
"finish_reason": choice.get("finish_reason"),
|
||||||
|
"message_keys": sorted(message.keys()) if isinstance(message, dict) else [],
|
||||||
|
"usage": data.get("usage") or {},
|
||||||
|
}
|
||||||
|
parsed = _parse_json_object_text(content, detail=detail)
|
||||||
return {"status": "success", "content": parsed, "model": model}
|
return {"status": "success", "content": parsed, "model": model}
|
||||||
|
|
||||||
|
|
||||||
@ -164,7 +193,7 @@ def _call_llm_json(prompt_version, payload):
|
|||||||
{"role": "user", "content": user_prompt},
|
{"role": "user", "content": user_prompt},
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
resp = _chat_completion_request(base_url, api_key, model, messages, max_tokens, timeout, response_format=True)
|
resp = _chat_completion_request(base_url, api_key, model, messages, max_tokens, timeout, response_format=True, params=params)
|
||||||
first = _parse_llm_response(resp, model)
|
first = _parse_llm_response(resp, model)
|
||||||
if first.get("status") == "success":
|
if first.get("status") == "success":
|
||||||
return first
|
return first
|
||||||
@ -173,9 +202,10 @@ def _call_llm_json(prompt_version, payload):
|
|||||||
# Some OpenAI-compatible providers occasionally return an empty message
|
# Some OpenAI-compatible providers occasionally return an empty message
|
||||||
# when strict JSON response_format is enabled. Retry once with prompt-only
|
# when strict JSON response_format is enabled. Retry once with prompt-only
|
||||||
# JSON enforcement so the job does not fail silently on provider quirks.
|
# JSON enforcement so the job does not fail silently on provider quirks.
|
||||||
retryable = str(exc) in ("llm_empty_content", "llm_empty_json_object")
|
error_text = str(exc)
|
||||||
|
retryable = error_text.startswith("llm_empty_content") or error_text.startswith("llm_empty_json_object")
|
||||||
if not retryable:
|
if not retryable:
|
||||||
return {"status": "failed", "error": str(exc)[:1000], "model": model}
|
return {"status": "failed", "error": error_text[:1000], "model": model}
|
||||||
try:
|
try:
|
||||||
retry_messages = [
|
retry_messages = [
|
||||||
{
|
{
|
||||||
@ -184,7 +214,7 @@ def _call_llm_json(prompt_version, payload):
|
|||||||
},
|
},
|
||||||
{"role": "user", "content": user_prompt},
|
{"role": "user", "content": user_prompt},
|
||||||
]
|
]
|
||||||
resp = _chat_completion_request(base_url, api_key, model, retry_messages, max_tokens, timeout, response_format=False)
|
resp = _chat_completion_request(base_url, api_key, model, retry_messages, max_tokens, timeout, response_format=False, params=params)
|
||||||
result = _parse_llm_response(resp, model)
|
result = _parse_llm_response(resp, model)
|
||||||
if result.get("status") == "success":
|
if result.get("status") == "success":
|
||||||
result["retry"] = "without_response_format"
|
result["retry"] = "without_response_format"
|
||||||
@ -423,6 +453,83 @@ def _build_sentiment_batch_payload(hours=24, limit=40):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sentiment_event_rank(item):
|
||||||
|
importance_weight = {"S": 100, "A": 80, "RISK": 75, "B": 50, "C": 20}
|
||||||
|
score = importance_weight.get(str(item.get("importance") or "").upper(), 30)
|
||||||
|
if item.get("rec_id"):
|
||||||
|
score += 18
|
||||||
|
if item.get("pushed"):
|
||||||
|
score += 10
|
||||||
|
if item.get("related_symbol") or item.get("related_base"):
|
||||||
|
score += 8
|
||||||
|
if item.get("tech_score") not in (None, ""):
|
||||||
|
try:
|
||||||
|
score += min(max(float(item.get("tech_score") or 0), 0), 100) / 10
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
event_type = str(item.get("event_type") or "").lower()
|
||||||
|
title = str(item.get("title") or "").lower()
|
||||||
|
if any(k in event_type or k in title for k in ("listing", "launch", "hack", "exploit", "upgrade", "partnership", "上币", "主网", "黑客", "漏洞", "升级", "合作")):
|
||||||
|
score += 12
|
||||||
|
if item.get("trend_rank"):
|
||||||
|
try:
|
||||||
|
score += max(0, 20 - float(item.get("trend_rank") or 0))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
def _compact_sentiment_batch_payload(payload, max_events=None):
|
||||||
|
"""Shrink noisy news/event rows before sending to the LLM."""
|
||||||
|
max_events = int(max_events or _env_int("ALPHAX_LLM_SENTIMENT_BATCH_MAX_EVENTS", 12) or 12)
|
||||||
|
events = list((payload or {}).get("events") or [])
|
||||||
|
ranked = sorted(events, key=_sentiment_event_rank, reverse=True)
|
||||||
|
compact_events = []
|
||||||
|
for idx, item in enumerate(ranked[:max(max_events, 1)], start=1):
|
||||||
|
compact_events.append({
|
||||||
|
"rank": idx,
|
||||||
|
"event_id": item.get("event_id"),
|
||||||
|
"source": item.get("source"),
|
||||||
|
"symbol": item.get("related_symbol") or item.get("symbol"),
|
||||||
|
"base": item.get("related_base"),
|
||||||
|
"title": str(item.get("title") or "")[:160],
|
||||||
|
"published_at": item.get("published_at"),
|
||||||
|
"importance": item.get("importance"),
|
||||||
|
"event_type": item.get("event_type"),
|
||||||
|
"decision": item.get("decision"),
|
||||||
|
"tech_score": item.get("tech_score"),
|
||||||
|
"trend_rank": item.get("trend_rank"),
|
||||||
|
"trend_score": item.get("trend_score"),
|
||||||
|
"change_24h_pct": item.get("change_24h_pct"),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"target_type": payload.get("target_type"),
|
||||||
|
"target_id": payload.get("target_id"),
|
||||||
|
"hours": payload.get("hours"),
|
||||||
|
"generated_at": payload.get("generated_at"),
|
||||||
|
"source_event_count": len(events),
|
||||||
|
"event_count": len(compact_events),
|
||||||
|
"events": compact_events,
|
||||||
|
"instructions": {
|
||||||
|
"role": "作为加密市场舆情分析师,基于精简事件判断短线市场影响。",
|
||||||
|
"focus": [
|
||||||
|
"只提炼真正可能影响 Binance 山寨币交易决策的主题",
|
||||||
|
"给出受影响币种、方向、理由和置信度",
|
||||||
|
"标记是否需要进入技术检查",
|
||||||
|
"如果没有有效事件,也必须返回空数组和 neutral 结论",
|
||||||
|
],
|
||||||
|
"expected_schema": {
|
||||||
|
"market_mood": "risk_on|neutral|risk_off",
|
||||||
|
"summary": "中文摘要",
|
||||||
|
"hot_themes": [{"theme": "", "impact": "", "symbols": [], "confidence": 0}],
|
||||||
|
"coin_impacts": [{"symbol": "", "direction": "positive|negative|risk|neutral", "reason": "", "confidence": 0, "need_technical_check": False}],
|
||||||
|
"risk_events": [{"title": "", "symbols": [], "risk_type": "", "severity": "low|medium|high"}],
|
||||||
|
"watchlist": [{"symbol": "", "why": "", "trigger": ""}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_review_payload(item):
|
def _build_review_payload(item):
|
||||||
return {
|
return {
|
||||||
"target_type": "review",
|
"target_type": "review",
|
||||||
@ -565,9 +672,10 @@ def generate_sentiment_insights(limit=30):
|
|||||||
def generate_sentiment_batch_analysis(limit=40, hours=24):
|
def generate_sentiment_batch_analysis(limit=40, hours=24):
|
||||||
if not get_llm_module_enabled("sentiment"):
|
if not get_llm_module_enabled("sentiment"):
|
||||||
return {"status": "skipped", "reason": "module_disabled", "processed": 0}
|
return {"status": "skipped", "reason": "module_disabled", "processed": 0}
|
||||||
payload = _build_sentiment_batch_payload(hours=hours, limit=limit)
|
source_payload = _build_sentiment_batch_payload(hours=hours, limit=limit)
|
||||||
if not payload.get("events"):
|
if not source_payload.get("events"):
|
||||||
return {"status": "skipped", "reason": "no_sentiment_events", "processed": 0}
|
return {"status": "skipped", "reason": "no_sentiment_events", "processed": 0}
|
||||||
|
payload = _compact_sentiment_batch_payload(source_payload)
|
||||||
input_hash = compute_input_hash(payload)
|
input_hash = compute_input_hash(payload)
|
||||||
cached = get_any_insight("sentiment_batch", payload["target_id"], PROMPTS["sentiment_batch_analyze_v1"], input_hash)
|
cached = get_any_insight("sentiment_batch", payload["target_id"], PROMPTS["sentiment_batch_analyze_v1"], input_hash)
|
||||||
if cached:
|
if cached:
|
||||||
|
|||||||
@ -221,6 +221,33 @@ def test_llm_json_code_fence_is_parsed(monkeypatch):
|
|||||||
assert result["content"]["summary"] == "ok"
|
assert result["content"]["summary"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_deepseek_thinking_is_not_sent_unless_enabled():
|
||||||
|
body = llm_insights._chat_completion_body(
|
||||||
|
{
|
||||||
|
"model": "deepseek-v4-pro",
|
||||||
|
"reasoning_effort": "",
|
||||||
|
"thinking_enabled": False,
|
||||||
|
},
|
||||||
|
[{"role": "user", "content": "Hello"}],
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
assert body["model"] == "deepseek-v4-pro"
|
||||||
|
assert "thinking" not in body
|
||||||
|
assert "reasoning_effort" not in body
|
||||||
|
|
||||||
|
enabled = llm_insights._chat_completion_body(
|
||||||
|
{
|
||||||
|
"model": "deepseek-v4-pro",
|
||||||
|
"reasoning_effort": "high",
|
||||||
|
"thinking_enabled": True,
|
||||||
|
},
|
||||||
|
[{"role": "user", "content": "Hello"}],
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
assert enabled["reasoning_effort"] == "high"
|
||||||
|
assert enabled["thinking"] == {"type": "enabled"}
|
||||||
|
|
||||||
|
|
||||||
def test_only_key_samples_generate_insights(monkeypatch, temp_db):
|
def test_only_key_samples_generate_insights(monkeypatch, temp_db):
|
||||||
_insert_recommendation(temp_db, symbol="CCC/USDT", action_status="观察", execution_status="observe", display_bucket="watch_pool", state_reason="普通观察")
|
_insert_recommendation(temp_db, symbol="CCC/USDT", action_status="观察", execution_status="observe", display_bucket="watch_pool", state_reason="普通观察")
|
||||||
_insert_recommendation(temp_db, symbol="DDD/USDT", action_status="等回踩", execution_status="wait_pullback", display_bucket="realtime", rec_time="2026-05-01T12:00:00")
|
_insert_recommendation(temp_db, symbol="DDD/USDT", action_status="等回踩", execution_status="wait_pullback", display_bucket="realtime", rec_time="2026-05-01T12:00:00")
|
||||||
@ -312,6 +339,28 @@ def test_sentiment_batch_analysis_api_returns_cached_result(temp_db):
|
|||||||
assert data["source_events"][0]["related_symbol"] == "ABC/USDT"
|
assert data["source_events"][0]["related_symbol"] == "ABC/USDT"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sentiment_batch_payload_is_compacted_before_llm(monkeypatch):
|
||||||
|
payload = {
|
||||||
|
"target_type": "sentiment_batch",
|
||||||
|
"target_id": "sentiment_batch:24h",
|
||||||
|
"hours": 24,
|
||||||
|
"generated_at": "2026-06-08T00:00:00",
|
||||||
|
"events": [
|
||||||
|
{"event_id": "low", "title": "small update", "importance": "C", "url": "https://example.com/low"},
|
||||||
|
{"event_id": "listing", "title": "Binance Will List AAAUSDT", "importance": "A", "related_symbol": "AAA/USDT", "event_type": "listing", "url": "https://example.com/high"},
|
||||||
|
{"event_id": "risk", "title": "BBB exploit risk", "importance": "RISK", "related_symbol": "BBB/USDT", "event_type": "exploit", "url": "https://example.com/risk"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
monkeypatch.setenv("ALPHAX_LLM_SENTIMENT_BATCH_MAX_EVENTS", "2")
|
||||||
|
|
||||||
|
compact = llm_insights._compact_sentiment_batch_payload(payload)
|
||||||
|
|
||||||
|
assert compact["source_event_count"] == 3
|
||||||
|
assert compact["event_count"] == 2
|
||||||
|
assert [item["event_id"] for item in compact["events"]] == ["listing", "risk"]
|
||||||
|
assert "url" not in compact["events"][0]
|
||||||
|
|
||||||
|
|
||||||
def test_sentiment_batch_enqueues_technical_check_candidates(monkeypatch, temp_db):
|
def test_sentiment_batch_enqueues_technical_check_candidates(monkeypatch, temp_db):
|
||||||
event_driven_screener.init_event_tables()
|
event_driven_screener.init_event_tables()
|
||||||
conn = sqlite3.connect(temp_db)
|
conn = sqlite3.connect(temp_db)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user