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>
87 lines
3.7 KiB
Python
Executable File
87 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""RSI DCA 백테스트 — DB 15m 캔들로 최근 N일 신호·매수·평가 산출.
|
|
|
|
python scripts/3_run_rsi_dca_backtest.py --days 90
|
|
python scripts/3_run_rsi_dca_backtest.py --days 90 --levels 30:20000,35:10000 --daily-max 60000
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
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_loader import load_candles # noqa: E402
|
|
from bithumb.operations.rsi_dca_engine import ( # noqa: E402
|
|
RsiDcaConfig,
|
|
backtest_rsi_dca,
|
|
parse_levels,
|
|
)
|
|
from bithumb.operations.rsi_dca_runner import config_from_settings # noqa: E402
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="RSI DCA 백테스트")
|
|
parser.add_argument("--days", type=int, default=90)
|
|
parser.add_argument("--symbols", default=None, help="쉼표 구분 (기본 RSI_DCA_SYMBOLS)")
|
|
parser.add_argument("--levels", default=None, help="예 30:20000,35:10000")
|
|
parser.add_argument("--daily-max", type=float, default=None)
|
|
parser.add_argument("--out", default="docs/spot/3_operations/rsi_dca_backtest.json")
|
|
args = parser.parse_args()
|
|
|
|
settings = load_settings()
|
|
base = config_from_settings(settings)
|
|
cfg = RsiDcaConfig(
|
|
symbols=[s.strip().upper() for s in args.symbols.split(",")] if args.symbols else base.symbols,
|
|
interval_min=base.interval_min,
|
|
period=base.period,
|
|
levels=parse_levels(args.levels) if args.levels else base.levels,
|
|
daily_max_krw=args.daily_max if args.daily_max is not None else base.daily_max_krw,
|
|
lookback_days=base.lookback_days,
|
|
max_bars_per_tick=base.max_bars_per_tick,
|
|
max_signal_age_min=base.max_signal_age_min,
|
|
min_order_krw=base.min_order_krw,
|
|
fee_rate=base.fee_rate,
|
|
slippage_rate=base.slippage_rate,
|
|
fee_lock_rate=base.fee_lock_rate,
|
|
)
|
|
|
|
candles = {}
|
|
for sym in cfg.symbols:
|
|
# RSI 워밍업을 위해 여유 있게 로드
|
|
df = load_candles(settings.db_path, sym, cfg.interval_min, lookback_days=args.days + 10)
|
|
candles[sym] = df
|
|
rep = backtest_rsi_dca(candles, cfg, days=args.days)
|
|
|
|
levels_txt = ", ".join(f"RSI {lv:g}↑ {krw:,.0f}원" for lv, krw in cfg.levels)
|
|
print(f"RSI DCA 백테스트 · 최근 {args.days}일 · {cfg.interval_min}m RSI({cfg.period}) · {levels_txt} · 일 상한 {cfg.daily_max_krw:,.0f}원")
|
|
print(f"수수료 {cfg.fee_rate*100:.3f}% · 슬리피지 {cfg.slippage_rate*100:.3f}% · 매도 없음(마지막 종가 평가)")
|
|
print()
|
|
print(f"{'종목':5s} {'신호':>5s} {'매수':>5s} {'투입(원)':>12s} {'평가(원)':>12s} {'손익%':>7s}")
|
|
for sym in cfg.symbols:
|
|
v = rep["per_symbol"].get(sym)
|
|
if not v:
|
|
print(f"{sym:5s} {'데이터없음':>5s}")
|
|
continue
|
|
print(f"{sym:5s} {int(v['signals']):5d} {int(v['buys']):5d} {v['spent_krw']:12,.0f} {v['value_krw']:12,.0f} {v['pnl_pct']:7.2f}")
|
|
print("-" * 52)
|
|
print(f"{'합계':5s} {rep['signals']:5d} {rep['buys']:5d} {rep['total_spent_krw']:12,.0f} {rep['total_value_krw']:12,.0f} {rep['total_pnl_pct']:7.2f}")
|
|
print(f"일 상한으로 스킵된 신호: {rep['skipped_daily_cap']}건 · 일평균 투입 {rep['avg_daily_spent_krw']:,.0f}원 (신호 구간 {rep['span_days']}일)")
|
|
|
|
out = ROOT / args.out
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_text(json.dumps(rep, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
|
print(f"\n저장: {out}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|