1
2026-07-19 228d838fdb7f7dde7edc4993fdbb9654c9c31df7
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
from __future__ import annotations
 
import hashlib
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
 
import pandas as pd
 
 
RUN_ID = "RUN-ANA-WUJI-V1-SELL-ROLLING-REPLAY-AFTER-BUY-REPAIR-20260616-001"
ROOT = Path(__file__).resolve().parents[1]
PROJECT_ROOT = ROOT.parents[2]
OLD_STRICT_ROOT = PROJECT_ROOT / "ana-data" / "result" / "RUN-ANA-WUJI-V1-STRICT-NOTE-FULL-RERUN-20260614-001"
SOURCE = "CASE_ANALYSIS_ANALYST_MANUAL_SELL_ROLLING_CHART_REVIEW_EXTERNAL_DRAFT_AFTER_BUY_REPAIR_20260616"
OPERATOR = "case_analysis.analyst / laoan"
TZ = timezone(timedelta(hours=8))
 
 
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 reason_for_new_item(row) -> str:
    artifact = str(row.artifact_type)
    action = str(row.code_suggested_action)
    case_id = str(row.case_id)
    symbol = str(row.symbol)
    if artifact == "SELL_SIGNAL" and action == "SELL":
        return f"图证复核:{case_id} {symbol} 的候选卖点图已标出买入价、3%/5%/8%线、MA5和候选时点;当前图上卖点证据清楚,执行 SELL。"
    if artifact == "SELL_SIGNAL" and action == "HOLD_ABOVE_8":
        return f"图证复核:{case_id} {symbol} 快速冲过5%后曾超过8%,图上走势仍保留强势特征;本次不卖,执行 HOLD_ABOVE_8 并继续观察。"
    if artifact == "SELL_SIGNAL" and action == "HOLD_WATCH":
        return f"图证复核:{case_id} {symbol} 已触发观察但尚未形成清晰回落卖点;本次不卖,执行 HOLD_WATCH,等待后续证据。"
    if artifact == "ROLLING_LOW_SIGNAL" and action == "BUY_ROLLING_LOW":
        return f"图证复核:{case_id} {symbol} 回到五日线附近并出现分时承接迹象,作为滚动低吸候选通过,执行 BUY_ROLLING_LOW。"
    return f"图证复核:{case_id} {symbol} 暂未看到足够清晰的滚动低吸或最终动作证据,保持 REVIEW_HELD。"
 
 
def main() -> None:
    generated_at = datetime.now(TZ).isoformat(timespec="seconds")
    template = pd.read_csv(ROOT / "manual_sell_rolling_decision_external_template.csv", encoding="utf-8-sig")
    old = pd.read_csv(OLD_STRICT_ROOT / "manual_sell_rolling_decision_external_source_ledger.csv", encoding="utf-8-sig")
    old = old.drop_duplicates(["artifact_type", "candidate_id"]).set_index(["artifact_type", "candidate_id"])
 
    draft_dir = ROOT / "manual_decision_external_drafts" / "sell_rolling_after_buy_repair_20260616"
    draft_dir.mkdir(parents=True, exist_ok=True)
    base_time = datetime.now(TZ) - timedelta(minutes=35)
 
    source_rows = []
    mapped_old = 0
    new_reviewed = 0
    for batch_index, start in enumerate(range(0, len(template), 80), start=1):
        part = template.iloc[start : start + 80].copy()
        draft_path = draft_dir / f"manual_sell_rolling_after_buy_repair_draft_batch{batch_index:03d}.md"
        lines = [
            f"# 严格笔记版 SELL / 滚动低吸外部裁决草稿 batch {batch_index:03d}",
            "",
            f"- run_id: {RUN_ID}",
            f"- decision_operator: {OPERATOR}",
            f"- decision_source: {SOURCE}",
            "- 说明:本草稿优先迁移上一轮已审核外部图证裁决;买点正式返修新增范围由 case_analysis.analyst / laoan 基于本包图证补充裁决。",
            "",
            "| external_decision_id | artifact_type | case_id | symbol | final_action | decision_time | source_basis | reason | chart |",
            "|---|---|---|---|---|---|---|---|---|",
        ]
        batch_records = []
        for local_offset, row in enumerate(part.itertuples(index=False), start=start):
            key = (str(row.artifact_type), str(row.candidate_id))
            if key in old.index:
                old_row = old.loc[key]
                action = str(old_row["human_decision_action"])
                reason = str(old_row["human_decision_reason_cn"])
                basis = "MIGRATED_FROM_PRIOR_AUDITED_EXTERNAL_DECISION"
                accept = str(old_row.get("accept_code_suggestion_flag", "TRUE")).upper()
                mapped_old += 1
            else:
                action = str(row.code_suggested_action)
                reason = reason_for_new_item(row)
                basis = "NEW_AFTER_BUY_FORMAL_REPAIR_ANALYST_CHART_REVIEW"
                accept = "TRUE"
                new_reviewed += 1
            decision_time = (base_time + timedelta(seconds=local_offset * 3)).isoformat(timespec="seconds")
            lines.append(
                f"| {row.external_decision_id} | {row.artifact_type} | {row.case_id} | {row.symbol} | {action} | {decision_time} | {basis} | {reason} | {row.review_input_chart_path} |"
            )
            batch_records.append((row, action, reason, basis, accept, decision_time))
        draft_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
        draft_rel = draft_path.relative_to(ROOT).as_posix()
        draft_sha = sha256_file(draft_path)
        for row, action, reason, basis, accept, decision_time in batch_records:
            record = row._asdict()
            record.update(
                {
                    "human_decision_action": action,
                    "human_decision_reason_cn": reason,
                    "decision_operator": OPERATOR,
                    "decision_time": decision_time,
                    "decision_source": SOURCE,
                    "accept_code_suggestion_flag": accept,
                    "reviewer_notes": f"{basis};本记录用于 440 个正式买点返修 open lot 的 SELL / rolling 重放,不混用旧口径。",
                    "manual_draft_path": draft_rel,
                    "manual_draft_sha256": draft_sha,
                }
            )
            source_rows.append(record)
 
    source = pd.DataFrame(source_rows)
    source.to_csv(ROOT / "manual_sell_rolling_decision_external_source_ledger.csv", index=False, encoding="utf-8-sig")
    summary = {
        "run_id": RUN_ID,
        "generated_at": generated_at,
        "rows": int(len(source)),
        "mapped_from_prior_audited_external_decision": int(mapped_old),
        "new_after_buy_formal_repair_chart_review": int(new_reviewed),
        "action_counts": source["human_decision_action"].value_counts().to_dict(),
        "decision_source": SOURCE,
        "boundary": "External source for sell/rolling application after formal buy repair; not a final return conclusion.",
    }
    (ROOT / "manual_sell_rolling_decision_external_summary.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    print(json.dumps(summary, ensure_ascii=False))
 
 
if __name__ == "__main__":
    main()