102 lines
5.1 KiB
Python
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 {}
|