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>
161 lines
7.3 KiB
Python
161 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
TMP_JSON = ROOT / "Temp" / "_strategy_execution_locks_regression_input.json"
|
|
RESULT_JSON = ROOT / "Temp" / "strategy_execution_locks_regression_result.json"
|
|
LADDER_JSON = ROOT / "Temp" / "execution_method_ladder_v1.json"
|
|
TEMP_FILES = {
|
|
"late": ROOT / "Temp" / "late_chase_attribution_v1.json",
|
|
"reb": ROOT / "Temp" / "rebound_sell_efficiency_v1.json",
|
|
"di": ROOT / "Temp" / "data_integrity_score_v1.json",
|
|
"dv": ROOT / "Temp" / "derivation_validity_score_v1.json",
|
|
"de": ROOT / "Temp" / "decision_evidence_score_v1.json",
|
|
"oq": ROOT / "Temp" / "outcome_quality_score_v1.json",
|
|
}
|
|
|
|
|
|
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
|
|
def _run_apply() -> dict[str, Any]:
|
|
cmd = [sys.executable, str(ROOT / "tools" / "apply_strategy_execution_locks.py"), "--json", str(TMP_JSON)]
|
|
proc = subprocess.run(cmd, cwd=str(ROOT), text=True, capture_output=True, encoding="utf-8", errors="replace")
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f"apply_strategy_execution_locks failed: {proc.stdout}\n{proc.stderr}")
|
|
payload = json.loads(TMP_JSON.read_text(encoding="utf-8"))
|
|
h = payload.get("hApex") if isinstance(payload.get("hApex"), dict) else {}
|
|
sel = h.get("strategy_execution_locks_v1_json") if isinstance(h.get("strategy_execution_locks_v1_json"), dict) else {}
|
|
return sel
|
|
|
|
|
|
def _build_execution_method_ladder() -> dict[str, Any]:
|
|
cmd = [
|
|
sys.executable,
|
|
str(ROOT / "tools" / "build_execution_method_ladder_v1.py"),
|
|
"--json",
|
|
str(ROOT / "GatherTradingData.json"),
|
|
"--out",
|
|
str(LADDER_JSON),
|
|
]
|
|
proc = subprocess.run(cmd, cwd=str(ROOT), text=True, capture_output=True, encoding="utf-8", errors="replace")
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f"build_execution_method_ladder_v1 failed: {proc.stdout}\n{proc.stderr}")
|
|
payload = json.loads(LADDER_JSON.read_text(encoding="utf-8"))
|
|
return payload
|
|
|
|
|
|
def _build_input() -> dict[str, Any]:
|
|
rows = [
|
|
{"ticker": "AAA001", "order_type": "BUY", "validation_status": "PASS", "quantity": 10, "order_qty": 10, "buy_qty": 10},
|
|
{"ticker": "AAA002", "order_type": "SELL", "validation_status": "PASS", "quantity": 10, "order_qty": 10, "sell_qty": 10},
|
|
{"ticker": "AAA003", "order_type": "STOP_LOSS", "validation_status": "PASS", "quantity": 8, "order_qty": 8, "sell_qty": 8},
|
|
]
|
|
return {
|
|
"data": {"_harness_context": {"order_blueprint_json": rows}},
|
|
"hApex": {"order_blueprint_json": rows},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
backups: dict[str, str | None] = {}
|
|
for key, path in TEMP_FILES.items():
|
|
backups[key] = path.read_text(encoding="utf-8") if path.exists() else None
|
|
|
|
try:
|
|
_write_json(TMP_JSON, _build_input())
|
|
|
|
# Case 1: outcome low with sufficient eval -> buy block + sell scale
|
|
_write_json(TEMP_FILES["late"], {"formula_id": "LATE_CHASE_ATTRIBUTION_V1", "status": "PASS"})
|
|
_write_json(TEMP_FILES["reb"], {"formula_id": "REBOUND_SELL_EFFICIENCY_V1", "metrics": {"rebound_efficiency_score": 70.0}})
|
|
_write_json(TEMP_FILES["di"], {"formula_id": "DATA_INTEGRITY_SCORE_V1", "score": 100.0, "gate": "PASS"})
|
|
_write_json(TEMP_FILES["dv"], {"formula_id": "DERIVATION_VALIDITY_SCORE_V1", "score": 100.0, "gate": "PASS"})
|
|
_write_json(TEMP_FILES["de"], {"formula_id": "DECISION_EVIDENCE_SCORE_V1", "score": 100.0, "gate": "PASS"})
|
|
_write_json(
|
|
TEMP_FILES["oq"],
|
|
{
|
|
"formula_id": "OUTCOME_QUALITY_SCORE_V1",
|
|
"score": 40.0,
|
|
"gate": "CRITICAL_MODE",
|
|
"metrics": {"has_sufficient_eval": True},
|
|
},
|
|
)
|
|
sel1 = _run_apply()
|
|
if int(sel1.get("buy_block_count") or 0) <= 0:
|
|
raise RuntimeError("REGRESSION_FAIL: expected buy_block_count > 0 for low outcome score")
|
|
if int(sel1.get("sell_scale_count") or 0) <= 0:
|
|
raise RuntimeError("REGRESSION_FAIL: expected sell_scale_count > 0 for low outcome score")
|
|
|
|
# Case 2: decision evidence gate block -> hard block
|
|
_write_json(TEMP_FILES["de"], {"formula_id": "DECISION_EVIDENCE_SCORE_V1", "score": 70.0, "gate": "BLOCK"})
|
|
_write_json(TEMP_FILES["oq"], {"formula_id": "OUTCOME_QUALITY_SCORE_V1", "score": 95.0, "gate": "PASS"})
|
|
sel2 = _run_apply()
|
|
if int(sel2.get("hard_block_count") or 0) <= 0:
|
|
raise RuntimeError("REGRESSION_FAIL: expected hard_block_count > 0 for decision evidence block")
|
|
|
|
# Case 3: insufficient eval should suspend outcome locks
|
|
_write_json(
|
|
TEMP_FILES["oq"],
|
|
{
|
|
"formula_id": "OUTCOME_QUALITY_SCORE_V1",
|
|
"score": 30.0,
|
|
"gate": "INSUFFICIENT_EVAL",
|
|
"metrics": {"has_sufficient_eval": False},
|
|
},
|
|
)
|
|
_write_json(TEMP_FILES["de"], {"formula_id": "DECISION_EVIDENCE_SCORE_V1", "score": 100.0, "gate": "PASS"})
|
|
sel3 = _run_apply()
|
|
if str(sel3.get("outcome_lock_mode") or "") != "SUSPENDED_DUE_TO_INSUFFICIENT_EVAL":
|
|
raise RuntimeError("REGRESSION_FAIL: expected suspended outcome lock mode under insufficient eval")
|
|
|
|
ladder = _build_execution_method_ladder()
|
|
if int(ladder.get("market_order_default_count") or 0) != 0:
|
|
raise RuntimeError("REGRESSION_FAIL: expected market_order_default_count == 0")
|
|
if int(ladder.get("emergency_full_sell_without_flag_count") or 0) != 0:
|
|
raise RuntimeError("REGRESSION_FAIL: expected emergency_full_sell_without_flag_count == 0")
|
|
|
|
result = {
|
|
"status": "OK",
|
|
"formula_id": "STRATEGY_EXECUTION_LOCKS_REGRESSION_V1",
|
|
"case1": sel1,
|
|
"case2": sel2,
|
|
"case3": sel3,
|
|
"execution_method_ladder": ladder,
|
|
"assertions": {
|
|
"case1_buy_block_count_gt_0": int(sel1.get("buy_block_count") or 0) > 0,
|
|
"case1_sell_scale_count_gt_0": int(sel1.get("sell_scale_count") or 0) > 0,
|
|
"case2_hard_block_count_gt_0": int(sel2.get("hard_block_count") or 0) > 0,
|
|
"case3_outcome_lock_suspended": str(sel3.get("outcome_lock_mode") or "") == "SUSPENDED_DUE_TO_INSUFFICIENT_EVAL",
|
|
"ladder_market_order_default_count_eq_0": int(ladder.get("market_order_default_count") or 0) == 0,
|
|
"ladder_emergency_flag_missing_count_eq_0": int(ladder.get("emergency_full_sell_without_flag_count") or 0) == 0,
|
|
},
|
|
}
|
|
_write_json(RESULT_JSON, result)
|
|
print("STRATEGY_EXEC_LOCKS_REGRESSION_OK")
|
|
print(json.dumps(result, ensure_ascii=False))
|
|
return 0
|
|
finally:
|
|
for key, path in TEMP_FILES.items():
|
|
if backups[key] is None:
|
|
if path.exists():
|
|
path.unlink()
|
|
else:
|
|
path.write_text(backups[key] or "", encoding="utf-8")
|
|
if TMP_JSON.exists():
|
|
try:
|
|
TMP_JSON.unlink()
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|