96 lines
4.4 KiB
Python
96 lines
4.4 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
研判闸客户端 · 二级关口 (设计 §7)
|
||
|
|
==================================
|
||
|
|
「PMS 备齐硬数字与账本流水作为 context → 请求决策系统研判 (其 process_intraday_audit
|
||
|
|
新增 PMS 请求类 direction, 复用既有仲裁哲学: 代码算数、AI 裁定性、越界收口)
|
||
|
|
→ 结构化答复 通过/驳回+理由。**研判服务不可用 → 自动降级 propose_only
|
||
|
|
(人工确认替位) + ERROR 告警**。命令驱动动作不过研判闸 (不用 AI 审用户)。」
|
||
|
|
|
||
|
|
当前状态: 决策系统侧的 PMS 请求 direction 尚未开发 (bionic 仓库的配套改造)。
|
||
|
|
因此本模块默认 `PMS_JUDGE_API_BASE` 为空 = 未接通, 一律返回 UNAVAILABLE, 调用方按设计
|
||
|
|
降级为「人工确认替位」。接通后只需在页面把 base 填上, 无需改代码。
|
||
|
|
|
||
|
|
返回结构固定: {"verdict": PASS|REJECT|UNAVAILABLE, "reason", "degraded", "raw"}
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
|
||
|
|
from app.services import param_store
|
||
|
|
|
||
|
|
logger = logging.getLogger("pms.judge")
|
||
|
|
|
||
|
|
PASS, REJECT, UNAVAILABLE = "PASS", "REJECT", "UNAVAILABLE"
|
||
|
|
|
||
|
|
|
||
|
|
def enabled() -> bool:
|
||
|
|
return param_store.get_bool("PMS_JUDGE_ENABLED", True)
|
||
|
|
|
||
|
|
|
||
|
|
def base_url() -> str:
|
||
|
|
return (param_store.get("PMS_JUDGE_API_BASE", "") or "").strip().rstrip("/")
|
||
|
|
|
||
|
|
|
||
|
|
def available() -> bool:
|
||
|
|
return bool(enabled() and base_url())
|
||
|
|
|
||
|
|
|
||
|
|
def judged_actions() -> set:
|
||
|
|
return set(param_store.get_list("PMS_JUDGE_ACTIONS", ["FILL", "ADD", "DCA", "SWITCH"]))
|
||
|
|
|
||
|
|
|
||
|
|
def status() -> dict:
|
||
|
|
if not enabled():
|
||
|
|
return {"available": False, "reason": "研判闸已关闭 (PMS_JUDGE_ENABLED=False), "
|
||
|
|
"自主提议一律走人工确认"}
|
||
|
|
if not base_url():
|
||
|
|
return {"available": False, "reason": "决策系统 PMS 研判接口未配置 "
|
||
|
|
"(PMS_JUDGE_API_BASE 为空) —— 待 bionic 侧配套改造"
|
||
|
|
"完成后填入, 当前自动降级为人工确认"}
|
||
|
|
return {"available": True, "base": base_url(), "actions": sorted(judged_actions())}
|
||
|
|
|
||
|
|
|
||
|
|
def request(candidate: dict, context: dict = None, *, timeout: int = None) -> dict:
|
||
|
|
"""请求一次研判。任何异常/超时/未接通都返回 UNAVAILABLE (绝不把提议当成通过)。"""
|
||
|
|
action = str(candidate.get("action") or "").upper()
|
||
|
|
if action not in judged_actions():
|
||
|
|
return {"verdict": PASS, "reason": f"{action} 不在研判范围, 规则闸通过即可",
|
||
|
|
"degraded": False, "raw": None}
|
||
|
|
if not available():
|
||
|
|
st = status()
|
||
|
|
logger.error("[研判闸] 不可用, 降级人工确认: %s", st["reason"])
|
||
|
|
return {"verdict": UNAVAILABLE, "reason": st["reason"], "degraded": True, "raw": None}
|
||
|
|
|
||
|
|
payload = {
|
||
|
|
"direction": "PMS_JUDGE", "action": action, "ts_code": candidate.get("ts_code"),
|
||
|
|
"qty": candidate.get("qty"), "reason": candidate.get("reason"),
|
||
|
|
"hard_numbers": candidate.get("hard_numbers") or {},
|
||
|
|
"context": context or {},
|
||
|
|
# 设计要求: 补仓类研判必须回答「下跌是杀逻辑还是杀情绪」
|
||
|
|
"must_answer": (["下跌是杀逻辑还是杀情绪"] if action == "DCA" else []),
|
||
|
|
}
|
||
|
|
to = int(timeout or param_store.get_int("PMS_JUDGE_TIMEOUT", 90))
|
||
|
|
url = base_url() + (param_store.get("PMS_JUDGE_PATH", "/api/intraday/pms_judge") or "")
|
||
|
|
try:
|
||
|
|
import requests
|
||
|
|
r = requests.post(url, json=payload, timeout=to)
|
||
|
|
r.raise_for_status()
|
||
|
|
data = r.json() or {}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error("[研判闸] 请求失败, 降级人工确认: %s: %s", type(e).__name__, e)
|
||
|
|
return {"verdict": UNAVAILABLE, "reason": f"研判请求失败: {type(e).__name__}: {e}",
|
||
|
|
"degraded": True, "raw": None}
|
||
|
|
|
||
|
|
verdict = str(data.get("verdict") or data.get("decision") or "").upper()
|
||
|
|
if verdict in ("PASS", "APPROVE", "APPROVED", "ALLOW", "通过"):
|
||
|
|
v = PASS
|
||
|
|
elif verdict in ("REJECT", "DENY", "DENIED", "BLOCK", "驳回"):
|
||
|
|
v = REJECT
|
||
|
|
else:
|
||
|
|
logger.error("[研判闸] 答复无法识别 (%s), 按不可用降级", verdict or data)
|
||
|
|
return {"verdict": UNAVAILABLE, "reason": f"研判答复无法识别: {verdict or data}",
|
||
|
|
"degraded": True, "raw": data}
|
||
|
|
return {"verdict": v, "reason": data.get("reason") or data.get("rationale") or "",
|
||
|
|
"degraded": False, "raw": data}
|