1
2026-06-10 80ff13305c5e5f2ace09bd575188a339fa84c3c3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
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()