f56dd37286
- 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>
109 lines
4.2 KiB
Python
109 lines
4.2 KiB
Python
import json
|
|
import os
|
|
import requests
|
|
import time
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
CLASPRC_PATH = ROOT / ".clasprc.json"
|
|
CLASP_PATH = ROOT / ".clasp.json"
|
|
SPREADSHEET_ID = "1e1TNlLfnT69nvw-I1wU_oBHmEtI2pfbld3e0fFmtrZM"
|
|
OUTPUT_XLSX = ROOT / "GatherTradingData.xlsx"
|
|
|
|
def get_tokens():
|
|
if not CLASPRC_PATH.exists():
|
|
raise FileNotFoundError(".clasprc.json not found")
|
|
with open(CLASPRC_PATH, "r", encoding="utf-8") as f:
|
|
return json.load(f)["tokens"]["default"]
|
|
|
|
def get_script_id():
|
|
if not CLASP_PATH.exists():
|
|
raise FileNotFoundError(".clasp.json not found")
|
|
with open(CLASP_PATH, "r", encoding="utf-8") as f:
|
|
return json.load(f)["scriptId"]
|
|
|
|
def refresh_access_token(tokens):
|
|
url = "https://oauth2.googleapis.com/token"
|
|
payload = {
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": tokens["refresh_token"],
|
|
"client_id": tokens["client_id"],
|
|
"client_secret": tokens["client_secret"],
|
|
}
|
|
resp = requests.post(url, data=payload)
|
|
resp.raise_for_status()
|
|
return resp.json()["access_token"]
|
|
|
|
def run_gas_function(script_id, access_token, function_name):
|
|
url = f"https://script.googleapis.com/v1/scripts/{script_id}:run"
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
payload = {"function": function_name, "devMode": True}
|
|
print(f"Executing GAS function: {function_name} ...")
|
|
resp = requests.post(url, headers=headers, json=payload)
|
|
|
|
# Handle response
|
|
if resp.status_code != 200:
|
|
print(f"Error executing function: HTTP {resp.status_code}")
|
|
print(resp.text)
|
|
return False
|
|
|
|
result = resp.json()
|
|
if "error" in result:
|
|
print(f"Function execution failed: {json.dumps(result['error'], indent=2)}")
|
|
# Check if error is because it's not deployed as API Executable (even if user said it is, common issues persist)
|
|
return False
|
|
|
|
print("Function execution triggered successfully.")
|
|
return True
|
|
|
|
def download_spreadsheet(spreadsheet_id, access_token, output_path):
|
|
print(f"Downloading spreadsheet {spreadsheet_id} as XLSX...")
|
|
# Using Drive API v3 to export Google Sheet as XLSX
|
|
url = f"https://www.googleapis.com/drive/v3/files/{spreadsheet_id}/export?mimeType=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
resp = requests.get(url, headers=headers)
|
|
|
|
if resp.status_code == 403:
|
|
print("Error: 403 Forbidden. This usually means the OAuth token lacks the 'https://www.googleapis.com/auth/drive.readonly' or 'drive' scope.")
|
|
print("Please ensure your clasp login was done with proper scopes or manual token has Drive access.")
|
|
return False
|
|
|
|
resp.raise_for_status()
|
|
with open(output_path, "wb") as f:
|
|
f.write(resp.content)
|
|
print(f"Successfully downloaded to {output_path}")
|
|
return True
|
|
|
|
def main():
|
|
try:
|
|
tokens = get_tokens()
|
|
script_id = get_script_id()
|
|
access_token = refresh_access_token(tokens)
|
|
|
|
# Step 1: Execute GAS run_all
|
|
if run_gas_function(script_id, access_token, "run_all"):
|
|
print("Waiting a bit for GAS processes to finalize (optional)...")
|
|
time.sleep(5)
|
|
|
|
# Step 2: Download spreadsheet
|
|
if download_spreadsheet(SPREADSHEET_ID, access_token, OUTPUT_XLSX):
|
|
print("\nRoutine Part 1 & 2 complete.")
|
|
print("Final step: npm run prepare-upload-zip")
|
|
else:
|
|
print("\nDownload failed. Please check Drive API scopes.")
|
|
else:
|
|
print("\nGAS execution failed. Process aborted.")
|
|
print("Falling back to local workbook sector-insight build...")
|
|
fallback = subprocess.run(["python", "tools/update_workbook_sector_insights.py"], cwd=str(ROOT))
|
|
if fallback.returncode == 0:
|
|
print("Local sector-insight workbook updated.")
|
|
else:
|
|
print("Local sector-insight workbook build failed.")
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|