feat: Sprint-3 완결 + Sprint-4 착수 (WBS-3.2, 3.4, 5.2)

주요 변경:
- [WBS-3.2] 리밸런싱 V2 신호 가중 목표배분 (signal_weighted_ss001_v1)
  * equal_weight -> SS001_Norm_Score 비례 버킷내 배분
  * 하네스: 삼성(36.84%) > SK하이닉스(29.16%), Core=66.00% PASS
- [WBS-3.4] logDailyAssetHistory_ SpreadsheetApp.getActiveSpreadsheet() -> getSpreadsheet_() 수정
  * run_all 컨텍스트에서 null 반환 방지
- [WBS-5.2] deploy_gas.py 전면 재작성
  * src/gas_adapter_parts/ + src/gas/ 양쪽 소스 탐색
  * gdc_01+gdc_02 -> gas_data_collect.gs 번들링
  * dry-run PASS: 17개 파일 WARN 0건
- src/gas/ 디렉토리 신규 추가 (CLASP 조직화 구조)
- tools/automate_routine.py, download_trading_data.py 신규 추가
- .gitignore: .clasprc.json OAuth 토큰 제외 추가
- ROADMAP_WBS.md: Sprint-3 [x] 완료, Sprint-4 착수 목록 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 16:22:19 +09:00
parent cb4787ca2d
commit 72f8d61244
26 changed files with 22879 additions and 85 deletions
+20 -8
View File
@@ -381,6 +381,9 @@ def main() -> int:
continue
seen_tickers.add(ticker)
df_row = df_row_map.get(ticker, {})
# SS001_Norm_Score: 0~100 신호 강도. 0은 데이터 없음 → 최소 가중치 1.0으로 폴백
ss001_raw = _f(df_row.get("SS001_Norm_Score"), default=-1.0)
ss001_norm = max(1.0, ss001_raw) if ss001_raw > 0 else 1.0
holdings.append({
"ticker": ticker,
"name": sp["name"] or _s(df_row.get("Name")),
@@ -392,6 +395,7 @@ def main() -> int:
"final_action": _s(df_row.get("Final_Action")),
"sell_reason": _s(df_row.get("Sell_Reason")),
"force_signal": _detect_force_signal(df_row),
"ss001_norm": ss001_norm,
})
# data_feed 에만 있는 보유 종목 보완 (snap 에 없는 경우 — 비정상이지만 방어)
@@ -403,6 +407,8 @@ def main() -> int:
acct_mv = _f(row.get("Account_Market_Value"))
if weight_pct <= 0 and acct_mv <= 0:
continue
ss001_raw = _f(row.get("SS001_Norm_Score"), default=-1.0)
ss001_norm = max(1.0, ss001_raw) if ss001_raw > 0 else 1.0
holdings.append({
"ticker": ticker,
"name": _s(row.get("Name")),
@@ -414,6 +420,7 @@ def main() -> int:
"final_action": _s(row.get("Final_Action")),
"sell_reason": _s(row.get("Sell_Reason")),
"force_signal": _detect_force_signal(row),
"ss001_norm": ss001_norm,
})
# ── 3. 버킷별 현재 비중 집계 ─────────────────────────────────────────────
@@ -462,17 +469,22 @@ def main() -> int:
})
# ── 4. 종목별 분석 ───────────────────────────────────────────────────────
# 버킷 내 equal-weight target
bucket_ticker_count: dict[str, int] = {}
# [WBS-3.2] 신호 가중 목표배분 (SS001_Norm_Score 기반)
# ticker_target_pct = bucket_target × (ss001_norm / Σ ss001_norm in bucket)
# 폴백: ss001_norm 모두 1.0 → equal_weight 와 동일
bucket_ss001_total: dict[str, float] = {}
for h in holdings:
bucket_ticker_count[h["bucket"]] = bucket_ticker_count.get(h["bucket"], 0) + 1
bucket_ss001_total[h["bucket"]] = (
bucket_ss001_total.get(h["bucket"], 0.0) + h.get("ss001_norm", 1.0)
)
ticker_rows: list[dict] = []
for h in holdings:
bname = h["bucket"]
bcfg = BUCKET_CONFIG.get(bname, BUCKET_CONFIG["Satellite"])
n_tickers = bucket_ticker_count.get(bname, 1)
target_pct = _round2(bcfg["target"] / n_tickers)
bname = h["bucket"]
bcfg = BUCKET_CONFIG.get(bname, BUCKET_CONFIG["Satellite"])
ss001_w = h.get("ss001_norm", 1.0)
bucket_ss001 = max(bucket_ss001_total.get(bname, ss001_w), 0.01)
target_pct = _round2(bcfg["target"] * ss001_w / bucket_ss001)
current_pct = _round2(h["weight_pct"])
drift = _round2(current_pct - target_pct)
band_min = _round2(target_pct - band["contract"])
@@ -612,7 +624,7 @@ def main() -> int:
out = {
"formula_id": FORMULA_ID,
"metadata": {
"per_ticker_target_method": "equal_weight_within_bucket",
"per_ticker_target_method": "signal_weighted_ss001_v1",
"regime_source": "macro.REGIME_PRELIM > settings > harness_context > computed_harness",
},
"summary": summary,