tradingSystem/app/services/consensus_service.py

102 lines
5.1 KiB
Python

# -*- coding: utf-8 -*-
"""三源合议装配 (2026-09-11 工作包二)。把基本面、技术面、择时三票凑齐, 合成方向与路由。
三个纯逻辑判定在 core (fund_rules / tech_rules / timing_rules / consensus); 本模块负责取数与装配:
基本面 —— 候选行 / 持仓行里带的公司深度评析 (company_review), 过 fund_rules。
技术面 —— tech_service 早上写的映射 (PMS_TECH_STATE_MAP) 里该票的紧凑立场。
择时 —— 决策系统昨夜定性 (downstream.fetch_nightly_verdicts 读 strategy_daily_results.signal_type),
加当天盘中转多留痕 (flip_at, 由调用方传入, 暂缺就只按昨夜定性)。
装配后给每行凑出四块意见, 再由 hard_keys 折成提议的六个硬数字键 (方案附录丁)。
立场词一律中文 (看多/看空/中性/无读数); 任何一路无读数都弃权, 绝不折成看空 (设计原则二)。
"""
from __future__ import annotations
import logging
from app.core import consensus, fund_rules, timing_rules, tradedays
from app.core.command_spec import normalize_code
from app.services import param_store, tech_service
logger = logging.getLogger("pms.consensus")
def _params() -> dict:
gi, gb = param_store.get_int, param_store.get_bool
return {
"fund_required": gb("PMS_FUND_REQUIRED", True),
"fund_stale_days": gi("PMS_FUND_STALE_DAYS", 120),
"fund_consensus_good_min": gi("PMS_FUND_CONSENSUS_GOOD_MIN", 8),
"consensus_route": gb("PMS_CONSENSUS_ROUTE", True),
"timing_stale_tdays": gi("PMS_REF_STALE_TDAYS", 3),
}
def _timing_stance(entry, *, today_ymd, stale_tdays, flip_at=None):
"""昨夜定性 entry {verdict, trade_date} → timing_rules 立场; trade_date 超期传 fresh=False。"""
if not entry:
return timing_rules.synthesize(None, intraday_flip_at=flip_at)
fresh = True
td_ = entry.get("trade_date")
if td_ is not None:
try:
ymd = int(str(td_).replace("-", "")[:8])
elapsed = max(0, tradedays.trade_days_left(today_ymd, ymd) - 1)
fresh = elapsed <= int(stale_tdays)
except (ValueError, TypeError):
fresh = True
return timing_rules.synthesize(entry.get("verdict"), intraday_flip_at=flip_at, fresh=fresh)
def nightly_map(codes) -> dict:
"""批量读昨夜定性 {ts_code: {verdict, trade_date}}。取不到返回空, 不抛 (无读数弃权)。"""
try:
from app.repo import downstream_repo
return downstream_repo.fetch_nightly_verdicts(codes)
except Exception as e: # noqa: BLE001
logger.warning("[合议] 昨夜定性取不到: %s", e)
return {}
def assemble(row, *, nightly=None, tech_states=None, flip_at=None, params=None) -> dict:
"""给一行 (候选 / 持仓) 装配四块意见。row 需带 ts_code 与 company_review。不改 row。
nightly: 该票昨夜定性 entry; tech_states: 技术面映射 (state_map); flip_at: 盘中转多时刻。"""
p = params or _params()
code = normalize_code(row.get("ts_code") or "")
cr = row.get("company_review") if isinstance(row.get("company_review"), dict) else None
fund = fund_rules.synthesize(cr, params={"stale_days": p["fund_stale_days"],
"consensus_good_min": p["fund_consensus_good_min"]})
tech = (tech_states or {}).get(code) or {"stance": "无读数", "no_read_why": "没有技术面读数"}
tm = _timing_stance(nightly, today_ymd=tradedays.ymd(),
stale_tdays=p["timing_stale_tdays"], flip_at=flip_at)
# 合议参与分流的开关关掉时, 基本面无读数不由合议这层拦 (回到只按判决分流)
con = consensus.decide(fund["stance"], tech.get("stance") or "无读数", tm["stance"],
tech_phase=tech.get("phase"),
fund_required=(p["fund_required"] and p["consensus_route"]))
return {"fund": fund, "tech": tech, "timing": tm, "consensus": con}
def hard_keys(blocks) -> dict:
"""四块意见 → 提议硬数字六键 (方案附录丁)。这六键不进 judge.OPEN_JUDGE_KEYS。"""
fund, tech, tm, con = blocks["fund"], blocks["tech"], blocks["timing"], blocks["consensus"]
return {
"fund_stance": {"stance": fund["stance"], "mark": fund.get("mark"), "fact": fund.get("fact")},
"tech": tech,
"tech_text": tech.get("reason") or tech.get("no_read_why"),
"timing": {"stance": tm["stance"], "nightly": tm.get("nightly"),
"flip_at": tm.get("intraday_flip_at")},
"consensus": {"direction": con["direction"], "votes": con["votes"],
"strength": con.get("strength"), "reason": con.get("reason"),
"route": con.get("route"), "route_reason": con.get("route_reason")},
"consensus_text": con.get("reason"),
}
def state_map():
"""技术面映射, 透传 tech_service.state_map (装配时批量取一次给所有行用)。"""
try:
return tech_service.state_map()
except Exception as e: # noqa: BLE001
logger.warning("[合议] 技术面映射取不到: %s", e)
return {}