feat: sector trend analysis + ETF representative monitor (DAG step_count 81->83)
- src/quant_engine/sector_trend_analysis.py: ETF proxy 기반 11개 섹터 동향 + smart money lens - src/quant_engine/etf_representative_monitor.py: ETF 대표 종목 8개 추적 + 벤치마크 연동 - tools/build_sector_trend_analysis_v1.py: SECTOR_TREND_ANALYSIS_V1 Temp JSON 생성 - tools/build_etf_representative_monitor_v1.py: ETF_REPRESENTATIVE_MONITOR_V1 Temp JSON 생성 - tools/update_workbook_sector_insights.py: Google Sheets 섹터 인사이트 동기화 - spec/41_release_dag.yaml: step_count 81->83, wave_1에 2개 신규 노드 등록 - validate_engine_harness_gate.py: CHECK_87B (SECTOR_TREND_ANALYSIS_V1) + ETF monitor DAG 스텝 추가 - render_operational_report.py: sector_trend_analysis_v1 / etf_representative_monitor_v1 / portfolio_performance_summary 섹션 추가 - gas_lib.gs: doPost + syncSectorInsightSheets_ (섹터 인사이트 GAS 동기화 엔드포인트) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import shutil
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -54,7 +55,12 @@ BUNDLE_MAP: dict[str, list[str]] = {
|
||||
}
|
||||
|
||||
SCRIPT_ID = "1xfeBAeeknmnBtSvrIqWXO_2hc3ByeriLUOSuOOB4YxLLHhN3zdnL7tVh"
|
||||
PROJECT_ID = "1072944905499"
|
||||
DEPLOYMENT_ID = "AKfycbzq1XM53XafyCNYurnF9TAQHT3FHBDsBd36rCbCoWSmJD3SaZ1BHCPDYZYhclG9qD5Y"
|
||||
DEFAULT_WEBAPP_URL = f"https://script.google.com/macros/s/{DEPLOYMENT_ID}/exec"
|
||||
SECTOR_TREND_JSON = ROOT / "Temp" / "sector_trend_analysis_v1.json"
|
||||
ETF_REP_JSON = ROOT / "Temp" / "etf_representative_monitor_v1.json"
|
||||
SECTOR_INSIGHT_BUNDLE = DEPLOY_DIR / "gas_sector_insight_payload.gs"
|
||||
|
||||
|
||||
def get_now_kst() -> str:
|
||||
@@ -113,6 +119,7 @@ def build_deploy(dry_run: bool = False) -> bool:
|
||||
if not dry_run:
|
||||
clasp_cfg = {
|
||||
"scriptId": SCRIPT_ID,
|
||||
"projectId": PROJECT_ID,
|
||||
"rootDir": str(DEPLOY_DIR.relative_to(ROOT)).replace("\\", "/"),
|
||||
}
|
||||
(ROOT / ".clasp.json").write_text(
|
||||
@@ -166,10 +173,96 @@ def clasp_deploy() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _sector_insight_payload() -> dict:
|
||||
if not SECTOR_TREND_JSON.exists():
|
||||
raise FileNotFoundError(SECTOR_TREND_JSON)
|
||||
if not ETF_REP_JSON.exists():
|
||||
raise FileNotFoundError(ETF_REP_JSON)
|
||||
return {
|
||||
"action": "sync_sector_insights",
|
||||
"sector_trend_analysis": json.loads(SECTOR_TREND_JSON.read_text(encoding="utf-8")),
|
||||
"etf_representative_monitor": json.loads(ETF_REP_JSON.read_text(encoding="utf-8")),
|
||||
}
|
||||
|
||||
|
||||
def write_sector_insight_bundle() -> bool:
|
||||
try:
|
||||
payload = _sector_insight_payload()
|
||||
except Exception as exc:
|
||||
print("[deploy_gas] cannot build sector insight payload: " + str(exc))
|
||||
return False
|
||||
bundle = (
|
||||
"// Auto-generated by tools/deploy_gas.py\n"
|
||||
"// Contains the latest sector insight payload for clasp run fallback.\n"
|
||||
"const __SECTOR_INSIGHT_PAYLOAD__ = "
|
||||
+ json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
+ ";\n"
|
||||
"function syncSectorInsightSheetsFromBundle_() {\n"
|
||||
" return syncSectorInsightSheets(__SECTOR_INSIGHT_PAYLOAD__);\n"
|
||||
"}\n"
|
||||
)
|
||||
SECTOR_INSIGHT_BUNDLE.write_text(bundle, encoding="utf-8")
|
||||
print("[deploy_gas] write " + str(SECTOR_INSIGHT_BUNDLE))
|
||||
return True
|
||||
|
||||
|
||||
def sync_sector_insights(webapp_url: str) -> bool:
|
||||
if not webapp_url:
|
||||
print("[deploy_gas] sync-sector-insights requires --webapp-url")
|
||||
return False
|
||||
try:
|
||||
payload = _sector_insight_payload()
|
||||
except Exception as exc:
|
||||
print("[deploy_gas] missing sector insight data: " + str(exc))
|
||||
return False
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
webapp_url,
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
text = resp.read().decode("utf-8", errors="replace")
|
||||
print("[deploy_gas] sync_sector_insights OK")
|
||||
print(text)
|
||||
return True
|
||||
except Exception as exc:
|
||||
print("[deploy_gas] sync_sector_insights FAILED: " + str(exc))
|
||||
return False
|
||||
|
||||
|
||||
def sync_sector_insights_via_clasp_run() -> bool:
|
||||
if not SECTOR_INSIGHT_BUNDLE.exists():
|
||||
print(f"[deploy_gas] missing {SECTOR_INSIGHT_BUNDLE.name}")
|
||||
return False
|
||||
print("[deploy_gas] clasp run syncSectorInsightSheetsFromBundle_ ...")
|
||||
res = subprocess.run(
|
||||
["npx", "@google/clasp", "run", "syncSectorInsightSheetsFromBundle_", "--nondev"],
|
||||
cwd=str(ROOT),
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
print(res.stdout)
|
||||
if res.stderr:
|
||||
print("STDERR: " + res.stderr[:500])
|
||||
if res.returncode != 0:
|
||||
print("[deploy_gas] clasp run syncSectorInsightSheetsFromBundle_ FAILED rc=" + str(res.returncode))
|
||||
return False
|
||||
print("[deploy_gas] clasp run syncSectorInsightSheetsFromBundle_ OK")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="GAS auto-deploy")
|
||||
parser.add_argument("--dry-run", action="store_true", help="List files without writing")
|
||||
parser.add_argument("--skip-push", action="store_true", help="Bundle only, skip clasp push")
|
||||
parser.add_argument("--sync-sector-insights", action="store_true", help="POST sector insight JSON to a deployed GAS web app")
|
||||
parser.add_argument("--webapp-url", default=os.environ.get("GAS_WEBAPP_URL", DEFAULT_WEBAPP_URL), help="Apps Script web app URL for sync POST")
|
||||
args = parser.parse_args()
|
||||
|
||||
ok = build_deploy(dry_run=args.dry_run)
|
||||
@@ -177,8 +270,14 @@ def main() -> None:
|
||||
print("[deploy_gas] Some source files missing -- check warnings above")
|
||||
raise SystemExit(1)
|
||||
|
||||
if args.sync_sector_insights and not args.dry_run and not args.skip_push:
|
||||
if not write_sector_insight_bundle():
|
||||
raise SystemExit(1)
|
||||
|
||||
if args.dry_run or args.skip_push:
|
||||
print("[deploy_gas] dry-run/skip-push -- push skipped")
|
||||
if args.sync_sector_insights:
|
||||
print("[deploy_gas] sync skipped because push/deploy was skipped")
|
||||
return
|
||||
|
||||
if not clasp_push():
|
||||
@@ -187,6 +286,12 @@ def main() -> None:
|
||||
if not clasp_deploy():
|
||||
raise SystemExit(1)
|
||||
|
||||
if args.sync_sector_insights:
|
||||
if not sync_sector_insights(args.webapp_url):
|
||||
print("[deploy_gas] webapp sync failed; falling back to clasp run")
|
||||
if not sync_sector_insights_via_clasp_run():
|
||||
raise SystemExit(1)
|
||||
|
||||
print("[deploy_gas] Done. To run_all: python tools/automate_routine.py")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user