ee3e799de1
주요 변경: - tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규 * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합 * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일) - src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규 * Logger.log / getSpreadsheet_() 로 run_all 연동 수정 - src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs * _mergePositionRecord_(): 소수주 중복 행 합산 신규 * parseInt → parseFloat (qty, availQty) - src/gas_adapter_parts/gdf_01_price_metrics.gs * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL - spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63) - spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
315 lines
13 KiB
Python
315 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
TESTS_PATH = ROOT / "tests" / "strategy_tests.yaml"
|
|
REQUIRED_CASES = {
|
|
"T023_OVERSOLD_REBOUND_50_50_SPLIT": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:K2_STAGED_REBOUND_SELL_V1",
|
|
"expected_keys": ["immediate_sell_qty", "rebound_wait_qty", "k2_emergency"],
|
|
},
|
|
"T024_OVERSOLD_REBOUND_EMERGENCY_FULL_EXIT": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:K2_STAGED_REBOUND_SELL_V1",
|
|
"expected_keys": ["immediate_sell_qty", "rebound_wait_qty", "k2_emergency"],
|
|
},
|
|
"T025_PROFIT_PROTECT_TRIM_LIMIT_POLICY": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:LIMIT_PRICE_POLICY_V1",
|
|
"expected_keys": ["sell_limit_price"],
|
|
},
|
|
"T026_WAIT_PILOT_SETUP_NO_POSITION": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:STAGED_ENTRY_TRANCHE_V1",
|
|
"expected_keys": ["tranche_phase", "current_tranche_allowed_pct", "next_tranche_condition"],
|
|
},
|
|
"T027_PA1_EXIT_SIGNAL_BLOCKS_BUY_PERMISSION": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:PREDICTIVE_ALPHA_ENGINE_V1",
|
|
"expected_keys": ["pa1_synthesis_verdict", "buy_permission_state"],
|
|
},
|
|
"T028_PA2_BLOCKED_LATE_ENTRY_FORCES_BLOCK": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:ANTI_LATE_ENTRY_GATE_V2",
|
|
"expected_keys": ["anti_late_entry_grade", "buy_permission_state"],
|
|
},
|
|
"T029_PA3_PORTFOLIO_ALPHA_CONFIDENCE_NEGATIVE_BLOCK": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:CASH_PRESERVATION_SELL_ENGINE_V2",
|
|
"expected_keys": ["buy_permission_state", "portfolio_alpha_confidence"],
|
|
},
|
|
"T030_PA5_CONSISTENCY_VALIDATOR_PASSES": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:CONSISTENCY_VALIDATOR_V2",
|
|
"expected_keys": ["consistency_score_min", "cv_verdict"],
|
|
},
|
|
"T031_WATCHLIST_ONLY_DOWNGRADES_PILOT": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:BUY_PERMISSION_MATRIX_V1",
|
|
"expected_keys": ["buy_permission_state"],
|
|
},
|
|
"T032_DISTRIBUTION_EXIT_STYLE": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:SMART_CASH_RAISE_PLAN_V1",
|
|
"expected_keys": ["execution_style"],
|
|
},
|
|
"T033_PROFIT_LOCK_20_CAP_35": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:SELL_VALUE_PRESERVATION_GATE_V1",
|
|
"expected_keys": ["immediate_sell_qty_max"],
|
|
},
|
|
"T034_SAQG_EXCLUDED_FORCES_BLOCKED": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:SATELLITE_ALPHA_QUALITY_GATE_V1",
|
|
"expected_keys": ["buy_permission_state", "blocked_reason"],
|
|
},
|
|
"T035_FOMC_7D_GATE_ACTIVE": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["fomc_position_size_gate", "fomc_size_limit"],
|
|
},
|
|
"T036_PA1_ANTITHESIS_BOOST_APPLIES_UNDER_60": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:PREDICTIVE_ALPHA_ENGINE_V1",
|
|
"expected_keys": ["antithesis_score_original", "antithesis_weight_boost"],
|
|
},
|
|
"T037_WATCH_BREAKOUT_PROMOTE_TO_WATCH": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:WATCH_BREAKOUT_REALTIME_GATE_V1",
|
|
"expected_keys": ["breakout_promotion_recommendation", "breakout_signal"],
|
|
},
|
|
"T038_ANTI_WHIPSAW_REENTRY_MARKS_CANDIDATE": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:ANTI_WHIPSAW_REENTRY_GATE_V1",
|
|
"expected_keys": ["reentry_candidate"],
|
|
},
|
|
"T039_FOMC_8D_GATE_INACTIVE": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["fomc_position_size_gate", "fomc_size_limit"],
|
|
"expected_values": {
|
|
"fomc_position_size_gate": "INACTIVE",
|
|
"fomc_size_limit": 1.0,
|
|
},
|
|
},
|
|
"T040_PA1_ANTITHESIS_NO_BOOST_AT_60": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:PREDICTIVE_ALPHA_ENGINE_V1",
|
|
"expected_keys": ["antithesis_weight_boost"],
|
|
"expected_values": {
|
|
"antithesis_weight_boost": 0,
|
|
},
|
|
},
|
|
"T041_PA1_ANTITHESIS_BOOST_AT_59_9": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:PREDICTIVE_ALPHA_ENGINE_V1",
|
|
"expected_keys": ["antithesis_weight_boost"],
|
|
"expected_values": {
|
|
"antithesis_weight_boost": 10,
|
|
},
|
|
},
|
|
"T042_PREDICTION_ACCURACY_ONE_PERCENT_PARSE": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:PREDICTIVE_ALPHA_ENGINE_V1",
|
|
"expected_keys": ["prediction_accuracy_rate", "antithesis_weight_boost"],
|
|
"expected_values": {
|
|
"prediction_accuracy_rate": 100,
|
|
"antithesis_weight_boost": 0,
|
|
},
|
|
},
|
|
"T043_PREDICTION_ACCURACY_POINT_NINE_NINE_PARSE": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:PREDICTIVE_ALPHA_ENGINE_V1",
|
|
"expected_keys": ["prediction_accuracy_rate", "antithesis_weight_boost"],
|
|
"expected_values": {
|
|
"prediction_accuracy_rate": 99,
|
|
"antithesis_weight_boost": 10,
|
|
},
|
|
},
|
|
"T044_MACRO_ELEVATED_AT_40": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["macro_risk_score", "macro_risk_regime", "heat_gate_adj"],
|
|
"expected_values": {
|
|
"macro_risk_score": 40,
|
|
"macro_risk_regime": "MACRO_ELEVATED",
|
|
"heat_gate_adj": -1,
|
|
},
|
|
},
|
|
"T045_MACRO_CRITICAL_AT_60": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["macro_risk_score", "macro_risk_regime", "heat_gate_adj"],
|
|
"expected_values": {
|
|
"macro_risk_score": 60,
|
|
"macro_risk_regime": "MACRO_CRITICAL",
|
|
"heat_gate_adj": -3,
|
|
},
|
|
},
|
|
"T046_MACRO_USD_KRW_WEAK_AT_1481": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["macro_risk_score", "macro_risk_regime", "heat_gate_adj"],
|
|
"expected_values": {
|
|
"macro_risk_score": 15,
|
|
"macro_risk_regime": "MACRO_FAVORABLE",
|
|
"heat_gate_adj": 1,
|
|
},
|
|
},
|
|
"T047_MACRO_VIX_ELEVATED_AT_21": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["macro_risk_score", "macro_risk_regime", "heat_gate_adj"],
|
|
"expected_values": {
|
|
"macro_risk_score": 10,
|
|
"macro_risk_regime": "MACRO_FAVORABLE",
|
|
"heat_gate_adj": 1,
|
|
},
|
|
},
|
|
"T048_MACRO_FOREIGN_SELL_HIGH_AT_5": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["macro_risk_score", "macro_risk_regime", "heat_gate_adj"],
|
|
"expected_values": {
|
|
"macro_risk_score": 15,
|
|
"macro_risk_regime": "MACRO_FAVORABLE",
|
|
"heat_gate_adj": 1,
|
|
},
|
|
},
|
|
"T049_MACRO_FOREIGN_SELL_MEGA_AT_10": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["macro_risk_score", "macro_risk_regime", "heat_gate_adj"],
|
|
"expected_values": {
|
|
"macro_risk_score": 20,
|
|
"macro_risk_regime": "MACRO_NEUTRAL",
|
|
"heat_gate_adj": 0,
|
|
},
|
|
},
|
|
"T050_WATCH_BREAKOUT_PROMOTION_ELIGIBLE": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:WATCH_BREAKOUT_REALTIME_GATE_V1",
|
|
"expected_keys": ["breakout_signal", "promotion_eligible", "promotion_block_reason"],
|
|
"expected_values": {
|
|
"breakout_signal": "WATCH_BREAKOUT_DETECTED",
|
|
"promotion_eligible": True,
|
|
"promotion_block_reason": None,
|
|
},
|
|
},
|
|
"T051_ANTI_WHIPSAW_REENTRY_GRADE_A": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:ANTI_WHIPSAW_REENTRY_GATE_V1",
|
|
"expected_keys": ["reentry_grade", "reentry_signal"],
|
|
"expected_values": {
|
|
"reentry_grade": "A",
|
|
"reentry_signal": "REENTRY_CANDIDATE",
|
|
},
|
|
},
|
|
"T052_CONSISTENCY_WARNING_BOUNDARY": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:CONSISTENCY_VALIDATOR_V2",
|
|
"expected_keys": ["consistency_score", "cv_verdict"],
|
|
"expected_values": {
|
|
"consistency_score": 92,
|
|
"cv_verdict": "WARNING",
|
|
},
|
|
},
|
|
"T053_CONSISTENCY_BLOCK_BOUNDARY": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:CONSISTENCY_VALIDATOR_V2",
|
|
"expected_keys": ["consistency_score", "cv_verdict"],
|
|
"expected_values": {
|
|
"consistency_score": 83,
|
|
"cv_verdict": "BLOCK",
|
|
},
|
|
},
|
|
"T054_MACRO_FOMC_NEAR_AT_5": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["macro_risk_score", "macro_risk_regime", "heat_gate_adj"],
|
|
"expected_event_tags": ["FOMC_WEEK"],
|
|
"expected_values": {
|
|
"macro_risk_score": 15,
|
|
"macro_risk_regime": "MACRO_FAVORABLE",
|
|
"heat_gate_adj": 1,
|
|
},
|
|
},
|
|
"T055_MACRO_MEGA_SELL_ALERT_TRIGGER": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:MACRO_EVENT_SYNCHRONIZER_V1",
|
|
"expected_keys": ["mega_sell_alert", "buy_gate_block_until"],
|
|
"expected_event_tags": ["MEGA_SELL_ALERT"],
|
|
"expected_values": {
|
|
"mega_sell_alert": True,
|
|
"buy_gate_block_until": "NON_EMPTY",
|
|
},
|
|
},
|
|
"T056_WATCH_BREAKOUT_BLOCKED_BY_LATE_ENTRY": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:WATCH_BREAKOUT_REALTIME_GATE_V1",
|
|
"expected_keys": ["breakout_signal", "promotion_eligible", "promotion_block_reason"],
|
|
"expected_values": {
|
|
"breakout_signal": "WATCH_BREAKOUT_DETECTED",
|
|
"promotion_eligible": False,
|
|
"promotion_block_reason": "ANTI_LATE_ENTRY_GRADE_F",
|
|
},
|
|
},
|
|
"T057_ANTI_WHIPSAW_REENTRY_GRADE_B": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:ANTI_WHIPSAW_REENTRY_GATE_V1",
|
|
"expected_keys": ["reentry_grade", "reentry_signal"],
|
|
"expected_values": {
|
|
"reentry_grade": "B",
|
|
"reentry_signal": "REENTRY_CANDIDATE",
|
|
},
|
|
},
|
|
"T058_WATCH_BREAKOUT_LOW_VELOCITY_MONITORED": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:WATCH_BREAKOUT_REALTIME_GATE_V1",
|
|
"expected_keys": ["breakout_signal", "promotion_eligible", "promotion_block_reason"],
|
|
"expected_values": {
|
|
"breakout_signal": "WATCH_BREAKOUT_MONITORED",
|
|
"promotion_eligible": False,
|
|
"promotion_block_reason": "LOW_VELOCITY",
|
|
},
|
|
},
|
|
"T059_ANTI_WHIPSAW_REENTRY_GRADE_C": {
|
|
"triggered_rule": "spec/13b_harness_formulas.yaml:ANTI_WHIPSAW_REENTRY_GATE_V1",
|
|
"expected_keys": ["reentry_grade", "reentry_signal"],
|
|
"expected_values": {
|
|
"reentry_grade": "C",
|
|
"reentry_signal": "REENTRY_CANDIDATE",
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _ensure_utf8_stdio() -> None:
|
|
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
|
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
|
|
if sys.stderr.encoding and sys.stderr.encoding.lower() not in ("utf-8", "utf8"):
|
|
sys.stderr = open(sys.stderr.fileno(), mode="w", encoding="utf-8", buffering=1)
|
|
|
|
|
|
def _load_tests() -> list[dict[str, Any]]:
|
|
payload = yaml.safe_load(TESTS_PATH.read_text(encoding="utf-8"))
|
|
tests = payload.get("tests") if isinstance(payload, dict) else []
|
|
return tests if isinstance(tests, list) else []
|
|
|
|
|
|
def main() -> int:
|
|
_ensure_utf8_stdio()
|
|
tests = _load_tests()
|
|
test_map = {
|
|
str(item.get("id")): item
|
|
for item in tests
|
|
if isinstance(item, dict) and item.get("id")
|
|
}
|
|
|
|
errors: list[str] = []
|
|
for case_id, meta in REQUIRED_CASES.items():
|
|
item = test_map.get(case_id)
|
|
if item is None:
|
|
errors.append(f"missing_case:{case_id}")
|
|
continue
|
|
expected = item.get("expected") or {}
|
|
if expected.get("triggered_rule") != meta["triggered_rule"]:
|
|
errors.append(f"{case_id}:triggered_rule={expected.get('triggered_rule')}")
|
|
for key in meta["expected_keys"]:
|
|
if key not in expected:
|
|
errors.append(f"{case_id}:missing_expected_key:{key}")
|
|
for key, value in (meta.get("expected_values") or {}).items():
|
|
if expected.get(key) != value:
|
|
errors.append(f"{case_id}:{key}={expected.get(key)}")
|
|
expected_event_matrix = expected.get("event_matrix") or []
|
|
for tag in meta.get("expected_event_tags") or []:
|
|
event_matrix = expected_event_matrix
|
|
if not isinstance(event_matrix, list) or not any(
|
|
isinstance(row, dict) and row.get("event") == tag for row in event_matrix
|
|
):
|
|
errors.append(f"{case_id}:missing_event_tag:{tag}")
|
|
|
|
if errors:
|
|
for error in errors:
|
|
print(error)
|
|
print("STRATEGY_TESTS_CONTRACT_FAIL")
|
|
return 1
|
|
|
|
print("STRATEGY_TESTS_CONTRACT_OK")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|