from __future__ import annotations
|
|
import csv
|
import hashlib
|
import json
|
import math
|
import os
|
import re
|
import shutil
|
from collections import defaultdict
|
from datetime import datetime
|
from decimal import Decimal
|
from pathlib import Path
|
|
import pandas as pd
|
|
try:
|
import pymysql
|
except ImportError: # pragma: no cover - runtime dependency check is recorded in self-check.
|
pymysql = None
|
|
|
RUN_ID = "RUN-ANA-WUJI-RETURN-STAT-PILOT-20260608-001"
|
SOURCE_RUN_ID = "RUN-ANA-WUJI-BASELINE-PILOT-20260607-001"
|
CASE_MATTER_ID = "ANA-WUJI-BASELINE-2023-2026"
|
DESIGN_ID = "DESIGN-WUJI-RETURN-STAT-READY-20260608"
|
SOURCE_DESIGN_ID = "DESIGN-WUJI-BASELINE-FLOW-20260607"
|
DESIGN_AUDIT_ID = "AUDIT-ANA-WUJI-RETURN-STAT-READY-20260608-DESIGN-001"
|
SOURCE_AUDIT_IDS = [
|
"AUDIT-ANA-WUJI-BASELINE-FLOW-20260607-001",
|
"AUDIT-ANA-WUJI-BASELINE-PILOT-20260608-EXEC-REREVIEW-001",
|
"AUDIT-ANA-WUJI-BASELINE-PILOT-20260608-EXIT-REVIEW-001",
|
"AUDIT-ANA-WUJI-BASELINE-PILOT-20260608-CANDIDATE-POOL-SUPP-001",
|
]
|
ROOT = Path(__file__).resolve().parents[1]
|
PROJECT_ROOT = ROOT.parents[2]
|
SOURCE_ROOT = PROJECT_ROOT / "ana-data" / "result" / SOURCE_RUN_ID
|
LOCAL_DB_INDEX = Path(r"D:\strategy_project\s-system-doc\observer\天下模型沉淀\数据库索引数据.md")
|
|
|
CONFIG = {
|
"schema_version": "1.0",
|
"run_id": RUN_ID,
|
"case_matter_id": CASE_MATTER_ID,
|
"design_id": DESIGN_ID,
|
"design_audit_id": DESIGN_AUDIT_ID,
|
"source_run_id": SOURCE_RUN_ID,
|
"source_audit_ids": SOURCE_AUDIT_IDS,
|
"config_frozen_at": None,
|
"status_policy": {
|
"execution_status": "RETURN_STAT_READY_CANDIDATE_EXECUTION_REVIEW_REQUIRED",
|
"return_stat_ready": False,
|
"allowed_scope": "only the 7 representative pilot cases from source run",
|
"not_allowed": [
|
"No full 2023-2026 return or success-rate conclusion.",
|
"No forced SELL for WINDOW_END_VALUATION_ONLY or EXIT_DATA_GAP_HELD.",
|
"No expansion beyond the audited pilot cases in this run.",
|
],
|
},
|
"cost_model": {
|
"cost_model_id": "WUJI_RETURN_STAT_COST_V1_20260608",
|
"initial_cash_cny": 1000000.0,
|
"cash_unit": "CNY",
|
"round_lot_size": 100,
|
"min_order_lot": 1,
|
"rounding_policy": "floor_to_100_shares_per_lot_budget",
|
"cash_residual_policy": "unused_budget_remains_cash_in_case_account",
|
"commission_rate_by_side": {
|
"buy": 0.00025,
|
"sell": 0.00025,
|
"source": "Inherited from audited source run_config commission_rate_each_side=0.00025; broker-specific minimum fee is not applied in this percent-account pilot.",
|
},
|
"stamp_tax_sell_rate_by_date": [
|
{
|
"start_date": "1900-01-01",
|
"end_date": "2023-08-27",
|
"rate": 0.001,
|
"source": "Run-frozen historical A-share sell-side stamp-tax schedule; reviewer should verify official fee basis before expanding sample.",
|
},
|
{
|
"start_date": "2023-08-28",
|
"end_date": "2099-12-31",
|
"rate": 0.0005,
|
"source": "Run-frozen schedule reflecting the 2023-08-28 half-rate stamp-tax policy; reviewer should verify official fee basis before expanding sample.",
|
},
|
],
|
"transfer_fee_rate_by_side": {
|
"buy": 0.00001,
|
"sell": 0.00001,
|
"source": "Run-frozen A-share transfer-fee sensitivity assumption; reviewer should verify official exchange/clearing fee basis before expanding sample.",
|
},
|
"slippage_bps": {
|
"buy": 5.0,
|
"sell": 5.0,
|
"source": "Internal conservative sensitivity setting for RETURN_STAT_READY preparation, not a note baseline rule.",
|
},
|
"price_rounding_decimal_places": 4,
|
"money_rounding_decimal_places": 2,
|
"return_rounding_decimal_places": 8,
|
},
|
"tradeability_policy": {
|
"data_source": "MYSQL_TIANXIA_LOCAL.a_share_minute_price + a_share_daily_price",
|
"limit_rate_rule": {
|
"BJ": 0.30,
|
"STAR_688": 0.20,
|
"CHINEXT_300_301": 0.20,
|
"MAINBOARD_DEFAULT": 0.10,
|
},
|
"limit_price_rounding": "round_to_0.01",
|
"buy_policy": "If BUY decision price is at upper limit and minute bar is locked at upper limit, mark LIMIT_UP_BUY_EXECUTION_HELD.",
|
"sell_policy": "If SELL decision price is at lower limit and minute bar is locked at lower limit, mark LIMIT_DOWN_SELL_EXECUTION_HELD.",
|
"missing_data_policy": "Missing minute or previous-close data is HELD for main return scope.",
|
},
|
"sensitivity_models": [
|
{"model_id": "LOW_CAPITAL_100K", "initial_cash_cny": 100000.0},
|
{"model_id": "MAIN_CAPITAL_1M", "initial_cash_cny": 1000000.0},
|
{"model_id": "HIGH_CAPITAL_10M", "initial_cash_cny": 10000000.0},
|
],
|
}
|
|
|
def now_iso() -> str:
|
return datetime.now().astimezone().isoformat(timespec="seconds")
|
|
|
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 read_csv(name: str) -> pd.DataFrame:
|
return pd.read_csv(SOURCE_ROOT / name, encoding="utf-8-sig", keep_default_na=False)
|
|
|
def write_csv(path: Path, rows: list[dict], fieldnames: list[str]) -> None:
|
with path.open("w", encoding="utf-8-sig", newline="") as f:
|
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
writer.writeheader()
|
for row in rows:
|
writer.writerow(row)
|
|
|
def write_json(path: Path, data: dict) -> None:
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
def clean_float(value, default: float = 0.0) -> float:
|
if value is None:
|
return default
|
if isinstance(value, str) and not value.strip():
|
return default
|
if isinstance(value, Decimal):
|
return float(value)
|
try:
|
if pd.isna(value):
|
return default
|
except TypeError:
|
pass
|
return float(value)
|
|
|
def clean_bool(value) -> bool:
|
return str(value).strip().lower() in {"true", "1", "yes"}
|
|
|
def money(value: float) -> float:
|
return round(float(value), CONFIG["cost_model"]["money_rounding_decimal_places"])
|
|
|
def pct(value: float) -> float:
|
return round(float(value), CONFIG["cost_model"]["return_rounding_decimal_places"])
|
|
|
def normalize_time(value) -> str:
|
text = str(value)
|
if "days" in text:
|
text = text.split()[-1]
|
if "." in text:
|
text = text.split(".")[0]
|
parts = text.split(":")
|
if len(parts) >= 3:
|
return f"{int(parts[0]):02d}:{int(parts[1]):02d}:{int(float(parts[2])):02d}"
|
return text
|
|
|
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():
|
if pymysql is None:
|
raise RuntimeError("pymysql is not installed.")
|
return pymysql.connect(
|
host="127.0.0.1",
|
port=3306,
|
user="root",
|
password=read_password(),
|
database="tianxia",
|
charset="utf8mb4",
|
connect_timeout=5,
|
read_timeout=120,
|
cursorclass=pymysql.cursors.DictCursor,
|
)
|
|
|
def symbol_market_group(symbol: str) -> str:
|
code = symbol.upper().strip()
|
if code.endswith(".BJ"):
|
return "BJ"
|
if re.match(r"^688\d{3}\.SH$", code):
|
return "STAR_688"
|
if re.match(r"^30[01]\d{3}\.SZ$", code):
|
return "CHINEXT_300_301"
|
return "MAINBOARD_DEFAULT"
|
|
|
def symbol_limit_rate(symbol: str) -> float:
|
group = symbol_market_group(symbol)
|
return float(CONFIG["tradeability_policy"]["limit_rate_rule"][group])
|
|
|
def stamp_tax_rate(trade_date: str) -> float:
|
for row in CONFIG["cost_model"]["stamp_tax_sell_rate_by_date"]:
|
if row["start_date"] <= trade_date <= row["end_date"]:
|
return float(row["rate"])
|
raise ValueError(f"No stamp-tax rate configured for {trade_date}")
|
|
|
def fetch_tradeability(orders: pd.DataFrame) -> tuple[dict[str, dict], list[str]]:
|
diagnostics: list[str] = []
|
result: dict[str, dict] = {}
|
try:
|
conn = get_conn()
|
except Exception as exc: # noqa: BLE001
|
diagnostics.append(f"DB_CONNECT_FAILED: {exc}")
|
return result, diagnostics
|
|
try:
|
with conn.cursor() as cur:
|
for _, order in orders.iterrows():
|
order_id = str(order.order_id)
|
symbol = str(order.symbol)
|
trade_date = str(order.trade_date)
|
trade_time = normalize_time(order.trade_time)
|
cur.execute(
|
"""
|
SELECT open_price, high_price, low_price, close_price, volume
|
FROM a_share_minute_price
|
WHERE symbol=%s AND trade_date=%s AND trade_time=%s
|
LIMIT 1
|
""",
|
(symbol, trade_date, trade_time),
|
)
|
minute = cur.fetchone()
|
cur.execute(
|
"""
|
SELECT trade_date, close_price
|
FROM a_share_daily_price
|
WHERE symbol=%s AND trade_date < %s
|
ORDER BY trade_date DESC
|
LIMIT 1
|
""",
|
(symbol, trade_date),
|
)
|
prev_daily = cur.fetchone()
|
cur.execute(
|
"""
|
SELECT trade_date, trade_time, close_price
|
FROM a_share_minute_price
|
WHERE symbol=%s AND trade_date < %s
|
ORDER BY trade_date DESC, trade_time DESC
|
LIMIT 1
|
""",
|
(symbol, trade_date),
|
)
|
prev_minute = cur.fetchone()
|
if minute is None or (prev_daily is None and prev_minute is None):
|
status = "DB_TRADEABILITY_DATA_MISSING_HELD"
|
reason = "缺少对应 1 分钟 bar 或同口径前一交易日收盘价,不能放入主收益口径。"
|
result[order_id] = {
|
"status": status,
|
"reason": reason,
|
"minute_open": "",
|
"minute_high": "",
|
"minute_low": "",
|
"minute_close": "",
|
"minute_volume": "",
|
"prev_close": "",
|
"prev_close_source": "",
|
"daily_prev_close": "",
|
"minute_prev_close": "",
|
"limit_rate": "",
|
"upper_limit_price": "",
|
"lower_limit_price": "",
|
}
|
continue
|
daily_prev_close = clean_float(prev_daily["close_price"]) if prev_daily else 0.0
|
minute_prev_close = clean_float(prev_minute["close_price"]) if prev_minute else 0.0
|
# The daily table is front-adjusted while the minute table is raw in some historical rows.
|
# Limit checks must stay in the same price space as the order/minute evidence.
|
if minute_prev_close > 0:
|
prev_close = minute_prev_close
|
prev_close_source = "a_share_minute_price_prev_trade_day_last_close"
|
else:
|
prev_close = daily_prev_close
|
prev_close_source = "a_share_daily_price_prev_close"
|
limit_rate = symbol_limit_rate(symbol)
|
upper = round(prev_close * (1 + limit_rate), 2)
|
lower = round(prev_close * (1 - limit_rate), 2)
|
price = clean_float(order.price)
|
high = clean_float(minute["high_price"])
|
low = clean_float(minute["low_price"])
|
volume = clean_float(minute["volume"])
|
action = str(order.action).upper()
|
status = "TRADEABILITY_PASS"
|
reason = "1 分钟 bar 存在,价格落在 bar 范围内,未命中保守涨跌停锁死规则。"
|
if price < low - 0.0001 or price > high + 0.0001:
|
status = "PRICE_OUTSIDE_MINUTE_BAR_HELD"
|
reason = "订单价格不在对应 1 分钟 bar 高低价范围内。"
|
elif action == "BUY" and price >= upper - 0.005 and low >= upper - 0.005:
|
status = "LIMIT_UP_BUY_EXECUTION_HELD"
|
reason = "BUY 价格处于涨停附近且分钟 bar 锁在涨停附近,按保守口径不可放入主收益。"
|
elif action == "SELL" and price <= lower + 0.005 and high <= lower + 0.005:
|
status = "LIMIT_DOWN_SELL_EXECUTION_HELD"
|
reason = "SELL 价格处于跌停附近且分钟 bar 锁在跌停附近,按保守口径不可放入主收益。"
|
elif volume <= 0:
|
status = "ZERO_VOLUME_MINUTE_BAR_HELD"
|
reason = "对应 1 分钟 bar 成交量为 0,不能证明可成交。"
|
result[order_id] = {
|
"status": status,
|
"reason": reason,
|
"minute_open": clean_float(minute["open_price"]),
|
"minute_high": high,
|
"minute_low": low,
|
"minute_close": clean_float(minute["close_price"]),
|
"minute_volume": volume,
|
"prev_close": prev_close,
|
"prev_close_source": prev_close_source,
|
"daily_prev_close": daily_prev_close,
|
"minute_prev_close": minute_prev_close,
|
"limit_rate": limit_rate,
|
"upper_limit_price": upper,
|
"lower_limit_price": lower,
|
}
|
finally:
|
conn.close()
|
return result, diagnostics
|
|
|
def make_order_rows(orders: pd.DataFrame, lots: pd.DataFrame, tradeability: dict[str, dict]) -> tuple[list[dict], dict[str, dict]]:
|
initial_cash = float(CONFIG["cost_model"]["initial_cash_cny"])
|
lot_size = int(CONFIG["cost_model"]["round_lot_size"])
|
commission_buy = float(CONFIG["cost_model"]["commission_rate_by_side"]["buy"])
|
commission_sell = float(CONFIG["cost_model"]["commission_rate_by_side"]["sell"])
|
transfer_buy = float(CONFIG["cost_model"]["transfer_fee_rate_by_side"]["buy"])
|
transfer_sell = float(CONFIG["cost_model"]["transfer_fee_rate_by_side"]["sell"])
|
slippage_buy = float(CONFIG["cost_model"]["slippage_bps"]["buy"])
|
slippage_sell = float(CONFIG["cost_model"]["slippage_bps"]["sell"])
|
|
buy_lot_by_order = {str(row.order_id): row for _, row in lots.iterrows()}
|
buy_calc_by_lot: dict[str, dict] = {}
|
rows: list[dict] = []
|
seq = 0
|
for _, order in orders.iterrows():
|
seq += 1
|
action = str(order.action).upper()
|
source_order_id = str(order.order_id)
|
lot_id = str(order.source_lot_id) if action == "SELL" else ""
|
if action == "BUY":
|
lot = buy_lot_by_order.get(source_order_id)
|
lot_id = "" if lot is None else str(lot.trade_lot_id)
|
price = clean_float(order.price)
|
position_pct = abs(clean_float(order.position_delta_pct))
|
planned_budget = initial_cash * position_pct
|
if action == "BUY":
|
raw_shares = planned_budget / price if price > 0 else 0.0
|
shares = math.floor(raw_shares / lot_size) * lot_size
|
slippage_bps = slippage_buy
|
effective_price = price * (1 + slippage_bps / 10000.0)
|
gross_notional = shares * effective_price
|
commission_rate = commission_buy
|
transfer_rate = transfer_buy
|
stamp_rate = 0.0
|
commission = gross_notional * commission_rate
|
transfer = gross_notional * transfer_rate
|
stamp_tax = 0.0
|
total_fee = commission + transfer + stamp_tax
|
cash_delta = -(gross_notional + total_fee)
|
if lot_id:
|
buy_calc_by_lot[lot_id] = {
|
"shares": shares,
|
"buy_effective_price": effective_price,
|
"buy_gross_notional": gross_notional,
|
"buy_total_fee": total_fee,
|
"buy_cash_delta": cash_delta,
|
"integer_lot_status": "INTEGER_LOT_PASS" if shares >= lot_size else "INTEGER_LOT_NOT_EXECUTABLE_HELD",
|
}
|
else:
|
buy_calc = buy_calc_by_lot.get(lot_id, {})
|
shares = clean_float(buy_calc.get("shares"))
|
slippage_bps = slippage_sell
|
effective_price = price * (1 - slippage_bps / 10000.0)
|
gross_notional = shares * effective_price
|
commission_rate = commission_sell
|
transfer_rate = transfer_sell
|
stamp_rate = stamp_tax_rate(str(order.trade_date))
|
commission = gross_notional * commission_rate
|
transfer = gross_notional * transfer_rate
|
stamp_tax = gross_notional * stamp_rate
|
total_fee = commission + transfer + stamp_tax
|
cash_delta = gross_notional - total_fee
|
trade = tradeability.get(source_order_id, {"status": "DB_TRADEABILITY_NOT_CHECKED_HELD", "reason": "未取得源库可成交检查结果。"})
|
row = {
|
"return_order_id": f"ORD-{RUN_ID}-{seq:04d}",
|
"source_order_id": source_order_id,
|
"source_run_id": SOURCE_RUN_ID,
|
"trade_lot_id": lot_id,
|
"case_id": str(order.case_id),
|
"symbol": str(order.symbol),
|
"trade_date": str(order.trade_date),
|
"trade_time": normalize_time(order.trade_time),
|
"action": action,
|
"source_price": f"{price:.4f}",
|
"slippage_bps": f"{slippage_bps:.2f}",
|
"effective_price_after_slippage": f"{effective_price:.4f}",
|
"shares": int(shares),
|
"planned_position_pct": f"{position_pct:.8f}",
|
"planned_budget_cny": f"{planned_budget:.2f}",
|
"gross_notional_cny": f"{gross_notional:.2f}",
|
"commission_rate": f"{commission_rate:.8f}",
|
"commission_cny": f"{commission:.2f}",
|
"stamp_tax_rate": f"{stamp_rate:.8f}",
|
"stamp_tax_cny": f"{stamp_tax:.2f}",
|
"transfer_fee_rate": f"{transfer_rate:.8f}",
|
"transfer_fee_cny": f"{transfer:.2f}",
|
"total_fee_cny": f"{total_fee:.2f}",
|
"cash_delta_cny": f"{cash_delta:.2f}",
|
"tradeability_status": trade["status"],
|
"tradeability_reason": trade["reason"],
|
"prev_close": trade.get("prev_close", ""),
|
"prev_close_source": trade.get("prev_close_source", ""),
|
"daily_prev_close": trade.get("daily_prev_close", ""),
|
"minute_prev_close": trade.get("minute_prev_close", ""),
|
"limit_rate": trade.get("limit_rate", ""),
|
"upper_limit_price": trade.get("upper_limit_price", ""),
|
"lower_limit_price": trade.get("lower_limit_price", ""),
|
"minute_low": trade.get("minute_low", ""),
|
"minute_high": trade.get("minute_high", ""),
|
"minute_volume": trade.get("minute_volume", ""),
|
"evidence_image_path": str(order.evidence_image_path),
|
}
|
rows.append(row)
|
return rows, buy_calc_by_lot
|
|
|
def make_lot_scope_rows(lots: pd.DataFrame, order_rows: list[dict], case_main_candidate: dict[str, bool]) -> tuple[list[dict], dict[str, dict]]:
|
orders_by_lot: dict[str, list[dict]] = defaultdict(list)
|
for order in order_rows:
|
if order["trade_lot_id"]:
|
orders_by_lot[order["trade_lot_id"]].append(order)
|
|
rows: list[dict] = []
|
lot_calc: dict[str, dict] = {}
|
for _, lot in lots.iterrows():
|
lot_id = str(lot.trade_lot_id)
|
status = str(lot.lot_status)
|
related = orders_by_lot.get(lot_id, [])
|
buy_order = next((r for r in related if r["action"] == "BUY"), None)
|
sell_order = next((r for r in related if r["action"] == "SELL"), None)
|
source_gross_return = clean_float(lot.lot_return_pct, default=0.0)
|
source_account_contribution = clean_float(lot.account_return_contribution_pct, default=0.0)
|
t1_pass = bool(str(lot.exit_trade_date) >= str(lot.sellable_from_trade_date)) if status == "CLOSED_BY_AI_SELL" else False
|
lookahead_pass = True
|
evidence_pass = bool(buy_order and buy_order["evidence_image_path"])
|
if status == "CLOSED_BY_AI_SELL":
|
evidence_pass = evidence_pass and bool(sell_order and sell_order["evidence_image_path"])
|
integer_pass = bool(buy_order and int(buy_order["shares"]) >= int(CONFIG["cost_model"]["round_lot_size"]))
|
tradeability_statuses = [r["tradeability_status"] for r in related]
|
tradeability_pass = bool(related) and all(s == "TRADEABILITY_PASS" for s in tradeability_statuses)
|
include_aux = (
|
status == "CLOSED_BY_AI_SELL"
|
and t1_pass
|
and lookahead_pass
|
and evidence_pass
|
and integer_pass
|
and tradeability_pass
|
)
|
include_case_main = include_aux and case_main_candidate.get(str(lot.case_id), False)
|
reason_parts: list[str] = []
|
if status != "CLOSED_BY_AI_SELL":
|
reason_parts.append(status)
|
if not t1_pass and status == "CLOSED_BY_AI_SELL":
|
reason_parts.append("T1_GUARD_NOT_PASS")
|
if not evidence_pass:
|
reason_parts.append("EVIDENCE_PATH_MISSING")
|
if not integer_pass:
|
reason_parts.append("INTEGER_LOT_NOT_EXECUTABLE_HELD")
|
if not tradeability_pass:
|
reason_parts.append("TRADEABILITY_NOT_PASS")
|
if include_aux and not include_case_main:
|
reason_parts.append("CASE_HAS_BOUNDARY_LOT_AUX_ONLY")
|
if not reason_parts:
|
reason_parts.append("STRICT_CLOSED_LOT_RECALC_READY")
|
buy_cash = clean_float(buy_order["cash_delta_cny"]) if buy_order else 0.0
|
sell_cash = clean_float(sell_order["cash_delta_cny"]) if sell_order else 0.0
|
buy_notional = clean_float(buy_order["gross_notional_cny"]) if buy_order else 0.0
|
net_pnl = sell_cash + buy_cash if sell_order else 0.0
|
net_account_contribution = net_pnl / float(CONFIG["cost_model"]["initial_cash_cny"])
|
net_lot_return = net_pnl / abs(buy_cash) if buy_cash else 0.0
|
gross_integer_contribution = 0.0
|
if buy_order and sell_order:
|
gross_integer_contribution = (
|
(clean_float(sell_order["shares"]) * clean_float(sell_order["source_price"]))
|
- (clean_float(buy_order["shares"]) * clean_float(buy_order["source_price"]))
|
) / float(CONFIG["cost_model"]["initial_cash_cny"])
|
row = {
|
"trade_lot_id": lot_id,
|
"case_id": str(lot.case_id),
|
"symbol": str(lot.symbol),
|
"lot_status": status,
|
"entry_trade_date": str(lot.entry_trade_date),
|
"entry_time": normalize_time(lot.entry_time),
|
"entry_price": str(lot.entry_price),
|
"sellable_from_trade_date": str(lot.sellable_from_trade_date),
|
"exit_trade_date": str(lot.exit_trade_date),
|
"exit_time": normalize_time(lot.exit_time) if str(lot.exit_time) else "",
|
"exit_price": str(lot.exit_price),
|
"shares": int(clean_float(buy_order["shares"]) if buy_order else 0),
|
"source_gross_lot_return_pct": f"{source_gross_return:.8f}",
|
"source_gross_account_contribution_pct": f"{source_account_contribution:.8f}",
|
"gross_integer_account_contribution_pct": f"{gross_integer_contribution:.8f}",
|
"net_lot_return_after_cost_pct": f"{net_lot_return:.8f}",
|
"net_account_contribution_after_cost_pct": f"{net_account_contribution:.8f}",
|
"t1_check_status": "T1_PASS" if t1_pass or status != "CLOSED_BY_AI_SELL" else "T1_GUARD_FAIL_HELD",
|
"lookahead_check_status": "LOOKAHEAD_PASS" if lookahead_pass else "LOOKAHEAD_VIOLATION_EXCLUDED",
|
"evidence_check_status": "EVIDENCE_PASS" if evidence_pass else "EVIDENCE_MISSING_HELD",
|
"integer_lot_check_status": "INTEGER_LOT_PASS" if integer_pass else "INTEGER_LOT_NOT_EXECUTABLE_HELD",
|
"tradeability_check_status": "TRADEABILITY_PASS" if tradeability_pass else ";".join(tradeability_statuses or ["NO_RELATED_ORDER"]),
|
"include_in_strict_closed_lot_recalc": str(include_aux),
|
"include_in_strict_closed_case_return": str(include_case_main),
|
"scope_status": "STRICT_CLOSED_LOT_RECALC_ONLY" if include_aux and not include_case_main else ("STRICT_CLOSED_CASE_RETURN_READY_CANDIDATE" if include_case_main else "RETURN_STAT_HELD_BOUNDARY_TABLE"),
|
"exclude_or_boundary_reason": ";".join(reason_parts),
|
"buy_evidence_image_path": buy_order["evidence_image_path"] if buy_order else "",
|
"sell_evidence_image_path": sell_order["evidence_image_path"] if sell_order else "",
|
}
|
rows.append(row)
|
lot_calc[lot_id] = row
|
return rows, lot_calc
|
|
|
def make_case_scope_and_summary(case_index: pd.DataFrame, lots: pd.DataFrame, lot_rows: list[dict]) -> tuple[list[dict], list[dict], dict]:
|
lot_by_case: dict[str, list[dict]] = defaultdict(list)
|
for row in lot_rows:
|
lot_by_case[row["case_id"]].append(row)
|
case_scope_rows: list[dict] = []
|
case_summary_rows: list[dict] = []
|
included_case_count = 0
|
success_count = 0
|
main_returns: list[float] = []
|
aux_lot_count = 0
|
aux_lot_win_count = 0
|
aux_lot_returns: list[float] = []
|
all_boundary_count = 0
|
main_drawdowns: list[float] = []
|
|
for _, case in case_index.iterrows():
|
case_id = str(case.case_id)
|
rows = lot_by_case.get(case_id, [])
|
buy_count = len(rows)
|
closed_count = sum(1 for r in rows if r["lot_status"] == "CLOSED_BY_AI_SELL")
|
boundary_rows = [r for r in rows if r["lot_status"] != "CLOSED_BY_AI_SELL"]
|
aux_rows = [r for r in rows if r["include_in_strict_closed_lot_recalc"] == "True"]
|
boundary_count = len(boundary_rows)
|
all_boundary_count += boundary_count
|
has_no_trade_case = buy_count == 0
|
all_lots_main_ready = buy_count > 0 and boundary_count == 0 and all(
|
r["include_in_strict_closed_lot_recalc"] == "True" for r in rows
|
)
|
exclude_reason = ""
|
if has_no_trade_case:
|
exclude_reason = "NO_BUY_MARKET_GATE_CLOSED_OR_NO_ENTRY_SIGNAL"
|
elif boundary_count > 0:
|
exclude_reason = "HAS_RETURN_STAT_HELD_BOUNDARY_LOT"
|
elif not all_lots_main_ready:
|
exclude_reason = "LOT_SCOPE_CHECK_NOT_ALL_PASS"
|
else:
|
exclude_reason = "STRICT_CLOSED_CASE_RETURN_READY_CANDIDATE"
|
net_contribution = sum(clean_float(r["net_account_contribution_after_cost_pct"]) for r in rows if r["include_in_strict_closed_case_return"] == "True")
|
aux_net_contribution = sum(clean_float(r["net_account_contribution_after_cost_pct"]) for r in aux_rows)
|
gross_source_contribution = sum(clean_float(r["source_gross_account_contribution_pct"]) for r in rows if r["lot_status"] == "CLOSED_BY_AI_SELL")
|
gross_integer_contribution = sum(clean_float(r["gross_integer_account_contribution_pct"]) for r in rows if r["lot_status"] == "CLOSED_BY_AI_SELL")
|
final_nav = 1.0 + net_contribution if all_lots_main_ready else ""
|
success = bool(all_lots_main_ready and clean_float(final_nav) > 1.0)
|
if all_lots_main_ready:
|
included_case_count += 1
|
success_count += 1 if success else 0
|
main_returns.append(net_contribution)
|
nav_path = [1.0]
|
nav = 1.0
|
for r in rows:
|
nav += clean_float(r["net_account_contribution_after_cost_pct"])
|
nav_path.append(nav)
|
peak = nav_path[0]
|
max_dd = 0.0
|
for value in nav_path:
|
peak = max(peak, value)
|
if peak:
|
max_dd = min(max_dd, value / peak - 1.0)
|
main_drawdowns.append(max_dd)
|
for r in aux_rows:
|
aux_lot_count += 1
|
ret = clean_float(r["net_lot_return_after_cost_pct"])
|
aux_lot_returns.append(ret)
|
if ret > 0:
|
aux_lot_win_count += 1
|
scope_row = {
|
"case_id": case_id,
|
"entry_trade_date": str(case.entry_trade_date),
|
"signal_trade_date": str(case.signal_trade_date),
|
"selection_bucket": str(case.selection_bucket),
|
"source_case_status": str(case.case_status),
|
"market_gate_status": str(case.market_gate_status),
|
"buy_lot_count": buy_count,
|
"closed_lot_count": closed_count,
|
"boundary_lot_count": boundary_count,
|
"include_in_strict_closed_case_return": str(all_lots_main_ready),
|
"include_in_aux_lot_recalc": str(len(aux_rows) > 0),
|
"scope_status": "STRICT_CLOSED_CASE_RETURN_READY_CANDIDATE" if all_lots_main_ready else "RETURN_STAT_HELD_BOUNDARY_TABLE",
|
"exclude_or_boundary_reason": exclude_reason,
|
"source_image_board_path": f"../{SOURCE_RUN_ID}/cases/{case_id}/case_image_board.md",
|
"source_story_board_path": f"../{SOURCE_RUN_ID}/cases/{case_id}/case_story_board.md",
|
}
|
summary_row = {
|
**scope_row,
|
"gross_source_closed_lot_account_contribution_pct": f"{gross_source_contribution:.8f}",
|
"gross_integer_closed_lot_account_contribution_pct": f"{gross_integer_contribution:.8f}",
|
"net_main_case_account_contribution_after_cost_pct": f"{net_contribution:.8f}" if all_lots_main_ready else "",
|
"net_aux_closed_lot_account_contribution_after_cost_pct": f"{aux_net_contribution:.8f}",
|
"final_nav_after_cost_pct": f"{clean_float(final_nav):.8f}" if all_lots_main_ready else "",
|
"case_success_after_cost_flag": str(success) if all_lots_main_ready else "",
|
}
|
case_scope_rows.append(scope_row)
|
case_summary_rows.append(summary_row)
|
summary = {
|
"strict_closed_case_count": included_case_count,
|
"strict_closed_case_success_count": success_count,
|
"strict_closed_case_success_rate": success_count / included_case_count if included_case_count else None,
|
"strict_closed_case_net_account_contribution_sum": sum(main_returns),
|
"strict_closed_case_average_net_account_contribution": sum(main_returns) / included_case_count if included_case_count else None,
|
"strict_closed_case_max_drawdown_event_based": min(main_drawdowns) if main_drawdowns else None,
|
"aux_closed_lot_count": aux_lot_count,
|
"aux_closed_lot_win_count": aux_lot_win_count,
|
"aux_closed_lot_win_rate": aux_lot_win_count / aux_lot_count if aux_lot_count else None,
|
"aux_closed_lot_average_net_return_after_cost": sum(aux_lot_returns) / aux_lot_count if aux_lot_count else None,
|
"boundary_lot_count": all_boundary_count,
|
}
|
return case_scope_rows, case_summary_rows, summary
|
|
|
def make_boundary_rows(case_scope_rows: list[dict], lot_rows: list[dict]) -> list[dict]:
|
rows: list[dict] = []
|
seq = 0
|
for row in case_scope_rows:
|
if row["include_in_strict_closed_case_return"] != "True":
|
seq += 1
|
rows.append(
|
{
|
"boundary_id": f"BOUNDARY-{seq:04d}",
|
"boundary_level": "CASE",
|
"case_id": row["case_id"],
|
"trade_lot_id": "",
|
"symbol": "",
|
"status": row["scope_status"],
|
"reason": row["exclude_or_boundary_reason"],
|
"main_return_policy": "EXCLUDED_FROM_STRICT_CLOSED_CASE_RETURN",
|
"aux_lot_policy": "AUX_LOT_ALLOWED_IF_CLOSED_AND_CHECKED",
|
"evidence_path": row["source_image_board_path"],
|
}
|
)
|
for row in lot_rows:
|
if row["scope_status"] == "RETURN_STAT_HELD_BOUNDARY_TABLE":
|
seq += 1
|
rows.append(
|
{
|
"boundary_id": f"BOUNDARY-{seq:04d}",
|
"boundary_level": "LOT",
|
"case_id": row["case_id"],
|
"trade_lot_id": row["trade_lot_id"],
|
"symbol": row["symbol"],
|
"status": row["lot_status"],
|
"reason": row["exclude_or_boundary_reason"],
|
"main_return_policy": "EXCLUDED_FROM_ALL_MAIN_RETURN_AND_WIN_RATE",
|
"aux_lot_policy": "NOT_INCLUDED_UNLESS_REAL_SELL_EXISTS_AND_REVIEW_PASSES",
|
"evidence_path": row["buy_evidence_image_path"] or row["sell_evidence_image_path"],
|
}
|
)
|
return rows
|
|
|
def check_limit_rate_policy(order_rows: list[dict]) -> tuple[bool, str]:
|
errors: list[str] = []
|
group_counts: dict[str, int] = defaultdict(int)
|
for row in order_rows:
|
symbol = row["symbol"]
|
group = symbol_market_group(symbol)
|
expected_rate = float(CONFIG["tradeability_policy"]["limit_rate_rule"][group])
|
actual_rate = clean_float(row.get("limit_rate"), default=-1.0)
|
group_counts[group] += 1
|
if abs(actual_rate - expected_rate) > 0.0000001:
|
errors.append(f"{row['return_order_id']} {symbol} limit_rate={actual_rate} expected={expected_rate}")
|
continue
|
prev_close = clean_float(row.get("prev_close"), default=0.0)
|
upper_limit = clean_float(row.get("upper_limit_price"), default=0.0)
|
lower_limit = clean_float(row.get("lower_limit_price"), default=0.0)
|
if prev_close > 0:
|
expected_upper = round(prev_close * (1 + expected_rate), 2)
|
expected_lower = round(prev_close * (1 - expected_rate), 2)
|
if abs(upper_limit - expected_upper) > 0.005 or abs(lower_limit - expected_lower) > 0.005:
|
errors.append(
|
f"{row['return_order_id']} {symbol} upper/lower={upper_limit}/{lower_limit} "
|
f"expected={expected_upper}/{expected_lower}"
|
)
|
detail_counts = ", ".join(f"{k}={v}" for k, v in sorted(group_counts.items()))
|
if errors:
|
return False, "; ".join(errors[:20])
|
return True, f"limit-rate policy matches order ledger; {detail_counts}."
|
|
|
def check_image_board_links() -> tuple[bool, str]:
|
board_paths = [ROOT / "case_image_board.md"]
|
case_root = ROOT / "cases"
|
if case_root.exists():
|
board_paths.extend(sorted(case_root.glob("*/case_image_board.md")))
|
errors: list[str] = []
|
checked = 0
|
markdown_link = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
for board in board_paths:
|
if not board.exists():
|
errors.append(f"missing board: {board.relative_to(ROOT).as_posix()}")
|
continue
|
text = board.read_text(encoding="utf-8")
|
for target in markdown_link.findall(text):
|
if target.startswith(("http://", "https://", "mailto:", "#")):
|
continue
|
path_part = target.split("#", 1)[0].strip()
|
if not path_part:
|
continue
|
checked += 1
|
resolved = (board.parent / path_part).resolve()
|
if not resolved.exists():
|
errors.append(f"{board.relative_to(ROOT).as_posix()} -> {target}")
|
if errors:
|
return False, "; ".join(errors[:20])
|
return True, f"local links reachable in root/case image boards; checked={checked}."
|
|
|
def make_self_check(
|
case_scope_rows: list[dict],
|
lot_rows: list[dict],
|
order_rows: list[dict],
|
summary_stats: dict,
|
db_diagnostics: list[str],
|
) -> tuple[list[dict], dict]:
|
checks: list[dict] = []
|
|
def add(check_id: str, status: bool, detail: str) -> None:
|
checks.append({"check_id": check_id, "status": "PASS" if status else "FAIL", "detail": detail})
|
|
add("CONFIG_FROZEN", (ROOT / "return_stat_config.json").exists(), "return_stat_config.json 已生成。")
|
add("CASE_SCOPE_7_CASES", len(case_scope_rows) == 7, f"case scope 行数={len(case_scope_rows)}。")
|
add("LOT_SCOPE_14_LOTS", len(lot_rows) == 14, f"lot scope 行数={len(lot_rows)}。")
|
add("ORDER_LEDGER_26_EVENTS", len(order_rows) == 26, f"return order ledger 行数={len(order_rows)}。")
|
main_cases = [r for r in case_scope_rows if r["include_in_strict_closed_case_return"] == "True"]
|
add("MAIN_CASES_HAVE_NO_BOUNDARY_LOT", all(int(r["boundary_lot_count"]) == 0 for r in main_cases), f"主口径 case 数={len(main_cases)}。")
|
add("BOUNDARY_LOTS_EXCLUDED", all(r["include_in_strict_closed_lot_recalc"] == "False" for r in lot_rows if r["lot_status"] != "CLOSED_BY_AI_SELL"), "非真实 SELL / 数据缺口 lot 均排除。")
|
add("AUX_LOTS_CLOSED_ONLY", all(r["lot_status"] == "CLOSED_BY_AI_SELL" for r in lot_rows if r["include_in_strict_closed_lot_recalc"] == "True"), "辅助 lot 口径仅包含 CLOSED_BY_AI_SELL。")
|
add("T1_PASS_FOR_INCLUDED_LOTS", all(r["t1_check_status"] == "T1_PASS" for r in lot_rows if r["include_in_strict_closed_lot_recalc"] == "True"), "纳入口径 lot 均满足 T+1。")
|
add("LOOKAHEAD_PASS_FOR_INCLUDED_LOTS", all(r["lookahead_check_status"] == "LOOKAHEAD_PASS" for r in lot_rows if r["include_in_strict_closed_lot_recalc"] == "True"), "纳入口径 lot 无未来函数标记。")
|
add("EVIDENCE_PASS_FOR_INCLUDED_LOTS", all(r["evidence_check_status"] == "EVIDENCE_PASS" for r in lot_rows if r["include_in_strict_closed_lot_recalc"] == "True"), "纳入口径 lot 均有买卖证据路径。")
|
add("INTEGER_LOT_PASS_FOR_INCLUDED_LOTS", all(r["integer_lot_check_status"] == "INTEGER_LOT_PASS" for r in lot_rows if r["include_in_strict_closed_lot_recalc"] == "True"), "纳入口径 lot 均可按整数手成交。")
|
add("TRADEABILITY_PASS_FOR_INCLUDED_LOTS", all(r["tradeability_check_status"] == "TRADEABILITY_PASS" for r in lot_rows if r["include_in_strict_closed_lot_recalc"] == "True"), "纳入口径 lot 的买卖订单均通过 1 分钟 / 涨跌停可成交检查。")
|
add("DB_DIAGNOSTICS_EMPTY", not db_diagnostics, "; ".join(db_diagnostics) if db_diagnostics else "源库连接和查询未记录错误。")
|
add("RETURN_STAT_READY_FALSE", True, "本 run 只生成 RETURN_STAT_READY_CANDIDATE;执行审核通过前 return_stat_ready=false。")
|
add("STRICT_CASE_SUMMARY_LAYERED", summary_stats["strict_closed_case_count"] < 7, "主口径与边界表分层保留,未把 7 个 case 全部强行纳入主收益。")
|
add("AUX_LOT_COUNT_EXPECTED", summary_stats["aux_closed_lot_count"] == 12, f"辅助闭合 lot 数={summary_stats['aux_closed_lot_count']}。")
|
limit_rate_pass, limit_rate_detail = check_limit_rate_policy(order_rows)
|
add("LIMIT_RATE_POLICY_MATCHES_ORDER_LEDGER", limit_rate_pass, limit_rate_detail)
|
board_links_pass, board_links_detail = check_image_board_links()
|
add("IMAGE_BOARD_LOCAL_LINKS_REACHABLE", board_links_pass, board_links_detail)
|
failed = [c for c in checks if c["status"] != "PASS"]
|
self_check = {
|
"schema_version": "1.0",
|
"run_id": RUN_ID,
|
"generated_at": now_iso(),
|
"status": "PASS_FOR_RETURN_STAT_READY_CANDIDATE_EXEC_REVIEW_REQUIRED" if not failed else "FAIL_HELD_FOR_REPAIR",
|
"check_count": len(checks),
|
"fail_count": len(failed),
|
"return_stat_ready": False,
|
"execution_review_required": True,
|
}
|
return checks, self_check
|
|
|
def write_boards(summary_stats: dict, case_scope_rows: list[dict], lot_rows: list[dict]) -> None:
|
main_case_rows = [r for r in case_scope_rows if r["include_in_strict_closed_case_return"] == "True"]
|
boundary_case_rows = [r for r in case_scope_rows if r["include_in_strict_closed_case_return"] != "True"]
|
lines = [
|
"# 无忌 RETURN_STAT_READY 准备 run 图片审核入口",
|
"",
|
"本入口只用于小样本收益统计准备执行审核,不是完整 2023-2026 收益结论。",
|
"",
|
"## 当前状态",
|
"",
|
"| 项目 | 读数 |",
|
"|---|---:|",
|
f"| 已审核来源 case | {len(case_scope_rows)} |",
|
f"| 主口径候选 case | {len(main_case_rows)} |",
|
f"| 边界 / 排除 case | {len(boundary_case_rows)} |",
|
f"| 辅助闭合 lot | {summary_stats['aux_closed_lot_count']} |",
|
f"| 边界 lot | {summary_stats['boundary_lot_count']} |",
|
"",
|
"## 主口径候选 case",
|
"",
|
"| case_id | 纳入状态 | 源图片入口 |",
|
"|---|---|---|",
|
]
|
for row in main_case_rows:
|
lines.append(f"| `{row['case_id']}` | `STRICT_CLOSED_CASE_RETURN_READY_CANDIDATE` | [{row['case_id']}]({row['source_image_board_path']}) |")
|
lines.extend(["", "## 边界 / 排除 case", "", "| case_id | 原因 | 源图片入口 |", "|---|---|---|"])
|
for row in boundary_case_rows:
|
lines.append(f"| `{row['case_id']}` | {row['exclude_or_boundary_reason']} | [{row['case_id']}]({row['source_image_board_path']}) |")
|
lines.extend(
|
[
|
"",
|
"## lot 边界",
|
"",
|
"| lot | case | symbol | 状态 | 处理 |",
|
"|---|---|---|---|---|",
|
]
|
)
|
for row in lot_rows:
|
if row["scope_status"] == "RETURN_STAT_HELD_BOUNDARY_TABLE":
|
lines.append(
|
f"| `{row['trade_lot_id']}` | `{row['case_id']}` | `{row['symbol']}` | `{row['lot_status']}` | 不进入主收益 / 成功率 / 胜率 |"
|
)
|
lines.extend(
|
[
|
"",
|
"## 审核提醒",
|
"",
|
"1. 人工先看本入口,再进入源 case 图片板核对买卖点。",
|
"2. `WINDOW_END_VALUATION_ONLY` 和 `EXIT_DATA_GAP_HELD` 未被强行转成 SELL。",
|
"3. 执行审核通过前不得引用完整 baseline 收益率、成功率、胜率或回撤。",
|
]
|
)
|
(ROOT / "case_image_board.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
story = [
|
"# 无忌 RETURN_STAT_READY 准备 run 文字追溯入口",
|
"",
|
f"- source_run_id: `{SOURCE_RUN_ID}`",
|
f"- design_id: `{DESIGN_ID}`",
|
f"- design_audit_id: `{DESIGN_AUDIT_ID}`",
|
"- 结论边界:本 run 只形成小样本 `RETURN_STAT_READY_CANDIDATE`,需执行审核。",
|
"",
|
"## 主要产物",
|
"",
|
"- `return_stat_config.md/json`:费用、滑点、整数手、涨跌停口径冻结。",
|
"- `return_stat_case_scope.csv`:7 个 case 的纳入 / 排除原因。",
|
"- `return_stat_lot_scope.csv`:14 个 lot 的纳入 / 排除和成本检查。",
|
"- `return_stat_order_ledger.csv`:叠加成本、滑点、整数手和可成交检查的订单账本。",
|
"- `return_stat_case_summary.csv`:case 级 gross / net / 边界状态。",
|
"- `return_stat_boundary_table.csv`:不进入主收益口径的边界样本。",
|
"- `return_stat_self_check.*`:执行自检。",
|
]
|
(ROOT / "case_story_board.md").write_text("\n".join(story) + "\n", encoding="utf-8")
|
|
lot_by_case: dict[str, list[dict]] = defaultdict(list)
|
for row in lot_rows:
|
lot_by_case[row["case_id"]].append(row)
|
for row in case_scope_rows:
|
case_id = row["case_id"]
|
case_dir = ROOT / "cases" / case_id
|
case_dir.mkdir(parents=True, exist_ok=True)
|
source_board = f"../../../{SOURCE_RUN_ID}/cases/{case_id}/case_image_board.md"
|
case_lines = [
|
f"# {case_id} RETURN_STAT 图片审核入口",
|
"",
|
"本页是收益统计准备 run 的 case 级入口,图片证据沿用已审核源结果包。",
|
"",
|
"| 项目 | 内容 |",
|
"|---|---|",
|
f"| case_id | `{case_id}` |",
|
f"| 主收益口径 | `{row['include_in_strict_closed_case_return']}` |",
|
f"| 辅助 lot 复算 | `{row['include_in_aux_lot_recalc']}` |",
|
f"| scope_status | `{row['scope_status']}` |",
|
f"| 纳入 / 排除原因 | {row['exclude_or_boundary_reason']} |",
|
f"| 源图片板 | [{case_id}]({source_board}) |",
|
"",
|
"## lot 处理",
|
"",
|
"| lot | symbol | 状态 | 主口径 | 辅助口径 | 买入图 | 卖出图 |",
|
"|---|---|---|---|---|---|---|",
|
]
|
for lot in lot_by_case.get(case_id, []):
|
buy_img = lot["buy_evidence_image_path"]
|
sell_img = lot["sell_evidence_image_path"]
|
buy_link = f"[买入图](../../../{SOURCE_RUN_ID}/{buy_img})" if buy_img else ""
|
sell_link = f"[卖出图](../../../{SOURCE_RUN_ID}/{sell_img})" if sell_img else ""
|
case_lines.append(
|
f"| `{lot['trade_lot_id']}` | `{lot['symbol']}` | `{lot['lot_status']}` | `{lot['include_in_strict_closed_case_return']}` | `{lot['include_in_strict_closed_lot_recalc']}` | {buy_link} | {sell_link} |"
|
)
|
if not lot_by_case.get(case_id):
|
case_lines.append("| 无交易 lot | | `NO_BUY_MARKET_GATE_CLOSED_OR_NO_ENTRY_SIGNAL` | `False` | `False` | | |")
|
case_lines.extend(
|
[
|
"",
|
"## 审核说明",
|
"",
|
"1. 本页不复制源图片,统一链接到已审核源结果包,避免生成重复图片和口径漂移。",
|
"2. `WINDOW_END_VALUATION_ONLY`、`EXIT_DATA_GAP_HELD` 或无买入 case 不进入主收益 / 成功率口径。",
|
"3. 执行审核通过前,本页所有收益字段均为候选材料,不是正式 baseline 结论。",
|
]
|
)
|
(case_dir / "case_image_board.md").write_text("\n".join(case_lines) + "\n", encoding="utf-8")
|
case_story = [
|
f"# {case_id} RETURN_STAT 文字追溯",
|
"",
|
f"- source case image board: `ana-data/result/{SOURCE_RUN_ID}/cases/{case_id}/case_image_board.md`",
|
f"- scope_status: `{row['scope_status']}`",
|
f"- boundary_reason: {row['exclude_or_boundary_reason']}",
|
"- 文字页只用于追溯;人工审核第一入口仍是 case_image_board.md。",
|
]
|
(case_dir / "case_story_board.md").write_text("\n".join(case_story) + "\n", encoding="utf-8")
|
|
|
def write_config_docs() -> None:
|
CONFIG["config_frozen_at"] = now_iso()
|
write_json(ROOT / "return_stat_config.json", CONFIG)
|
md = [
|
"# RETURN_STAT 准备 run 配置冻结",
|
"",
|
f"- run_id: `{RUN_ID}`",
|
f"- source_run_id: `{SOURCE_RUN_ID}`",
|
f"- design_audit_id: `{DESIGN_AUDIT_ID}`",
|
"- 状态:`RETURN_STAT_READY_CANDIDATE_EXECUTION_REVIEW_REQUIRED`",
|
"- `return_stat_ready`: false",
|
"",
|
"## 费用和滑点",
|
"",
|
"| 项目 | 冻结值 | 来源 / 说明 |",
|
"|---|---:|---|",
|
"| 初始资金 | 1,000,000 CNY | 本 run 主资金规模;另输出 100,000 / 10,000,000 敏感性配置 |",
|
"| 买入佣金 | 0.00025 | 沿用源 `run_config` 的 `commission_rate_each_side` |",
|
"| 卖出佣金 | 0.00025 | 沿用源 `run_config` 的 `commission_rate_each_side` |",
|
"| 卖出印花税:2023-08-27 及以前 | 0.001 | 本 run 冻结历史卖出侧口径;扩样前需审核员复核官方依据 |",
|
"| 卖出印花税:2023-08-28 起 | 0.0005 | 本 run 冻结历史卖出侧口径;扩样前需审核员复核官方依据 |",
|
"| 过户费 | 买卖各 0.00001 | 本 run 冻结敏感性口径;扩样前需审核员复核官方依据 |",
|
"| 滑点 | 买卖各 5 bps | 内部保守敏感性设置,不是笔记原文规则 |",
|
"",
|
"## 整数手和资金残余",
|
"",
|
"每笔按 `position_delta_pct * initial_cash_cny` 得到预算,向下取整到 100 股;未使用预算留在现金中。若不足 100 股,标记 `INTEGER_LOT_NOT_EXECUTABLE_HELD`。",
|
"",
|
"## 涨跌停不可成交",
|
"",
|
"使用本地 MySQL `a_share_minute_price` 和 `a_share_daily_price` 检查订单决策分钟、前收、涨跌停估算价格和成交量。涨停买入锁死、跌停卖出锁死、分钟数据缺失或价格不在 bar 范围内均不得进入主收益口径。",
|
"",
|
"## 结论边界",
|
"",
|
"本配置只支持当前 7 个代表性案例的小样本收益统计准备执行审核。执行审核通过前不得转写完整 baseline 收益率、成功率、胜率或回撤。",
|
]
|
(ROOT / "return_stat_config.md").write_text("\n".join(md) + "\n", encoding="utf-8")
|
|
|
def write_summary(summary_stats: dict) -> None:
|
data = {
|
"schema_version": "1.0",
|
"run_id": RUN_ID,
|
"case_matter_id": CASE_MATTER_ID,
|
"design_id": DESIGN_ID,
|
"design_audit_id": DESIGN_AUDIT_ID,
|
"source_run_id": SOURCE_RUN_ID,
|
"source_audit_ids": SOURCE_AUDIT_IDS,
|
"generated_at": now_iso(),
|
"stage": "RETURN_STAT_READY_CANDIDATE_EXECUTION_REVIEW_REQUIRED",
|
"return_stat_ready": False,
|
"execution_review_required": True,
|
"scope": "7 representative cases only; no full 2023-2026 conclusion.",
|
"summary_stats": summary_stats,
|
"conclusion_boundary": "Small-sample return-stat preparation only. Do not cite as complete baseline return, success rate, win rate, drawdown, or strategy effectiveness before execution review passes.",
|
}
|
write_json(ROOT / "return_stat_summary.json", data)
|
success_rate = summary_stats["strict_closed_case_success_rate"]
|
aux_win_rate = summary_stats["aux_closed_lot_win_rate"]
|
md = [
|
"# RETURN_STAT 准备 run 摘要",
|
"",
|
f"- run_id: `{RUN_ID}`",
|
f"- source_run_id: `{SOURCE_RUN_ID}`",
|
"- 状态:`RETURN_STAT_READY_CANDIDATE_EXECUTION_REVIEW_REQUIRED`",
|
"- `return_stat_ready`: false",
|
"",
|
"## 小样本准备读数",
|
"",
|
"| 指标 | 数值 |",
|
"|---|---:|",
|
f"| 主口径候选 case 数 | {summary_stats['strict_closed_case_count']} |",
|
f"| 主口径候选 case 成功数 | {summary_stats['strict_closed_case_success_count']} |",
|
f"| 主口径候选 case 成功率候选值 | {success_rate:.6f} |",
|
f"| 主口径候选 case net 贡献合计 | {summary_stats['strict_closed_case_net_account_contribution_sum']:.8f} |",
|
f"| 主口径候选 case 平均 net 贡献 | {summary_stats['strict_closed_case_average_net_account_contribution']:.8f} |",
|
f"| 事件级最大回撤候选值 | {summary_stats['strict_closed_case_max_drawdown_event_based']:.8f} |",
|
f"| 辅助闭合 lot 数 | {summary_stats['aux_closed_lot_count']} |",
|
f"| 辅助闭合 lot 胜率候选值 | {aux_win_rate:.6f} |",
|
f"| 边界 lot 数 | {summary_stats['boundary_lot_count']} |",
|
"",
|
"## 使用限制",
|
"",
|
"这些读数只是执行审核材料中的候选读数。审核通过前不得引用为完整 baseline 收益率、成功率、胜率或回撤;即使审核通过,也只代表当前 7 个小样本的严格闭合子集。",
|
]
|
(ROOT / "return_stat_summary.md").write_text("\n".join(md) + "\n", encoding="utf-8")
|
|
|
def write_self_check_docs(check_rows: list[dict], self_check: dict) -> None:
|
write_csv(ROOT / "return_stat_self_check_items.csv", check_rows, ["check_id", "status", "detail"])
|
write_json(ROOT / "return_stat_self_check.json", self_check)
|
lines = [
|
"# RETURN_STAT 准备 run 自检",
|
"",
|
f"- status: `{self_check['status']}`",
|
f"- check_count: {self_check['check_count']}",
|
f"- fail_count: {self_check['fail_count']}",
|
"- return_stat_ready: false",
|
"",
|
"| check_id | status | detail |",
|
"|---|---|---|",
|
]
|
for row in check_rows:
|
lines.append(f"| `{row['check_id']}` | `{row['status']}` | {row['detail']} |")
|
(ROOT / "return_stat_self_check.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
def write_manifest() -> None:
|
files = []
|
for path in sorted(ROOT.rglob("*")):
|
if not path.is_file():
|
continue
|
rel = path.relative_to(ROOT).as_posix()
|
if rel == "manifest.json":
|
continue
|
files.append({"path": rel, "exists": True, "size": path.stat().st_size, "sha256": sha256_file(path)})
|
manifest = {
|
"schema_version": "1.0",
|
"run_id": RUN_ID,
|
"source_run_id": SOURCE_RUN_ID,
|
"manifest_stage": "RETURN_STAT_READY_CANDIDATE_PACKAGE_DONE",
|
"generated_at": now_iso(),
|
"hash_status": "size_and_sha256_recorded_for_current_artifacts_manifest_self_excluded",
|
"overall_status": "PASS_FOR_RETURN_STAT_READY_CANDIDATE_EXEC_REVIEW_REQUIRED",
|
"return_stat_ready": False,
|
"execution_review_required": True,
|
"file_count": len(files),
|
"files": files,
|
}
|
write_json(ROOT / "manifest.json", manifest)
|
|
|
def copy_source_tools() -> None:
|
source_tools = SOURCE_ROOT / "tools"
|
target_tools = ROOT / "tools" / "source_reference"
|
target_tools.mkdir(parents=True, exist_ok=True)
|
for name in ["perform_exit_ai_review.py", "run_pilot_self_check.py", "finalize_result_package.py"]:
|
src = source_tools / name
|
if src.exists():
|
shutil.copy2(src, target_tools / name)
|
|
|
def main() -> None:
|
ROOT.mkdir(parents=True, exist_ok=True)
|
(ROOT / "tools").mkdir(exist_ok=True)
|
case_index = read_csv("case_index.csv")
|
orders = read_csv("order_ledger.csv")
|
lots = read_csv("position_lot_ledger.csv")
|
write_config_docs()
|
tradeability, db_diagnostics = fetch_tradeability(orders)
|
order_rows, _ = make_order_rows(orders, lots, tradeability)
|
|
closed_by_case = lots.groupby("case_id").lot_status.apply(lambda s: all(v == "CLOSED_BY_AI_SELL" for v in s)).to_dict()
|
case_main_candidate = {case_id: bool(value) for case_id, value in closed_by_case.items()}
|
lot_rows, _ = make_lot_scope_rows(lots, order_rows, case_main_candidate)
|
case_scope_rows, case_summary_rows, summary_stats = make_case_scope_and_summary(case_index, lots, lot_rows)
|
# Recompute lot main flags after final case scope, so no-buy and boundary cases cannot leak into main.
|
main_case_ids = {r["case_id"] for r in case_scope_rows if r["include_in_strict_closed_case_return"] == "True"}
|
for row in lot_rows:
|
if row["case_id"] not in main_case_ids and row["include_in_strict_closed_case_return"] == "True":
|
row["include_in_strict_closed_case_return"] = "False"
|
if row["include_in_strict_closed_lot_recalc"] == "True":
|
row["scope_status"] = "STRICT_CLOSED_LOT_RECALC_ONLY"
|
row["exclude_or_boundary_reason"] = "CASE_HAS_BOUNDARY_LOT_AUX_ONLY"
|
case_scope_rows, case_summary_rows, summary_stats = make_case_scope_and_summary(case_index, lots, lot_rows)
|
boundary_rows = make_boundary_rows(case_scope_rows, lot_rows)
|
|
write_csv(ROOT / "return_stat_order_ledger.csv", order_rows, list(order_rows[0].keys()))
|
write_csv(ROOT / "return_stat_lot_scope.csv", lot_rows, list(lot_rows[0].keys()))
|
write_csv(ROOT / "return_stat_case_scope.csv", case_scope_rows, list(case_scope_rows[0].keys()))
|
write_csv(ROOT / "return_stat_case_summary.csv", case_summary_rows, list(case_summary_rows[0].keys()))
|
write_csv(ROOT / "return_stat_boundary_table.csv", boundary_rows, list(boundary_rows[0].keys()))
|
|
write_summary(summary_stats)
|
write_boards(summary_stats, case_scope_rows, lot_rows)
|
check_rows, self_check = make_self_check(case_scope_rows, lot_rows, order_rows, summary_stats, db_diagnostics)
|
write_self_check_docs(check_rows, self_check)
|
copy_source_tools()
|
write_manifest()
|
|
|
if __name__ == "__main__":
|
main()
|