from __future__ import annotations
|
|
import csv
|
import hashlib
|
import json
|
import os
|
import re
|
from datetime import datetime
|
from pathlib import Path
|
|
import pandas as pd
|
import pymysql
|
|
|
RUN_ID = "RUN-ANA-WUJI-V1-ORIGINAL-BUY-LIMITUP-SCOPE-CHECK-20260614-001"
|
TASK_ID = "ANA-WUJI-V1-ORIGINAL-BUY-LIMITUP-SCOPE-20260614"
|
DESIGN_AUDIT_ID = "AUDIT-ANA-WUJI-V1-ORIGINAL-BUY-LIMITUP-SCOPE-20260614-DESIGN-001"
|
ISSUE_ID = "ANA-ISSUE-WUJI-V1-ORIGINAL-BUY-LIMITUP-SCOPE-20260614-001"
|
|
ROOT = Path(__file__).resolve().parents[1]
|
PROJECT_ROOT = ROOT.parents[2]
|
SOURCE_V1 = PROJECT_ROOT / "ana-data/result/RUN-ANA-WUJI-STRICT-SELL-ROLLING-REPAIR-20260609-001"
|
SOURCE_FULL = PROJECT_ROOT / "ana-data/result/RUN-ANA-WUJI-FULL-2023-2026-20260608-001"
|
LOCAL_DB_INDEX = Path(r"D:\strategy_project\s-system-doc\observer\天下模型沉淀\数据库索引数据.md")
|
|
STRICT_POLICY = {
|
"window_trading_days": 30,
|
"exclude_signal_day": True,
|
"price_source_table": "a_share_daily_price",
|
"limit_hit_field": "high_price",
|
"previous_close_field": "prev_close",
|
"limit_rate_policy": {
|
"BJ": 0.30,
|
"STAR_688": 0.20,
|
"CHINEXT_300_301": 0.20,
|
"MAINBOARD_DEFAULT": 0.10,
|
},
|
"tolerance_pct_points": 0.05,
|
"limit_hit_formula": "((high_price / prev_close) - 1) * 100 >= limit_rate_pct - tolerance_pct_points",
|
"source_buy_rule": "strict_position_lot_ledger.lot_source_type == SOURCE_BUY and tranche_index == 1",
|
"rolling_buy_rule": "strict_position_lot_ledger.lot_source_type == ROLLING_LOW_BUY and tranche_index > 1",
|
}
|
|
|
def now_iso() -> str:
|
return datetime.now().astimezone().isoformat(timespec="seconds")
|
|
|
def read_password() -> str:
|
env = os.environ.get("TIANXIA_MYSQL_PASSWORD") or os.environ.get("MYSQL_PWD")
|
if env:
|
return env
|
text = LOCAL_DB_INDEX.read_text(encoding="utf-8")
|
match = re.search(r"^\s*-\s*密码:`([^`]+)`", text, re.MULTILINE)
|
if not match:
|
raise RuntimeError("Unable to read local MySQL credential from approved local index.")
|
return match.group(1)
|
|
|
def get_conn():
|
return pymysql.connect(
|
host="127.0.0.1",
|
port=3306,
|
user="root",
|
password=read_password(),
|
database="tianxia",
|
charset="utf8mb4",
|
connect_timeout=5,
|
read_timeout=180,
|
)
|
|
|
def sha256_file(path: Path) -> str:
|
h = hashlib.sha256()
|
with path.open("rb") as f:
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
h.update(chunk)
|
return h.hexdigest()
|
|
|
def rel(path: Path) -> str:
|
return path.relative_to(ROOT).as_posix()
|
|
|
def write_csv(df: pd.DataFrame, name: str) -> Path:
|
path = ROOT / name
|
path.parent.mkdir(parents=True, exist_ok=True)
|
df.to_csv(path, index=False, encoding="utf-8-sig")
|
return path
|
|
|
def write_json(data: dict, name: str) -> Path:
|
path = ROOT / name
|
path.parent.mkdir(parents=True, exist_ok=True)
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
return path
|
|
|
def read_csv(path: Path) -> pd.DataFrame:
|
return pd.read_csv(path, encoding="utf-8-sig")
|
|
|
def board_policy(symbol: str) -> tuple[str, float]:
|
s = str(symbol).upper()
|
code = s.split(".")[0]
|
suffix = s.split(".")[-1] if "." in s else ""
|
if suffix == "BJ" or code.startswith(("8", "4", "9")):
|
return "BJ", 0.30
|
if suffix == "SH" and code.startswith("688"):
|
return "STAR_688", 0.20
|
if suffix == "SZ" and code.startswith(("300", "301")):
|
return "CHINEXT_300_301", 0.20
|
return "MAINBOARD_DEFAULT", 0.10
|
|
|
def clean_str(value: object) -> str:
|
text = "" if pd.isna(value) else str(value).strip()
|
return "" if text.lower() in {"nan", "nat", "none"} else text
|
|
|
def load_scope() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
lots = read_csv(SOURCE_V1 / "strict_position_lot_ledger.csv")
|
orders = read_csv(SOURCE_V1 / "strict_order_ledger.csv")
|
selected = read_csv(SOURCE_FULL / "full_selected_candidate_ledger.csv")
|
|
lots["tranche_index"] = pd.to_numeric(lots["tranche_index"], errors="coerce")
|
source_lots = lots[(lots["lot_source_type"] == "SOURCE_BUY") & (lots["tranche_index"] == 1)].copy()
|
rolling_lots = lots[(lots["lot_source_type"] == "ROLLING_LOW_BUY") & (lots["tranche_index"] > 1)].copy()
|
|
buy_orders = orders[orders["action"] == "BUY"].copy()
|
source_buy_orders = buy_orders[pd.to_numeric(buy_orders["tranche_index"], errors="coerce") == 1].copy()
|
rolling_buy_orders = buy_orders[pd.to_numeric(buy_orders["tranche_index"], errors="coerce") > 1].copy()
|
|
source = source_lots.merge(
|
source_buy_orders[
|
[
|
"order_id",
|
"source_order_id",
|
"case_id",
|
"candidate_id",
|
"symbol",
|
"trade_date",
|
"trade_time",
|
"price",
|
"position_delta_pct",
|
]
|
],
|
left_on=["source_order_id", "case_id", "symbol"],
|
right_on=["source_order_id", "case_id", "symbol"],
|
how="left",
|
suffixes=("_lot", "_order"),
|
)
|
source = source.merge(
|
selected[
|
[
|
"candidate_id",
|
"case_id",
|
"symbol",
|
"signal_trade_date",
|
"entry_trade_date",
|
"candidate_rank",
|
"candidate_status",
|
"strict_candidate_flag",
|
"recent_limitup_30_flag",
|
"last_limitup_date",
|
"days_since_last_limitup",
|
"run_id",
|
"price_source_table",
|
]
|
],
|
on=["candidate_id", "case_id", "symbol"],
|
how="left",
|
suffixes=("", "_candidate"),
|
)
|
return source, rolling_lots, rolling_buy_orders
|
|
|
def query_daily(symbols: list[str], min_date: str, max_date: str) -> pd.DataFrame:
|
placeholders = ",".join(["%s"] * len(symbols))
|
sql = f"""
|
SELECT trade_date, symbol, open_price, high_price, low_price, close_price, volume, amount
|
FROM a_share_daily_price
|
WHERE trade_date BETWEEN %s AND %s
|
AND symbol IN ({placeholders})
|
ORDER BY symbol, trade_date
|
"""
|
params = [min_date, max_date] + symbols
|
with get_conn() as conn:
|
daily = pd.read_sql(sql, conn, params=params)
|
daily["trade_date"] = pd.to_datetime(daily["trade_date"])
|
for col in ["open_price", "high_price", "low_price", "close_price", "volume", "amount"]:
|
daily[col] = pd.to_numeric(daily[col], errors="coerce")
|
daily = daily.sort_values(["symbol", "trade_date"]).reset_index(drop=True)
|
daily["prev_close"] = daily.groupby("symbol")["close_price"].shift(1)
|
return daily
|
|
|
def evaluate_source_buys(source: pd.DataFrame, daily: pd.DataFrame) -> pd.DataFrame:
|
daily_by_symbol = {symbol: g.reset_index(drop=True) for symbol, g in daily.groupby("symbol")}
|
rows = []
|
tol = STRICT_POLICY["tolerance_pct_points"]
|
for row in source.itertuples(index=False):
|
case_id = clean_str(getattr(row, "case_id", ""))
|
symbol = clean_str(getattr(row, "symbol", ""))
|
candidate_id = clean_str(getattr(row, "candidate_id", ""))
|
signal_date_raw = clean_str(getattr(row, "signal_trade_date", ""))
|
entry_date_raw = clean_str(getattr(row, "entry_trade_date_candidate", "")) or clean_str(getattr(row, "entry_trade_date", ""))
|
board, limit_rate = board_policy(symbol)
|
status = "STRICT_LIMITUP_PASS"
|
reason = ""
|
evidence_date = ""
|
evidence_return_pct = ""
|
evidence_high = ""
|
evidence_prev_close = ""
|
window_start = ""
|
window_end = ""
|
window_rows = 0
|
max_return_pct = ""
|
data_gap_reason = ""
|
if not signal_date_raw:
|
status = "SIGNAL_DATE_MISSING_HELD"
|
reason = "候选账本缺 signal_trade_date,无法复核信号日前 30 个交易日涨停。"
|
data_gap_reason = "signal_trade_date_missing"
|
window = pd.DataFrame()
|
elif symbol not in daily_by_symbol:
|
status = "DAILY_DATA_GAP_HELD"
|
reason = "日线源缺该 symbol,无法复核严格涨停记忆。"
|
data_gap_reason = "symbol_daily_missing"
|
window = pd.DataFrame()
|
else:
|
signal_ts = pd.Timestamp(signal_date_raw)
|
g = daily_by_symbol[symbol]
|
prior = g[g["trade_date"] < signal_ts].tail(30).copy()
|
window = prior
|
window_rows = len(prior)
|
if not prior.empty:
|
window_start = prior["trade_date"].iloc[0].date().isoformat()
|
window_end = prior["trade_date"].iloc[-1].date().isoformat()
|
prior["limit_return_pct"] = (prior["high_price"] / prior["prev_close"] - 1.0) * 100.0
|
prior["limit_rate_pct"] = limit_rate * 100.0
|
prior["strict_hit"] = prior["prev_close"].gt(0) & prior["limit_return_pct"].ge(
|
prior["limit_rate_pct"] - tol
|
)
|
max_return_pct = "" if prior["limit_return_pct"].dropna().empty else round(float(prior["limit_return_pct"].max()), 6)
|
hits = prior[prior["strict_hit"]].copy()
|
else:
|
hits = pd.DataFrame()
|
if window_rows < 30:
|
status = "PRIOR_30_TRADING_DAY_WINDOW_INCOMPLETE_HELD"
|
reason = f"信号日前可用日线不足 30 个交易日,实际 {window_rows}。"
|
data_gap_reason = "prior_window_incomplete"
|
elif hits.empty:
|
status = "STRICT_LIMITUP_FAIL"
|
reason = "信号日前 30 个交易日内未发现按板块阈值触及涨停。"
|
else:
|
hit = hits.iloc[-1]
|
evidence_date = hit["trade_date"].date().isoformat()
|
evidence_return_pct = round(float(hit["limit_return_pct"]), 6)
|
evidence_high = round(float(hit["high_price"]), 6)
|
evidence_prev_close = round(float(hit["prev_close"]), 6)
|
reason = f"信号日前 30 个交易日内于 {evidence_date} 触及板块涨停阈值。"
|
|
rows.append(
|
{
|
"run_id": RUN_ID,
|
"case_id": case_id,
|
"symbol": symbol,
|
"strict_lot_id": clean_str(getattr(row, "strict_lot_id", "")),
|
"source_lot_id": clean_str(getattr(row, "source_lot_id", "")),
|
"source_order_id": clean_str(getattr(row, "source_order_id", "")),
|
"v1_order_id": clean_str(getattr(row, "order_id", "")),
|
"candidate_id": candidate_id,
|
"candidate_rank": clean_str(getattr(row, "candidate_rank", "")),
|
"signal_trade_date": signal_date_raw,
|
"entry_trade_date": entry_date_raw,
|
"board_policy": board,
|
"limit_rate": limit_rate,
|
"limit_rate_pct": limit_rate * 100.0,
|
"tolerance_pct_points": tol,
|
"limit_hit_field": STRICT_POLICY["limit_hit_field"],
|
"previous_close_field": STRICT_POLICY["previous_close_field"],
|
"window_start_trade_date": window_start,
|
"window_end_trade_date": window_end,
|
"window_trading_days": window_rows,
|
"window_excludes_signal_day_flag": True,
|
"strict_limitup_status": status,
|
"strict_limitup_pass_flag": status == "STRICT_LIMITUP_PASS",
|
"strict_limitup_evidence_date": evidence_date,
|
"strict_limitup_evidence_return_pct": evidence_return_pct,
|
"strict_limitup_evidence_high_price": evidence_high,
|
"strict_limitup_evidence_prev_close": evidence_prev_close,
|
"max_prior_30_high_return_pct": max_return_pct,
|
"data_gap_reason": data_gap_reason,
|
"upstream_recent_limitup_30_flag": clean_str(getattr(row, "recent_limitup_30_flag", "")),
|
"upstream_last_limitup_date": clean_str(getattr(row, "last_limitup_date", "")),
|
"upstream_days_since_last_limitup": clean_str(getattr(row, "days_since_last_limitup", "")),
|
"evaluation_reason_cn": reason,
|
}
|
)
|
return pd.DataFrame(rows)
|
|
|
def make_case_summary(detail: pd.DataFrame) -> pd.DataFrame:
|
grouped = detail.groupby("case_id", dropna=False)
|
summary = grouped.agg(
|
source_buy_lots=("strict_lot_id", "count"),
|
strict_pass_lots=("strict_limitup_pass_flag", "sum"),
|
strict_fail_lots=("strict_limitup_status", lambda s: int((s == "STRICT_LIMITUP_FAIL").sum())),
|
held_lots=("strict_limitup_status", lambda s: int(s.astype(str).str.endswith("_HELD").sum())),
|
).reset_index()
|
summary["case_has_any_strict_fail_or_held"] = (summary["strict_fail_lots"] + summary["held_lots"]) > 0
|
summary["case_strict_all_source_buys_pass_flag"] = summary["strict_pass_lots"].eq(summary["source_buy_lots"])
|
summary["case_scope_status"] = summary["case_strict_all_source_buys_pass_flag"].map(
|
{True: "ALL_ORIGINAL_BUY_STRICT_LIMITUP_PASS", False: "HAS_ORIGINAL_BUY_STRICT_LIMITUP_FAIL_OR_HELD"}
|
)
|
return summary
|
|
|
def build_manifest(files: list[Path]) -> pd.DataFrame:
|
rows = []
|
for path in sorted(files, key=lambda p: rel(p)):
|
rows.append(
|
{
|
"path": rel(path),
|
"size": path.stat().st_size,
|
"sha256": sha256_file(path),
|
}
|
)
|
return pd.DataFrame(rows)
|
|
|
def main() -> None:
|
ROOT.mkdir(parents=True, exist_ok=True)
|
(ROOT / "tools").mkdir(parents=True, exist_ok=True)
|
source, rolling_lots, rolling_orders = load_scope()
|
symbols = sorted(source["symbol"].dropna().astype(str).unique())
|
min_signal = pd.to_datetime(source["signal_trade_date"]).min()
|
max_signal = pd.to_datetime(source["signal_trade_date"]).max()
|
pull_start = (min_signal - pd.Timedelta(days=80)).date().isoformat()
|
pull_end = max_signal.date().isoformat()
|
daily = query_daily(symbols, pull_start, pull_end)
|
detail = evaluate_source_buys(source, daily)
|
case_summary = make_case_summary(detail)
|
|
board_rows = []
|
for symbol in sorted(detail["symbol"].unique()):
|
board, rate = board_policy(symbol)
|
board_rows.append({"symbol": symbol, "board_policy": board, "limit_rate": rate})
|
board_df = pd.DataFrame(board_rows)
|
|
colleague_expected = {
|
"source_buy_lots": 710,
|
"rolling_low_buy_lots": 25,
|
"strict_pass_lots": 235,
|
"strict_fail_or_nonpass_lots": 475,
|
"cases_with_any_nonpass": 222,
|
}
|
pass_count = int(detail["strict_limitup_pass_flag"].sum())
|
fail_or_held = int(len(detail) - pass_count)
|
cases_nonpass = int((~case_summary["case_strict_all_source_buys_pass_flag"]).sum())
|
mismatch = pd.DataFrame(
|
[
|
{
|
"metric": "source_buy_lots",
|
"colleague_readout": colleague_expected["source_buy_lots"],
|
"this_run_readout": int(len(detail)),
|
"match_flag": int(len(detail)) == colleague_expected["source_buy_lots"],
|
},
|
{
|
"metric": "rolling_low_buy_lots",
|
"colleague_readout": colleague_expected["rolling_low_buy_lots"],
|
"this_run_readout": int(len(rolling_lots)),
|
"match_flag": int(len(rolling_lots)) == colleague_expected["rolling_low_buy_lots"],
|
},
|
{
|
"metric": "strict_pass_lots",
|
"colleague_readout": colleague_expected["strict_pass_lots"],
|
"this_run_readout": pass_count,
|
"match_flag": pass_count == colleague_expected["strict_pass_lots"],
|
},
|
{
|
"metric": "strict_fail_or_nonpass_lots",
|
"colleague_readout": colleague_expected["strict_fail_or_nonpass_lots"],
|
"this_run_readout": fail_or_held,
|
"match_flag": fail_or_held == colleague_expected["strict_fail_or_nonpass_lots"],
|
},
|
{
|
"metric": "cases_with_any_nonpass",
|
"colleague_readout": colleague_expected["cases_with_any_nonpass"],
|
"this_run_readout": cases_nonpass,
|
"match_flag": cases_nonpass == colleague_expected["cases_with_any_nonpass"],
|
},
|
]
|
)
|
|
source_manifest = []
|
for path in [
|
SOURCE_V1 / "strict_position_lot_ledger.csv",
|
SOURCE_V1 / "strict_order_ledger.csv",
|
SOURCE_FULL / "full_selected_candidate_ledger.csv",
|
SOURCE_FULL / "candidate_generation_summary.json",
|
]:
|
source_manifest.append(
|
{
|
"source_run_id": path.parent.name,
|
"path": path.relative_to(PROJECT_ROOT).as_posix(),
|
"size": path.stat().st_size,
|
"sha256": sha256_file(path),
|
}
|
)
|
source_manifest_df = pd.DataFrame(source_manifest)
|
|
files: list[Path] = []
|
run_config = {
|
"schema_version": "1.0",
|
"run_id": RUN_ID,
|
"task_id": TASK_ID,
|
"generated_at": now_iso(),
|
"design_audit_id": DESIGN_AUDIT_ID,
|
"issue_id": ISSUE_ID,
|
"source_runs": {
|
"v1_run": "RUN-ANA-WUJI-STRICT-SELL-ROLLING-REPAIR-20260609-001",
|
"full_run": "RUN-ANA-WUJI-FULL-2023-2026-20260608-001",
|
},
|
"source_buy_identification": {
|
"source_buy_rule": STRICT_POLICY["source_buy_rule"],
|
"rolling_buy_exclusion_rule": STRICT_POLICY["rolling_buy_rule"],
|
"expected_source_buy_lots": 710,
|
"expected_rolling_low_buy_lots": 25,
|
},
|
"signal_date_policy": {
|
"primary_source": "full_selected_candidate_ledger.signal_trade_date joined by candidate_id/case_id/symbol",
|
"entry_date_source": "full_selected_candidate_ledger.entry_trade_date",
|
"missing_or_conflict_status": "SIGNAL_DATE_MISSING_HELD",
|
},
|
"strict_limitup_policy": STRICT_POLICY,
|
"data_sources": {
|
"daily_price_table": "tianxia.a_share_daily_price",
|
"daily_price_fields": ["trade_date", "symbol", "open_price", "high_price", "low_price", "close_price", "volume", "amount"],
|
"source_v1_lot_ledger": (SOURCE_V1 / "strict_position_lot_ledger.csv").relative_to(PROJECT_ROOT).as_posix(),
|
"source_v1_order_ledger": (SOURCE_V1 / "strict_order_ledger.csv").relative_to(PROJECT_ROOT).as_posix(),
|
"source_full_selected_candidate_ledger": (SOURCE_FULL / "full_selected_candidate_ledger.csv").relative_to(PROJECT_ROOT).as_posix(),
|
},
|
"rounding_and_tolerance": {
|
"reason": "A-share price-limit hits are rounded to price ticks; strict threshold uses a 0.05 percentage-point tolerance and exposes exact high/prev_close return.",
|
"tolerance_pct_points": STRICT_POLICY["tolerance_pct_points"],
|
},
|
"boundary_statuses": [
|
"STRICT_LIMITUP_PASS",
|
"STRICT_LIMITUP_FAIL",
|
"SIGNAL_DATE_MISSING_HELD",
|
"DAILY_DATA_GAP_HELD",
|
"PRIOR_30_TRADING_DAY_WINDOW_INCOMPLETE_HELD",
|
],
|
"citation_boundary": "Execution review is required before this run's readouts can be cited as formal conclusions.",
|
}
|
files.append(write_json(run_config, "run_config.json"))
|
run_config_md = ROOT / "run_config.md"
|
run_config_md.write_text(
|
"\n".join(
|
[
|
f"# {RUN_ID} run_config",
|
"",
|
f"- 设计审计 ID:`{DESIGN_AUDIT_ID}`",
|
f"- 关联问题:`{ISSUE_ID}`",
|
"- 原始新开仓 BUY:`lot_source_type == SOURCE_BUY` 且 `tranche_index == 1`。",
|
"- 滚动低吸 BUY:`lot_source_type == ROLLING_LOW_BUY` 且 `tranche_index > 1`,本轮从新开仓严格涨停复核排除。",
|
"- 信号日来源:`full_selected_candidate_ledger.signal_trade_date`,按 `candidate_id / case_id / symbol` 追溯。",
|
"- 严格涨停窗口:信号日前 30 个交易日,不含信号日。",
|
"- 涨停判定:`a_share_daily_price.high_price / prev_close - 1` 达到板块阈值,容差 0.05 个百分点。",
|
"- 板块阈值:`.BJ` / 8、4、9 开头为 30%;`688*.SH` 为 20%;`300*.SZ` / `301*.SZ` 为 20%;其他默认 10%。",
|
"- 边界:执行审核通过前,不引用本包读数为正式审计结论。",
|
]
|
),
|
encoding="utf-8",
|
)
|
files.append(run_config_md)
|
files.append(write_csv(detail, "original_buy_limitup_scope_check.csv"))
|
files.append(write_csv(case_summary, "case_limitup_scope_summary.csv"))
|
files.append(write_csv(board_df, "symbol_board_policy_check.csv"))
|
files.append(write_csv(mismatch, "mismatch_with_colleague_readout.csv"))
|
files.append(write_csv(source_manifest_df, "source_artifact_manifest.csv"))
|
|
self_checks = [
|
{
|
"check_id": "SOURCE_BUY_LOT_COUNT_IS_710",
|
"status": "PASS" if len(detail) == 710 else "FAIL",
|
"detail": f"source_buy_lots={len(detail)}",
|
},
|
{
|
"check_id": "ROLLING_LOW_BUY_EXCLUDED_COUNT_IS_25",
|
"status": "PASS" if len(rolling_lots) == 25 else "FAIL",
|
"detail": f"rolling_low_buy_lots={len(rolling_lots)}; rolling_buy_orders={len(rolling_orders)}",
|
},
|
{
|
"check_id": "STATUS_TOTAL_EQUALS_710",
|
"status": "PASS" if int(detail["strict_limitup_status"].count()) == 710 else "FAIL",
|
"detail": detail["strict_limitup_status"].value_counts().to_dict(),
|
},
|
{
|
"check_id": "SOURCE_BUY_TRACE_FIELDS_COMPLETE",
|
"status": "PASS"
|
if not detail[["case_id", "symbol", "candidate_id", "signal_trade_date", "entry_trade_date", "strict_lot_id", "v1_order_id"]]
|
.replace("", pd.NA)
|
.isna()
|
.any()
|
.any()
|
else "FAIL",
|
"detail": "case_id/symbol/candidate_id/signal_trade_date/entry_trade_date/strict_lot_id/v1_order_id present for all source BUY rows.",
|
},
|
{
|
"check_id": "WINDOW_EXCLUDES_SIGNAL_DAY",
|
"status": "PASS" if bool(detail["window_excludes_signal_day_flag"].all()) else "FAIL",
|
"detail": "All rows use daily.trade_date < signal_trade_date.",
|
},
|
{
|
"check_id": "STRICT_WINDOW_DAY_COUNT_OR_HELD",
|
"status": "PASS"
|
if bool((detail["window_trading_days"].ge(30) | detail["strict_limitup_status"].eq("PRIOR_30_TRADING_DAY_WINDOW_INCOMPLETE_HELD")).all())
|
else "FAIL",
|
"detail": "Rows with fewer than 30 prior trading days are marked HELD.",
|
},
|
{
|
"check_id": "BOARD_POLICY_ASSIGNED",
|
"status": "PASS" if not detail["board_policy"].isna().any() else "FAIL",
|
"detail": board_df["board_policy"].value_counts().to_dict(),
|
},
|
{
|
"check_id": "MATCHES_COLLEAGUE_READOUT_TABLE_CREATED",
|
"status": "PASS" if not mismatch.empty else "FAIL",
|
"detail": mismatch[["metric", "match_flag"]].to_dict("records"),
|
},
|
{
|
"check_id": "SOURCE_ARTIFACT_MANIFEST_CREATED",
|
"status": "PASS" if len(source_manifest_df) == 4 else "FAIL",
|
"detail": f"source_artifacts={len(source_manifest_df)}",
|
},
|
]
|
self_check_df = pd.DataFrame(self_checks)
|
files.append(write_csv(self_check_df, "self_check_items.csv"))
|
|
summary = {
|
"schema_version": "1.0",
|
"run_id": RUN_ID,
|
"task_id": TASK_ID,
|
"generated_at": now_iso(),
|
"stage": "ORIGINAL_BUY_LIMITUP_SCOPE_CHECK_DONE_READY_FOR_EXEC_REVIEW",
|
"design_audit_id": DESIGN_AUDIT_ID,
|
"issue_id": ISSUE_ID,
|
"source_runs": [
|
"RUN-ANA-WUJI-STRICT-SELL-ROLLING-REPAIR-20260609-001",
|
"RUN-ANA-WUJI-FULL-2023-2026-20260608-001",
|
],
|
"strict_policy": STRICT_POLICY,
|
"readouts": {
|
"source_buy_lots": int(len(detail)),
|
"rolling_low_buy_lots_excluded": int(len(rolling_lots)),
|
"strict_limitup_pass_lots": pass_count,
|
"strict_limitup_fail_or_held_lots": fail_or_held,
|
"strict_limitup_fail_lots": int((detail["strict_limitup_status"] == "STRICT_LIMITUP_FAIL").sum()),
|
"held_lots": int(detail["strict_limitup_status"].astype(str).str.endswith("_HELD").sum()),
|
"cases_total": int(case_summary["case_id"].nunique()),
|
"cases_all_source_buys_pass": int(case_summary["case_strict_all_source_buys_pass_flag"].sum()),
|
"cases_with_any_fail_or_held": cases_nonpass,
|
},
|
"colleague_readout_comparison": mismatch.to_dict("records"),
|
"conclusion_boundary": [
|
"This package only checks original SOURCE_BUY strict prior limit-up memory.",
|
"ROLLING_LOW_BUY lots are excluded from original new-position buy screening.",
|
"This package does not change V1 sell/rolling-ledger correctness or audited V1 return arithmetic.",
|
"Execution review is required before these readouts can be cited as formal audit conclusions.",
|
],
|
}
|
files.append(write_json(summary, "summary.json"))
|
|
summary_md = ROOT / "summary.md"
|
summary_md.write_text(
|
"\n".join(
|
[
|
f"# {RUN_ID} 摘要",
|
"",
|
f"- 生成时间:{summary['generated_at']}",
|
f"- 设计审计 ID:`{DESIGN_AUDIT_ID}`",
|
f"- 关联问题:`{ISSUE_ID}`",
|
f"- 原始新开仓 BUY lot:{len(detail)}",
|
f"- 已排除滚动低吸 BUY lot:{len(rolling_lots)}",
|
f"- 严格涨停通过 lot:{pass_count}",
|
f"- 严格涨停未通过或待审 lot:{fail_or_held}",
|
f"- 至少一只原始 BUY 未通过或待审的 case:{cases_nonpass}",
|
"",
|
"## 口径",
|
"",
|
"严格涨停窗口为信号日前 30 个交易日,不含信号日;价源使用 `a_share_daily_price.high_price / prev_close` 是否达到板块阈值,容差 0.05 个百分点。",
|
"",
|
"## 边界",
|
"",
|
"本包只复核 V1 原始新开仓 BUY 的严格近期涨停记忆,不改变 V1 卖点、滚动低吸、人工裁决、订单、lot、账户账本或已审核收益读数。执行审核通过前不得把本包读数作为正式审计结论引用。",
|
]
|
),
|
encoding="utf-8",
|
)
|
files.append(summary_md)
|
self_check = {
|
"run_id": RUN_ID,
|
"generated_at": summary["generated_at"],
|
"overall_status": "PASS_FOR_EXECUTION_REVIEW_READY"
|
if all(item["status"] == "PASS" for item in self_checks)
|
else "FAIL",
|
"pass_count": sum(1 for item in self_checks if item["status"] == "PASS"),
|
"fail_count": sum(1 for item in self_checks if item["status"] != "PASS"),
|
"items": self_checks,
|
}
|
files.append(write_json(self_check, "self_check.json"))
|
|
# Add this script after generated artifacts exist.
|
files.append(Path(__file__))
|
manifest_df = build_manifest(files)
|
manifest_csv = write_csv(manifest_df, "manifest.csv")
|
write_json({"run_id": RUN_ID, "generated_at": now_iso(), "manifest_self_included": False, "files": manifest_df.to_dict("records")}, "manifest.json")
|
# Manifest files are intentionally not self-listed; self-hashes are not stable after write.
|
manifest_df.to_csv(manifest_csv, index=False, encoding="utf-8-sig")
|
|
print(json.dumps(summary["readouts"], ensure_ascii=False, indent=2))
|
|
|
if __name__ == "__main__":
|
main()
|