Skip to content

Commit 403fcd2

Browse files
authored
target-weight 승격 증거 일관성 검증 강화
target-weight pilot 증거의 날짜와 params hash 일관성을 승격 및 live gate에서 검증합니다.
2 parents 7289562 + 8b02f32 commit 403fcd2

6 files changed

Lines changed: 263 additions & 18 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,4 @@ Target-weight pilot 승인/재시도 보강: `tools/paper_pilot_control.py --ena
231231

232232
Target-weight 실행 차단 기록: `--execute`가 pilot cap validation에서 막히면 주문·체결·증거 수집 없이 session JSON artifact에 차단 사유를 남깁니다. runtime pilot session은 쓰지 않아 cap을 고친 뒤 같은 거래일 계획을 다시 점검할 수 있습니다.
233233

234-
Target-weight 승격 증거 보강: target-weight 계열 전략은 일반 `execution_backed=True` paper record만으로 promotion evidence day를 채우지 않습니다. `pilot_paper`/authorized record가 target-weight plan과 execution proof를 포함하고, liquidity/pre-trade risk/order result/fill/position reconciliation complete 및 plan/execution params hash 일치를 만족한 날만 승격 카운트에 들어갑니다. live gate도 이 proof summary가 없거나 invalid day가 있으면 target-weight live 전환을 차단합니다.
234+
Target-weight 승격 증거 보강: target-weight 계열 전략은 일반 `execution_backed=True` paper record만으로 promotion evidence day를 채우지 않습니다. `pilot_paper`/authorized record가 target-weight plan과 execution proof를 포함하고, record date와 plan trade day가 일치하며, liquidity/pre-trade risk/order result/fill/position reconciliation complete 및 plan/execution params hash 일치를 만족한 날만 승격 카운트에 들어갑니다. 60영업일 전체 verified pilot evidence는 하나의 params hash로 고정되어야 하며, live gate도 canonical metadata의 params hash와 paper evidence params hash가 다르면 target-weight live 전환을 차단합니다.

core/live_gate.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,26 @@ def _is_target_weight_strategy(strategy_name: str) -> bool:
197197
return strategy_name.startswith("target_weight_")
198198

199199

200+
def _canonical_target_weight_params_hash(
201+
metadata: dict[str, Any],
202+
strategy_name: str,
203+
) -> str | None:
204+
specs = metadata.get("strategy_specs")
205+
if not isinstance(specs, list):
206+
return None
207+
for spec in specs:
208+
if not isinstance(spec, dict):
209+
continue
210+
if spec.get("candidate_id") == strategy_name:
211+
params_hash = spec.get("params_hash")
212+
return params_hash if isinstance(params_hash, str) and params_hash else None
213+
return None
214+
215+
200216
def _validate_target_weight_evidence_summary(
201217
strategy_name: str,
202218
evidence: dict[str, Any],
219+
canonical_params_hash: str | None = None,
203220
) -> list[str]:
204221
if not _is_target_weight_strategy(strategy_name):
205222
return []
@@ -214,6 +231,18 @@ def _validate_target_weight_evidence_summary(
214231
invalid_days = _as_int(summary.get("invalid_days")) or 0
215232
if summary.get("all_promotable_days_verified") is not True:
216233
issues.append("target-weight promotable evidence가 모두 검증된 pilot_paper 실행 증거가 아님.")
234+
if summary.get("params_hash_consistent") is not True:
235+
issues.append("target-weight pilot evidence params_hash가 60영업일 전체에서 일관되지 않음.")
236+
evidence_params_hash = summary.get("params_hash") or evidence.get("target_weight_params_hash")
237+
if not isinstance(evidence_params_hash, str) or not evidence_params_hash:
238+
issues.append("target-weight pilot evidence params_hash 누락.")
239+
elif not canonical_params_hash:
240+
issues.append("target-weight canonical params_hash 누락.")
241+
elif evidence_params_hash != canonical_params_hash:
242+
issues.append(
243+
"target-weight canonical params_hash 불일치: "
244+
f"evidence={evidence_params_hash}, canonical={canonical_params_hash}."
245+
)
217246
if valid_days < promotable_days or valid_days < 60:
218247
issues.append(
219248
"target-weight verified pilot_paper evidence 60영업일 미달 "
@@ -434,6 +463,12 @@ def validate_live_readiness(
434463
issues.append("paper evidence win_rate 45% 미달.")
435464
if frozen_days > 0:
436465
issues.append("paper evidence에 frozen day가 존재함.")
437-
issues.extend(_validate_target_weight_evidence_summary(strategy_name, evidence))
466+
issues.extend(
467+
_validate_target_weight_evidence_summary(
468+
strategy_name,
469+
evidence,
470+
canonical_params_hash=_canonical_target_weight_params_hash(metadata, strategy_name),
471+
)
472+
)
438473

439474
return issues

core/paper_evidence.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,10 @@ def _target_weight_record_proof_status(strategy: str, record: dict) -> tuple[boo
11811181
return False, "missing_target_weight_execution"
11821182
if plan.get("candidate_id") != strategy:
11831183
return False, "target_weight_candidate_mismatch"
1184+
plan_trade_day = plan.get("trade_day")
1185+
record_date = record.get("date")
1186+
if plan_trade_day and record_date and plan_trade_day != record_date:
1187+
return False, "target_weight_trade_day_mismatch"
11841188

11851189
plan_hash = plan.get("params_hash")
11861190
execution_hash = execution.get("params_hash")
@@ -1205,6 +1209,17 @@ def _target_weight_record_proof_status(strategy: str, record: dict) -> tuple[boo
12051209
return True, "verified_target_weight_pilot_execution"
12061210

12071211

1212+
def _target_weight_record_params_hash(record: dict) -> str | None:
1213+
caps = record.get("pilot_caps_snapshot") or {}
1214+
plan = caps.get("target_weight_plan") or {}
1215+
execution = caps.get("target_weight_execution") or {}
1216+
plan_hash = plan.get("params_hash")
1217+
execution_hash = execution.get("params_hash")
1218+
if isinstance(plan_hash, str) and plan_hash and plan_hash == execution_hash:
1219+
return plan_hash
1220+
return None
1221+
1222+
12081223
def _split_target_weight_promotion_records(
12091224
strategy: str,
12101225
records: list[dict],
@@ -1252,15 +1267,31 @@ def generate_promotion_package(strategy: str) -> tuple[Path | None, Path | None]
12521267
target_weight_valid_records: list[dict] = []
12531268
target_weight_invalid_records: list[dict] = []
12541269
target_weight_invalid_reasons: dict[str, int] = {}
1270+
target_weight_params_hashes: list[str] = []
12551271
if target_weight_required:
12561272
(
12571273
target_weight_valid_records,
12581274
target_weight_invalid_records,
12591275
target_weight_invalid_reasons,
12601276
) = _split_target_weight_promotion_records(strategy, execution_records)
1277+
target_weight_params_hashes = sorted({
1278+
params_hash
1279+
for record in target_weight_valid_records
1280+
if (params_hash := _target_weight_record_params_hash(record))
1281+
})
12611282
records = target_weight_valid_records
12621283
else:
12631284
records = execution_records
1285+
target_weight_params_hash = (
1286+
target_weight_params_hashes[0] if len(target_weight_params_hashes) == 1 else None
1287+
)
1288+
target_weight_params_hash_consistent = (
1289+
not target_weight_required
1290+
or (
1291+
len(target_weight_valid_records) > 0
1292+
and len(target_weight_params_hashes) == 1
1293+
)
1294+
)
12641295
total_days = len(records)
12651296
promotable_days = total_days
12661297
shadow_days = len(shadow_records)
@@ -1331,6 +1362,11 @@ def generate_promotion_package(strategy: str) -> tuple[Path | None, Path | None]
13311362
block_reasons.append(
13321363
"target_weight_invalid_execution_evidence=%d" % len(target_weight_invalid_records)
13331364
)
1365+
if target_weight_required and target_weight_valid_records and not target_weight_params_hash_consistent:
1366+
blocked = True
1367+
block_reasons.append(
1368+
"target_weight_params_hash_drift=%d" % len(target_weight_params_hashes)
1369+
)
13341370
if frozen_days > 0:
13351371
blocked = True
13361372
block_reasons.append("frozen_days=%d" % frozen_days)
@@ -1427,15 +1463,20 @@ def generate_promotion_package(strategy: str) -> tuple[Path | None, Path | None]
14271463
"valid_pilot_days": len(target_weight_valid_records),
14281464
"invalid_days": len(target_weight_invalid_records),
14291465
"invalid_reasons": target_weight_invalid_reasons,
1466+
"params_hash": target_weight_params_hash,
1467+
"params_hashes": target_weight_params_hashes,
1468+
"params_hash_consistent": target_weight_params_hash_consistent,
14301469
"all_promotable_days_verified": (
14311470
not target_weight_required
14321471
or (
14331472
len(target_weight_invalid_records) == 0
14341473
and len(target_weight_valid_records) == promotable_days
14351474
and promotable_days > 0
1475+
and target_weight_params_hash_consistent
14361476
)
14371477
),
14381478
},
1479+
"target_weight_params_hash": target_weight_params_hash,
14391480
"target_weight_verified_pilot_days": len(target_weight_valid_records),
14401481
"target_weight_invalid_days": len(target_weight_invalid_records),
14411482
# backward compat aliases

docs/PROJECT_GUIDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,7 @@ full paper 신규 BUY는 preflight status artifact와 runtime state가 모두
718718
|**target-weight pilot enable guard 추가** | `tools/paper_pilot_control.py --enable`이 target-weight 후보를 승인할 때 pilot auth 기록 전에 readiness audit을 재실행하고, 운영자가 요청한 cap이 현재 plan/launch readiness/유동성 preflight/비용 반영 pre-trade risk를 만족할 때만 승인한다. stale plan이나 추천 cap 미충족 상태에서 auth만 먼저 열리는 경로를 차단한다 |
719719
|**target-weight cap validation artifact 추가** | `--execute`가 pilot cap validation에서 막혀도 예외로 조기 종료하지 않고 session JSON artifact에 차단 사유, skipped orders, evidence block reason을 남긴다. 주문/체결/증거 수집/runtime pilot session 저장은 하지 않아 cap 조정 후 재점검할 수 있다 |
720720
|**target-weight promotion proof guard 추가** | promotion package와 live gate가 target-weight 계열 전략의 promotable day를 일반 `execution_backed=True` record로 인정하지 않는다. `pilot_paper`/authorized record가 target-weight plan/execution proof를 포함하고 liquidity/pre-trade/order/fill/position complete와 plan/execution params hash 일치를 만족한 날만 승격 카운트에 포함한다 |
721+
|**target-weight proof consistency guard 추가** | target-weight promotion evidence는 record date와 plan trade day가 일치해야 하며, 60영업일 verified pilot evidence 전체가 하나의 params hash로 고정되어야 한다. live gate는 paper evidence의 target-weight params hash와 canonical `strategy_specs.params_hash`가 다르면 실전 전환을 차단한다 |
721722
|**target-weight liquidity preflight 추가** | `core.target_weight_rotation.build_target_weight_plan()`이 주문 종목별 최근 20일 평균 거래대금을 plan diagnostics에 기록하고, `tools/target_weight_rotation_pilot.py`가 주문별 notional이 평균 거래대금의 기본 5%(`--max-order-adv-pct`)를 초과하거나 유동성 diagnostics가 누락되면 readiness audit과 `--execute`를 fail-closed 차단한다. 차단 결과는 session artifact, readiness JSON/MD, pilot session의 `target_weight_execution.liquidity_check`에 남기며 기존 pilot evidence 재사용도 liquidity-complete 조건을 요구한다 |
722723
|**target-weight pre-trade risk 추가** | `RiskManager.calculate_transaction_costs()`의 수수료/세금/동적 슬리피지 예상 체결가를 재사용해 plan 전체 주문을 제출 전 시뮬레이션한다. 예상 현금 부족, 최소 현금비중, 최대 투자비중, 종목별 최대 비중, 최대 보유 종목 수 위반은 readiness audit과 `--execute`에서 fail-closed 차단하고, session/readiness/pilot evidence snapshot에 `pre_trade_risk_check`와 cost summary를 남긴다. 매도 주문은 plan diagnostics의 기존 평균매입가를 사용해 세금/양도세 옵션과도 연결된다 |
723724
|**target-weight completed rerun block 추가** | same-candidate/trade-day pilot session artifact가 이미 `execution_complete=True`이고 실제 주문 실행 수량이 있으면 `--allow-rerun`을 줘도 재실행을 차단한다. `--allow-rerun`은 부분 실행/중단 세션 복구에만 사용해 완료된 실행 증거가 중복 주문으로 오염되지 않게 한다 |

tests/test_live_gate.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,29 @@ def _write_bundle(
5757
evaluation_errors=None,
5858
walk_forward_errors=None,
5959
metric_overrides=None,
60+
strategy_specs=None,
6061
):
6162
allowed_modes = ["backtest", "paper", "live"] if allowed_modes is None else allowed_modes
6263
generated_at = generated_at or datetime(2026, 4, 29, 12, 0, 0).isoformat()
6364
snapshot_manifest = snapshot_manifest or _snapshot_manifest()
6465

65-
_write_json(
66-
promotion_dir / "run_metadata.json",
67-
{
68-
"schema_version": LIVE_GATE_SCHEMA_VERSION,
69-
"artifact_type": LIVE_GATE_ARTIFACT_TYPE,
70-
"commit_hash": commit_hash,
71-
"config_yaml_hash": yaml_hash,
72-
"config_resolved_hash": resolved_hash,
73-
"generated_at": generated_at,
74-
"data_snapshot_hash": snapshot_manifest["data_snapshot_hash"],
75-
"data_snapshot_manifest": snapshot_manifest,
76-
"evaluation_errors": evaluation_errors or {},
77-
"walk_forward_errors": walk_forward_errors or {},
78-
},
79-
)
66+
metadata = {
67+
"schema_version": LIVE_GATE_SCHEMA_VERSION,
68+
"artifact_type": LIVE_GATE_ARTIFACT_TYPE,
69+
"commit_hash": commit_hash,
70+
"config_yaml_hash": yaml_hash,
71+
"config_resolved_hash": resolved_hash,
72+
"generated_at": generated_at,
73+
"data_snapshot_hash": snapshot_manifest["data_snapshot_hash"],
74+
"data_snapshot_manifest": snapshot_manifest,
75+
"evaluation_errors": evaluation_errors or {},
76+
"walk_forward_errors": walk_forward_errors or {},
77+
}
78+
if strategy_specs is not None:
79+
metadata["strategy_specs"] = strategy_specs
80+
elif strategy.startswith("target_weight_"):
81+
metadata["strategy_specs"] = [{"candidate_id": strategy, "params_hash": "hash"}]
82+
_write_json(promotion_dir / "run_metadata.json", metadata)
8083
metrics = {"total_return": 12.0, "sharpe": 0.6, "profit_factor": 1.3}
8184
metrics.update(metric_overrides or {})
8285
_write_json(
@@ -404,10 +407,14 @@ def test_target_weight_live_gate_accepts_verified_pilot_evidence(tmp_path):
404407
"valid_pilot_days": 60,
405408
"invalid_days": 0,
406409
"invalid_reasons": {},
410+
"params_hash": "hash",
411+
"params_hashes": ["hash"],
412+
"params_hash_consistent": True,
407413
"all_promotable_days_verified": True,
408414
},
409415
target_weight_verified_pilot_days=60,
410416
target_weight_invalid_days=0,
417+
target_weight_params_hash="hash",
411418
)
412419

413420
issues = validate_live_readiness(
@@ -420,3 +427,42 @@ def test_target_weight_live_gate_accepts_verified_pilot_evidence(tmp_path):
420427
)
421428

422429
assert issues == []
430+
431+
432+
def test_target_weight_live_gate_blocks_params_hash_mismatch(tmp_path):
433+
strategy = "target_weight_rotation_test"
434+
promotion_dir = tmp_path / "reports" / "promotion"
435+
evidence_dir = tmp_path / "reports" / "paper_evidence"
436+
_write_bundle(
437+
promotion_dir,
438+
strategy=strategy,
439+
strategy_specs=[{"candidate_id": strategy, "params_hash": "canonical-hash"}],
440+
)
441+
_write_evidence(
442+
evidence_dir,
443+
strategy=strategy,
444+
target_weight_evidence={
445+
"required": True,
446+
"valid_pilot_days": 60,
447+
"invalid_days": 0,
448+
"invalid_reasons": {},
449+
"params_hash": "evidence-hash",
450+
"params_hashes": ["evidence-hash"],
451+
"params_hash_consistent": True,
452+
"all_promotable_days_verified": True,
453+
},
454+
target_weight_verified_pilot_days=60,
455+
target_weight_invalid_days=0,
456+
target_weight_params_hash="evidence-hash",
457+
)
458+
459+
issues = validate_live_readiness(
460+
DummyConfig(),
461+
strategy,
462+
promotion_dir=promotion_dir,
463+
evidence_dir=evidence_dir,
464+
current_git_hash="abc123",
465+
now=datetime(2026, 4, 29, 12, 0, 0),
466+
)
467+
468+
assert any("target-weight canonical params_hash 불일치" in issue for issue in issues)

0 commit comments

Comments
 (0)