Files
QuantEngineByItz/tools/fetch_trade_statistics_motie_v1.py
T
kjh2064 da0e1b0f7e 비기계적 매도전략(가치보존) + 위성종목 추천 엔진 추가
매크로·실적·펀더멘털·공매도수급·호가미시구조·대내외 변수 5개 독립
팩터군의 confluence(최소 3/5 합의) 없이는 매도 트리거를 금지하는
정성적 매도판단 엔진과, 보유종목 제외 위성후보 추천 로직을 추가한다.

- 단일 팩터 임계값 돌파만으로는 매도 신호를 생성하지 않음
  (mechanical_sell_prohibited=true)
- 데이터 결측 시 항상 DATA_MISSING/INSUFFICIENT_DATA_NO_ACTION —
  추정값으로 채우지 않음
- KIS 호가10단계·공매도거래비중 + Naver 시세/수급 스크래핑 입력 연동
- SQLite 시계열 저장 + 사후 적중률 자체평가
  (evaluate_qualitative_sell_strategy_accuracy_v1)
- Gitea 일일 스케줄(장마감 후) + 파이프라인 계약 검증 게이트
2026-06-21 20:05:55 +09:00

187 lines
7.6 KiB
Python

"""관세청/산업통상부 수출입동향 → 섹터별 수출 추세(sector_export_trend) 산출기.
실측 결과(2026-06-21 세션): investing.com 직접 스크래핑은 403(Cloudflare)으로 차단되고,
관세청·산업통상부는 실시간 무인증 JSON API를 공개하지 않는다(통계청/관세청 수출입통계는
data.go.kr 공공데이터포털의 서비스키 기반 OpenAPI 또는 매월 발표되는 보도자료 첨부
XLSX/CSV로만 배포). 따라서 이 모듈은 두 경로를 모두 지원한다:
1) API 경로 — data.go.kr 관세청 수출입통계 API. CUSTOMS_API_KEY 환경변수(또는
--api-key) 필요. 키가 없거나 호출 실패 시 추정하지 않고 DATA_MISSING 반환.
2) CSV 경로(권장, 안정적) — 관세청 수출입무역통계(https://unipass.customs.go.kr/ets/)
또는 산업통상부 보도자료에서 사용자가 다운로드한 월별 HS코드별 수출입 CSV를
--csv 인자로 입력. 이 경로가 실패할 일이 없어 1차 권장 경로다.
산출물 sector_export_trend(%, MoM 또는 YoY)는 qualitative_sell_strategy_v1의
fundamental_trajectory 보강 입력 및 compute_satellite_candidate_score의 1차 팩터로 쓰인다.
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import sys
from collections import defaultdict
from pathlib import Path
from typing import Any
import requests
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
# 섹터 → HS코드 prefix(2~4자리). 위성종목 추천/매도판단에 쓰는 핵심 수출 섹터만 우선 등록.
SECTOR_HS_MAP: dict[str, tuple[str, ...]] = {
"반도체": ("8541", "8542"),
"자동차": ("8701", "8702", "8703", "8704"),
"2차전지": ("8507",),
"조선": ("8901", "8902", "8905"),
"철강": ("72",),
"석유화학": ("29", "39"),
"디스플레이": ("8524", "9013"),
"기계": ("84",),
"바이오": ("30",), # universe.Sector 실측 라벨이 "바이오"(헬스 접미사 없음) — 그대로 매칭
"방산": ("93",), # 무기류·탄약(HS Ch.93) — 현대로템 등 보유종목 K-방산 테마 대응
}
CUSTOMS_API_BASE = "https://apis.data.go.kr/1220000/nitemtrade/getNitemtradeList"
def fetch_customs_trade_api(
session: requests.Session,
api_key: str | None,
hs_code: str,
start_ym: str,
end_ym: str,
) -> dict[str, Any]:
"""data.go.kr 관세청 수출입통계 API 호출. 키 없거나 실패 시 DATA_MISSING(추정 금지)."""
if not api_key:
return {"status": "DATA_MISSING", "note": "CUSTOMS_API_KEY 미설정 — --csv 경로 사용 권장"}
try:
resp = session.get(
CUSTOMS_API_BASE,
params={
"serviceKey": api_key,
"strtYymm": start_ym,
"endYymm": end_ym,
"hsSgn": hs_code,
"type": "json",
},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
except Exception as exc: # noqa: BLE001 — 외부 API 실패는 광범위하게 잡아 DATA_MISSING 처리
return {"status": "API_ERROR", "note": str(exc)}
return {"status": "OK", "raw": data, "source_url": CUSTOMS_API_BASE}
def load_trade_statistics_csv(path: Path) -> list[dict[str, Any]]:
"""관세청/산업통상부 배포 CSV. 컬럼: 기간(YYYYMM), HS코드, 수출액(달러), 수입액(달러).
헤더명은 배포처마다 다를 수 있어 한글/영문 별칭을 모두 허용한다.
"""
alias = {
"기간": "period", "year_month": "period", "period": "period",
"hs코드": "hs_code", "hs_code": "hs_code", "hscode": "hs_code",
"수출액": "export_usd", "export": "export_usd", "export_usd": "export_usd",
"수입액": "import_usd", "import": "import_usd", "import_usd": "import_usd",
}
rows: list[dict[str, Any]] = []
with path.open(encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
for raw_row in reader:
row: dict[str, Any] = {}
for key, value in raw_row.items():
norm_key = alias.get(str(key).strip().lower())
if norm_key:
row[norm_key] = value
if {"period", "hs_code"}.issubset(row):
for money_field in ("export_usd", "import_usd"):
if money_field in row:
try:
row[money_field] = float(str(row[money_field]).replace(",", ""))
except ValueError:
row[money_field] = 0.0
rows.append(row)
return rows
def compute_sector_export_trend(
rows: list[dict[str, Any]],
sector: str,
compare: str = "yoy",
) -> dict[str, Any]:
"""sector_export_trend(%) = 최신월 수출액 / 비교월 수출액 - 1.
compare="yoy": 12개월 전 동월 대비. compare="mom": 직전월 대비.
데이터 부족 시 추정하지 않고 DATA_MISSING.
"""
hs_prefixes = SECTOR_HS_MAP.get(sector)
if not hs_prefixes:
return {"status": "UNKNOWN_SECTOR", "sector": sector, "known_sectors": list(SECTOR_HS_MAP)}
by_period: dict[str, float] = defaultdict(float)
for row in rows:
hs_code = str(row.get("hs_code") or "")
if any(hs_code.startswith(prefix) for prefix in hs_prefixes):
period = str(row.get("period") or "")
by_period[period] += float(row.get("export_usd") or 0.0)
if len(by_period) < 2:
return {"status": "DATA_MISSING", "sector": sector, "note": "기간별 수출액 표본 부족"}
periods_sorted = sorted(by_period)
latest_period = periods_sorted[-1]
latest_value = by_period[latest_period]
if compare == "mom":
compare_period = periods_sorted[-2]
else:
latest_ym = int(latest_period)
target_ym = latest_ym - 100 # YYYYMM에서 12개월 전 = -100
compare_period = str(target_ym)
if compare_period not in by_period:
return {"status": "DATA_MISSING", "sector": sector, "note": f"YoY 비교월({compare_period}) 데이터 없음 — MoM으로 재시도 권장"}
compare_value = by_period.get(compare_period, 0.0)
if compare_value <= 0:
return {"status": "DATA_MISSING", "sector": sector, "note": "비교월 수출액이 0 이하"}
trend_pct = round((latest_value / compare_value - 1.0) * 100.0, 4)
return {
"status": "OK",
"sector": sector,
"compare": compare,
"latest_period": latest_period,
"compare_period": compare_period,
"sector_export_trend": trend_pct,
}
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--csv", type=Path, help="관세청/산업통상부 배포 수출입 CSV 경로(권장 경로)")
ap.add_argument("--sector", default="반도체", choices=list(SECTOR_HS_MAP))
ap.add_argument("--compare", default="yoy", choices=["yoy", "mom"])
ap.add_argument("--api-key", default=os.environ.get("CUSTOMS_API_KEY"))
ap.add_argument("--hs-code", default="", help="API 경로 사용 시 HS코드")
ap.add_argument("--start-ym", default="")
ap.add_argument("--end-ym", default="")
args = ap.parse_args()
if args.csv:
rows = load_trade_statistics_csv(args.csv)
result = compute_sector_export_trend(rows, args.sector, args.compare)
else:
session = requests.Session()
result = fetch_customs_trade_api(session, args.api_key, args.hs_code, args.start_ym, args.end_ym)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())