132 lines
5.9 KiB
Python
132 lines
5.9 KiB
Python
"""环境标签:只读决策系统的日频市场区制接口,写进当日计划快照;只展示与复盘分组,不作交易前置。
|
||
|
||
## 为什么要有它
|
||
|
||
实证(docs/主观选股改进方案_2026-09-02.md 1.8、1.8e):市场环境是第一解释变量,但
|
||
"看过去五日或十日涨跌"没有预测力(相关 −0.28、−0.55)。所以桥不自算环境,只读
|
||
决策系统已有的、带迟滞的区制判断(主参考指数跌破二十日线且缩量或连续破位判弱势),
|
||
把它当标签挂在每张卡上,供复盘按"事前标签"分组。标签有没有预测力由复盘过线条件验证;
|
||
过线前不拦任何票,"弱环境不加新票"已退役(09-02 拍板)。
|
||
|
||
## 时序
|
||
|
||
决策系统 08:40 预热当日快照;桥 07:10 出计划时拿不到,标 UNKNOWN。统一调度平台在 08:45
|
||
再触发一次追加步骤(xxl.py 的 regime-append),把当日快照写进当天 JSON 的 regime 段;
|
||
/plan 应答的 regime 段只读那份落盘的快照,盘中不再向来源发请求。
|
||
|
||
## 接口契约(桥按此消费;决策系统侧按此实现只读端点)
|
||
|
||
GET {REGIME_API_URL}?date=YYYY-MM-DD ->
|
||
{"status": "OK" | "UNKNOWN" | "DISABLED", "data_date": "YYYY-MM-DD",
|
||
"indices": [{"code": "000300.SH", "name": "沪深300", "weak": true, "reason": "..."}],
|
||
"weak_count": 3, "degraded": false, "computed_at": "..."}
|
||
DISABLED 归 UNKNOWN 并在 source 里注明总开关关闭。任何失败 -> UNKNOWN,绝不抛错。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import datetime as dt
|
||
import json
|
||
import os
|
||
import urllib.parse
|
||
import urllib.request
|
||
|
||
import config
|
||
|
||
UNKNOWN = "UNKNOWN"
|
||
|
||
|
||
def fetch(day: str) -> dict:
|
||
"""读一次当日区制。返回归一后的 regime 段。"""
|
||
base = {"status": UNKNOWN, "data_date": None, "weak_count": None, "weak_day": None,
|
||
"indices": [], "degraded": None, "source": None,
|
||
"fetched_at": dt.datetime.now().isoformat(timespec="seconds"),
|
||
"weak_threshold": config.REGIME_WEAK_COUNT}
|
||
if not config.REGIME_API_URL:
|
||
base["source"] = "unconfigured(决策系统只读区制接口未接入)"
|
||
return base
|
||
url = f"{config.REGIME_API_URL}?{urllib.parse.urlencode({'date': day})}"
|
||
base["source"] = url
|
||
try:
|
||
with urllib.request.urlopen(url, timeout=config.REGIME_API_TIMEOUT) as resp:
|
||
payload = json.loads(resp.read().decode("utf-8", "replace"))
|
||
except Exception as e: # noqa: BLE001 —— 来源不可达就是 UNKNOWN,不拦票
|
||
base["source"] = f"{url}(不可达: {type(e).__name__})"
|
||
return base
|
||
if not isinstance(payload, dict):
|
||
return base
|
||
status = str(payload.get("status") or UNKNOWN).upper()
|
||
if status == "DISABLED":
|
||
base["source"] += "(总开关关闭)"
|
||
status = UNKNOWN
|
||
# 两种形状都收:契约形状 indices 为列表;决策系统缓存快照的原始形状 indices 为
|
||
# {指数代码: {name, weak, weak_via, qrs_stale, ...}}、广度字段叫 breadth_weak。
|
||
raw_idx = payload.get("indices")
|
||
if isinstance(raw_idx, dict):
|
||
indices = [{"code": c, **(v if isinstance(v, dict) else {})} for c, v in raw_idx.items()]
|
||
elif isinstance(raw_idx, list):
|
||
indices = [x for x in raw_idx if isinstance(x, dict)]
|
||
else:
|
||
indices = []
|
||
weak_count = payload.get("weak_count")
|
||
if weak_count is None:
|
||
weak_count = payload.get("breadth_weak")
|
||
if weak_count is None and indices:
|
||
weak_count = sum(1 for x in indices if x.get("weak"))
|
||
degraded = payload.get("degraded")
|
||
if degraded is None and indices:
|
||
degraded = any(x.get("qrs_stale") for x in indices)
|
||
base.update({
|
||
"status": "OK" if status == "OK" else UNKNOWN,
|
||
"data_date": payload.get("data_date"),
|
||
"weak_count": weak_count,
|
||
"weak_total": payload.get("breadth_total") or (len(indices) or None),
|
||
"indices": [{"code": x.get("code"), "name": x.get("name"), "weak": bool(x.get("weak")),
|
||
"weak_via": x.get("weak_via"), "qrs_stale": x.get("qrs_stale"),
|
||
"dev_pct": x.get("dev_pct")} for x in indices],
|
||
"degraded": bool(degraded) if degraded is not None else None,
|
||
"computed_at": payload.get("computed_at"),
|
||
})
|
||
if base["status"] == "OK" and isinstance(weak_count, int):
|
||
base["weak_day"] = weak_count >= config.REGIME_WEAK_COUNT
|
||
return base
|
||
|
||
|
||
def snapshot_path(day: str) -> str:
|
||
return os.path.join(config.PLAN_SNAPSHOT_DIR, f"plan_{day}.json")
|
||
|
||
|
||
def append_to_snapshot(day: str) -> dict:
|
||
"""08:45 追加步骤:把当日区制写进当天 JSON 快照的 regime 段。
|
||
快照不存在(当日构建失败或 07:10 之前)就只打印,不创建空快照。"""
|
||
reg = fetch(day)
|
||
path = snapshot_path(day)
|
||
if not os.path.exists(path):
|
||
print(f"快照 {path} 不存在,环境段未写入(状态 {reg['status']})")
|
||
return reg
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
doc = json.load(f)
|
||
doc["regime"] = reg
|
||
tmp = path + ".tmp"
|
||
with open(tmp, "w", encoding="utf-8") as f:
|
||
json.dump(doc, f, ensure_ascii=False, indent=1)
|
||
os.replace(tmp, path)
|
||
print(f"环境段已写入 {path}:status={reg['status']} weak_count={reg['weak_count']} "
|
||
f"weak_day={reg['weak_day']}")
|
||
return reg
|
||
|
||
|
||
def read_section(day: str, key: str):
|
||
"""只读当日快照里的某一段(regime、market 等顶层键),快照不存在或坏 JSON 返回 None。
|
||
/plan 应答里的环境类字段一律从落盘快照取,盘中不向任何来源发请求。"""
|
||
path = snapshot_path(day)
|
||
try:
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
return json.load(f).get(key)
|
||
except (OSError, ValueError):
|
||
return None
|
||
|
||
|
||
def read_from_snapshot(day: str) -> dict | None:
|
||
"""/plan 用:只读当日快照里的 regime 段,没有就 None(应答里给 UNKNOWN)。"""
|
||
return read_section(day, "regime")
|