Files
upbit-trader/tests/collect_1y_data.py
joungmin cfbacdacbc feat: rewrite strategy to 10m vol-lead with undying signal + watch alert
- core/strategy.py: full rewrite to Volume Lead strategy
  - 10m candle direct detection (no 40m resampling)
  - F&G 3-tier vol threshold: <=40->6x, 41-50->5x, >50->blocked
  - Undying signal: price drop does not cancel signal (sig_p fixed)
  - Vol refresh: stronger vol_r updates signal price and timer
  - Watch alert: 4x-6x approaching threshold notifies via Telegram
  - WATCH_VOL_THRESH=4.0, WATCH_COOLDOWN_MIN=30, WATCH_VOL_JUMP=0.5
- daemon/runner.py: remove FNG_MIN_ENTRY block and Bear regime block
  - Only FNG_MAX_ENTRY(>50) blocks scan (greed/extreme greed)
  - Fast-poll loop cleaned of regime check
- core/notify.py: add notify_watch() for near-signal Telegram alerts
  - Shows vol_r, distance to threshold, price, quiet pct
- tests/: add 1y data collection and simulation scripts
  - collect_1y_data.py, refresh_cache.py
  - sim_10m_vol.py, sim_current.py, sim_regime_1y.py
  - sim_regime_sweep.py, sim_vol_override.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:28:13 +09:00

154 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""1년치 데이터 수집 — 10분봉 OHLCV + F&G 히스토리.
생성 파일:
data/sim1y_cache.pkl — {"10m": {ticker: DataFrame}} (10분봉, 365일)
data/fng_1y.json — {"YYYY-MM-DD": int, ...} (Fear & Greed 1년치)
소요 시간: 약 10~15분 (20종목 × 263 API 호출)
"""
import os as _os, sys as _sys
_sys.path.insert(0, _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))))
import json
import pickle
import time
import urllib.request
from datetime import datetime, timedelta
from pathlib import Path
import pandas as pd
import pyupbit
from dotenv import load_dotenv
load_dotenv(dotenv_path=Path(__file__).parent.parent / ".env")
# ── 설정 ─────────────────────────────────────────────────
CACHE_FILE = Path(__file__).parent.parent / "data" / "sim1y_cache.pkl"
FNG_FILE = Path(__file__).parent.parent / "data" / "fng_1y.json"
TOP30_FILE = Path(__file__).parent.parent / "data" / "top30_tickers.pkl"
DAYS = 365
TOP_N = 20
# ── 10분봉 수집 ───────────────────────────────────────────
def fetch_10m(ticker: str, days: int) -> "pd.DataFrame | None":
target_start = datetime.now() - timedelta(days=days)
all_dfs, to, prev_oldest = [], None, None
while True:
kwargs = dict(ticker=ticker, interval="minute10", count=200)
if to:
kwargs["to"] = to.strftime("%Y-%m-%d %H:%M:%S")
try:
df = pyupbit.get_ohlcv(**kwargs)
except Exception:
time.sleep(0.5)
break
if df is None or df.empty:
break
all_dfs.append(df)
oldest = df.index[0]
if prev_oldest is not None and oldest >= prev_oldest:
break
prev_oldest = oldest
if oldest <= target_start:
break
to = oldest
time.sleep(0.12)
if not all_dfs:
return None
combined = pd.concat(all_dfs).sort_index()
combined = combined[~combined.index.duplicated(keep="last")]
return combined[combined.index >= target_start]
# ── F&G 1년치 수집 ────────────────────────────────────────
def fetch_fng(limit: int = 400) -> dict:
url = f"https://api.alternative.me/fng/?limit={limit}&format=json"
with urllib.request.urlopen(url, timeout=15) as r:
data = json.loads(r.read())
result = {}
for e in data["data"]:
dt = datetime.fromtimestamp(int(e["timestamp"]))
result[dt.strftime("%Y-%m-%d")] = int(e["value"])
return result
# ── 메인 ─────────────────────────────────────────────────
def main():
# ── 종목 목록 ─────────────────────────────────────────
try:
from core.market import get_top_tickers
tickers = get_top_tickers()[:TOP_N]
print(f"Top{TOP_N} 종목 API 조회: {tickers}\n")
# top30 파일 갱신
pickle.dump(tickers, open(TOP30_FILE, "wb"))
except Exception as e:
print(f" [경고] 종목 API 실패: {e}")
if TOP30_FILE.exists():
tickers = pickle.load(open(TOP30_FILE, "rb"))[:TOP_N]
print(f" 기존 top30 파일 사용: {tickers}\n")
else:
print(" [오류] 종목 목록 없음. 종료.")
return
# ── F&G 1년치 ─────────────────────────────────────────
print("F&G 1년치 수집...")
try:
fng_map = fetch_fng(limit=400)
sorted_dates = sorted(fng_map.keys())
print(f" 기간: {sorted_dates[0]} ~ {sorted_dates[-1]} ({len(fng_map)}일)")
# 분포
zones = {"극공포(≤25)": 0, "공포(26~40)": 0, "중립(41~55)": 0,
"탐욕(56~75)": 0, "극탐욕(76+)": 0}
for v in fng_map.values():
if v <= 25: zones["극공포(≤25)"] += 1
elif v <= 40: zones["공포(26~40)"] += 1
elif v <= 55: zones["중립(41~55)"] += 1
elif v <= 75: zones["탐욕(56~75)"] += 1
else: zones["극탐욕(76+)"] += 1
total = sum(zones.values())
for name, cnt in zones.items():
print(f" {name:12} {cnt:>3}일 ({cnt/total*100:.1f}%)")
json.dump(fng_map, open(FNG_FILE, "w"))
print(f" 저장: {FNG_FILE}\n")
except Exception as e:
print(f" [오류] F&G 수집 실패: {e}\n")
fng_map = {}
# ── 10분봉 1년치 ──────────────────────────────────────
print(f"10분봉 {DAYS}일치 수집 중 ({len(tickers)}종목)...")
print(f" 예상 소요: {len(tickers) * 265 * 0.12 / 60:.0f}~{len(tickers) * 265 * 0.15 / 60:.0f}\n")
data = {"10m": {}}
for i, ticker in enumerate(tickers, 1):
start_t = time.time()
df = fetch_10m(ticker, DAYS)
elapsed = time.time() - start_t
if df is not None and len(df) > 500:
data["10m"][ticker] = df
candles = len(df)
period = f"{df.index[0].strftime('%Y-%m-%d')}~{df.index[-1].strftime('%Y-%m-%d')}"
print(f" {i:>2}/{len(tickers)} {ticker:<15} {candles:>6}{period} ({elapsed:.0f}s)")
else:
print(f" {i:>2}/{len(tickers)} {ticker:<15} 데이터 부족 ({elapsed:.0f}s)")
time.sleep(0.15)
# ── 저장 ──────────────────────────────────────────────
print(f"\n수집 완료: {len(data['10m'])}종목")
if data["10m"]:
sample = next(iter(data["10m"].values()))
print(f"기간: {sample.index[0].strftime('%Y-%m-%d')} ~ {sample.index[-1].strftime('%Y-%m-%d')}")
print(f"봉 수: {len(sample)}개 (10분봉)")
# 파일 크기 추정
import sys
size_mb = sys.getsizeof(pickle.dumps(data)) / 1024 / 1024
print(f"예상 크기: {size_mb:.1f} MB")
pickle.dump(data, open(CACHE_FILE, "wb"))
print(f"\n캐시 저장: {CACHE_FILE}")
print("완료!")
if __name__ == "__main__":
main()