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>
107 lines
4.2 KiB
Python
Executable File
107 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""RSI DCA 현물 정액 매수 tick (15m RSI 30/35 상향 돌파, 매도 없음).
|
|
|
|
python scripts/3_run_rsi_dca.py # RSI_DCA_MODE(.env, 기본 paper) 1회 tick
|
|
python scripts/3_run_rsi_dca.py --mode paper --loop 60
|
|
python scripts/3_run_rsi_dca.py --mode live # 실제 주문 — 사용자 직접 실행
|
|
python scripts/3_run_rsi_dca.py --status # 상태만 출력
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import sys
|
|
import time
|
|
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.operations.rsi_dca_engine import load_state # noqa: E402
|
|
from bithumb.operations.rsi_dca_runner import RsiDcaRunner # noqa: E402
|
|
|
|
|
|
def _configure_logging(verbose: bool) -> None:
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if verbose else logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
|
|
|
|
def _print_status(settings, mode: str) -> None:
|
|
st = load_state(settings.rsi_dca_state_json, mode)
|
|
d = st.get("daily") or {}
|
|
t = st.get("totals") or {}
|
|
print(f"strategy={st.get('strategy')} mode={st.get('mode')} last_run={st.get('last_run_at')}")
|
|
print(f"daily {d.get('date')} spent={float(d.get('spent_krw') or 0):,.0f}/{settings.rsi_dca_daily_max_krw:,.0f} count={d.get('count')}")
|
|
print(f"totals spent={float(t.get('spent_krw') or 0):,.0f} count={t.get('count')} trades={len(st.get('trades') or [])}")
|
|
for sym, s in (st.get("symbols") or {}).items():
|
|
rsi = s.get("last_rsi")
|
|
print(f" {sym:4s} init={s.get('initialized')} cursor={s.get('last_confirm_time')} "
|
|
f"rsi={rsi if rsi is None else round(rsi, 1)} buys={s.get('buy_count')} spent={float(s.get('spent_krw') or 0):,.0f}")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Bithumb RSI DCA 정액 매수 tick")
|
|
parser.add_argument("--mode", choices=("paper", "live"), default=None,
|
|
help="기본: .env RSI_DCA_MODE (paper)")
|
|
parser.add_argument("--loop", type=int, default=0, metavar="SEC")
|
|
parser.add_argument("--status", action="store_true", help="상태 출력만")
|
|
parser.add_argument("-v", "--verbose", action="store_true")
|
|
args = parser.parse_args()
|
|
_configure_logging(args.verbose)
|
|
|
|
settings = load_settings()
|
|
mode = (args.mode or settings.rsi_dca_mode or "paper").lower()
|
|
if args.status:
|
|
_print_status(settings, mode)
|
|
return 0
|
|
|
|
if mode == "live":
|
|
print("경고: live — 실제 주문 발생 (RSI 정액 매수, 매도 없음)")
|
|
levels = ", ".join(f"RSI {lv:g}↑ {krw:,.0f}원" for lv, krw in settings.rsi_dca_levels)
|
|
print(
|
|
f"rsi_dca {mode} | symbols={settings.rsi_dca_symbols} | {settings.rsi_dca_interval_min}m "
|
|
f"RSI({settings.rsi_dca_period}) | {levels} | 일 상한 {settings.rsi_dca_daily_max_krw:,.0f}원"
|
|
)
|
|
|
|
def _once() -> dict:
|
|
runner = RsiDcaRunner(settings, mode=mode)
|
|
report = runner.tick()
|
|
if not report.get("ok"):
|
|
print(f" skip: {report.get('note')}")
|
|
return report
|
|
d = report.get("daily") or {}
|
|
for row in report.get("results") or []:
|
|
if "error" in row:
|
|
print(f" {row['symbol']}: ERROR {row['error']}")
|
|
continue
|
|
rsi = row.get("rsi")
|
|
print(f" {row['symbol']:4s} rsi={'-' if rsi is None else f'{rsi:.1f}':>5s} fills={row['fills']} {row['note']}")
|
|
print(f" daily {d.get('date')} spent={float(d.get('spent_krw') or 0):,.0f} "
|
|
f"remaining={float(d.get('remaining_krw') or 0):,.0f} kill_switch={report.get('kill_switch')}")
|
|
return report
|
|
|
|
if args.loop <= 0:
|
|
_once()
|
|
return 0
|
|
while True:
|
|
try:
|
|
_once()
|
|
except KeyboardInterrupt:
|
|
print("\n종료")
|
|
return 0
|
|
except Exception: # noqa: BLE001
|
|
logging.exception("rsi_dca loop tick failed")
|
|
time.sleep(args.loop)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|