RSI DCA 전략 (신규) - rsi_dca_engine/runner/control: 1분봉 RSI(14) 종목별 기준선 상향 돌파 시 정액 매수, 일 상한, 신호 45분 유효, 킬스위치, 인터벌 변경 시 커서 재초기화, 체결가 거래소 보정 - scripts: 3_run_rsi_dca(.py/_cron.sh), 백테스트·인터벌 비교, go-live 스위치 - 설정: RSI_DCA_* (모드·종목·기준선·종목별 오버라이드·일 상한 등) 모니터 (vol_live_monitor / vol_monitor_chart) - 분봉 탭(/api/candles), RSI(14) 패널·종목별 기준선, 3패널 시간축 정렬, KST 표기 - 자동매수 ON/OFF 패널(/api/rsi/status·toggle), 빌드 해시 기반 자동 새로고침, 지연 경고 - 요약표: 거래소 평균매입가 기준 보유원금·수익률, 총평가 손익, 수익률순 동적 정렬 - 잔고 스냅샷을 계좌 전체 조회 1회로 통합, 실시간 시세 반영 데이터·수집 - candle_store/loader: SQL 범위·LIMIT 조회로 대형 테이블 전량 스캔 제거 - 절전·재부팅 후 공백 자동 백필(gap_backfill, 00_backfill_gaps) 및 cron 연동 - 수집 cron 분할(매분 핵심 분봉·5분 전체), 프로젝트 한정 lock 패턴, exec 제거로 lock 정리 복구 - 모니터 종목(VOL_MONITOR_SYMBOLS)·수집 종목 7종 분리, 한글 코인명 추가 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
101 lines
3.8 KiB
Python
Executable File
101 lines
3.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""절전·재부팅 후 캔들 공백 자동 백필 (cron 수집 전 단계).
|
|
|
|
증분 수집(최신 200봉)으로 못 채우는 공백이 있는 (심볼, 인터벌)만 골라
|
|
00_download_candles.py --full --days N 으로 채운다. 공백이 없으면 즉시 종료(약 0.1초).
|
|
|
|
python scripts/00_backfill_gaps.py # 점검 후 필요 시 백필
|
|
python scripts/00_backfill_gaps.py --dry-run # 계획만 출력
|
|
python scripts/00_backfill_gaps.py --dry-run --assume-now "2026-09-12 09:00:00"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src"
|
|
if str(SRC) not in sys.path:
|
|
sys.path.insert(0, str(SRC))
|
|
|
|
from bithumb.config import load_settings # noqa: E402
|
|
from bithumb.data.candle_store import CandleStore # noqa: E402
|
|
from bithumb.data.gap_backfill import plan_backfill # noqa: E402
|
|
|
|
logger = logging.getLogger("backfill")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="캔들 공백 자동 백필")
|
|
parser.add_argument("--dry-run", action="store_true", help="계획만 출력")
|
|
parser.add_argument("--assume-now", default=None, help="테스트용 기준 시각 'YYYY-MM-DD HH:MM:SS'")
|
|
parser.add_argument("--max-days", type=int, default=30, help="백필 상한 일수 (기본 30)")
|
|
parser.add_argument("--safety", type=float, default=0.9, help="증분 커버리지 안전계수 (기본 0.9)")
|
|
parser.add_argument("-v", "--verbose", action="store_true")
|
|
args = parser.parse_args()
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
|
|
settings = load_settings()
|
|
now = datetime.strptime(args.assume_now, "%Y-%m-%d %H:%M:%S") if args.assume_now else datetime.now()
|
|
symbols = list(settings.download_symbols)
|
|
intervals = list(settings.download_intervals)
|
|
|
|
store = CandleStore(settings.db_path)
|
|
try:
|
|
db_max = {
|
|
sym: {iv: store.get_max_datetime(sym, iv) for iv in intervals}
|
|
for sym in symbols
|
|
}
|
|
finally:
|
|
store.close()
|
|
|
|
plan = plan_backfill(
|
|
db_max, now=now, intervals=intervals,
|
|
batch_size=settings.candle_count, safety=args.safety, max_days=args.max_days,
|
|
)
|
|
if plan.empty:
|
|
logger.info("공백 없음 (증분 수집 범위 내) — 백필 생략")
|
|
return 0
|
|
|
|
logger.warning("캔들 공백 감지 → 백필 계획: %s", plan.describe())
|
|
if args.dry_run:
|
|
return 0
|
|
|
|
# 인터벌별 필요 일수가 다르므로 일수별로 묶어 실행 (요청 수 최소화)
|
|
by_days: dict[int, dict[str, set[int]]] = {}
|
|
for sym, m in plan.needs.items():
|
|
for iv, days in m.items():
|
|
by_days.setdefault(days, {}).setdefault(sym, set()).add(iv)
|
|
|
|
python = sys.executable
|
|
rc_all = 0
|
|
for days in sorted(by_days):
|
|
sym_ivs = by_days[days]
|
|
syms = ",".join(sorted(sym_ivs))
|
|
ivs = ",".join(str(i) for i in sorted({iv for s in sym_ivs.values() for iv in s}))
|
|
cmd = [
|
|
python, str(ROOT / "scripts" / "00_download_candles.py"),
|
|
"--full", "--days", str(days), "--symbols", syms, "--intervals", ivs,
|
|
]
|
|
logger.warning("백필 실행: --full --days %s --symbols %s --intervals %s", days, syms, ivs)
|
|
rc = subprocess.call(cmd, cwd=str(ROOT))
|
|
if rc != 0:
|
|
logger.error("백필 실패 rc=%s (days=%s)", rc, days)
|
|
rc_all = rc
|
|
if rc_all == 0:
|
|
logger.warning("백필 완료: %s", plan.describe())
|
|
return rc_all
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|