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>
112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""vol_live 모니터 에쿼티·B&H 테스트."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
from bithumb.operations.vol_live_monitor import (
|
|
build_multi_buyhold_series,
|
|
build_spot_strategy_equity_series,
|
|
)
|
|
|
|
|
|
def test_multi_buyhold_thirds() -> None:
|
|
"""3종목 1/3씩 B&H — 한 종목만 10% 상승 시 포트폴리오 +3.33% 근사."""
|
|
panel = pd.DataFrame({
|
|
"datetime": pd.to_datetime(["2026-06-01 10:00:00", "2026-06-01 10:15:00"]),
|
|
"TRX": [100.0, 110.0],
|
|
"NEAR": [200.0, 200.0],
|
|
"WLD": [300.0, 300.0],
|
|
})
|
|
bh = build_multi_buyhold_series(panel, ["TRX", "NEAR", "WLD"], seed_krw=300_000.0)
|
|
assert bh[0]["value"] == 0.0
|
|
assert bh[1]["value"] == pytest.approx(3.3333, rel=1e-3)
|
|
|
|
|
|
def test_strategy_replay_buy_sell() -> None:
|
|
panel = pd.DataFrame({
|
|
"datetime": pd.to_datetime(["2026-06-01 10:00:00", "2026-06-01 10:15:00"]),
|
|
"TRX": [100.0, 110.0],
|
|
"NEAR": [200.0, 200.0],
|
|
"WLD": [300.0, 300.0],
|
|
})
|
|
trades = [
|
|
{
|
|
"symbol": "TRX",
|
|
"side": "buy",
|
|
"ts": "2026-06-01 10:05:00",
|
|
"order_krw": 100_000.0,
|
|
"order_coin": 1000.0,
|
|
"price": 100.0,
|
|
},
|
|
]
|
|
curve = build_spot_strategy_equity_series(
|
|
panel,
|
|
["TRX", "NEAR", "WLD"],
|
|
trades,
|
|
seed_krw=300_000.0,
|
|
current_equity=310_000.0,
|
|
window_start=pd.Timestamp("2026-06-01 10:00:00"),
|
|
)
|
|
assert curve[0]["value"] == 0.0
|
|
assert curve[-1]["value"] == pytest.approx(3.3333, rel=1e-2)
|
|
|
|
|
|
def test_write_vol_monitor_html_no_format_error(tmp_path: Path) -> None:
|
|
import re
|
|
|
|
from bithumb.operations.vol_monitor_chart import write_vol_monitor_html
|
|
|
|
out = tmp_path / "vol_live_monitor.html"
|
|
write_vol_monitor_html(out)
|
|
text = out.read_text(encoding="utf-8")
|
|
assert "/api/chart" in text
|
|
assert "equityChart" in text
|
|
js = re.search(r"<script>(.*)</script>", text, re.S)
|
|
assert js is not None
|
|
js_path = tmp_path / "monitor.js"
|
|
js_path.write_text(js.group(1), encoding="utf-8")
|
|
import subprocess
|
|
|
|
subprocess.run(["node", "--check", str(js_path)], check=True, capture_output=True)
|
|
|
|
|
|
def test_candles_api_payload_filters_and_limits(monkeypatch) -> None:
|
|
"""/api/candles — 허용 종목·분봉 검증, max_bars 제한, 최신봉 순서."""
|
|
from types import SimpleNamespace
|
|
|
|
from bithumb.operations import vol_live_monitor as m
|
|
|
|
df = pd.DataFrame({
|
|
"datetime": pd.date_range("2026-06-01 10:00:00", periods=5, freq="5min"),
|
|
"open": [1.0, 2.0, 3.0, 4.0, 5.0],
|
|
"high": [1.5, 2.5, 3.5, 4.5, 5.5],
|
|
"low": [0.5, 1.5, 2.5, 3.5, 4.5],
|
|
"close": [1.2, 2.2, 3.2, 4.2, 5.2],
|
|
"volume": [1, 1, 1, 1, 1],
|
|
})
|
|
monkeypatch.setattr(m, "load_last_candles", lambda _db, _sym, _iv, n: df.tail(int(n)).reset_index(drop=True))
|
|
settings = SimpleNamespace(
|
|
db_path="x.db",
|
|
ops_symbols=["TRX"],
|
|
vol_monitor_symbols=["XRP", "TRX"],
|
|
vol_monitor_intervals=[1, 5, 15],
|
|
vol_monitor_max_bars=3,
|
|
)
|
|
out = m.build_candles_api_payload(settings, "trx", "5")
|
|
assert out["ok"] and out["symbol"] == "TRX" and out["interval"] == 5
|
|
assert out["count"] == 3 and out["label"] == "5분"
|
|
assert [c["close"] for c in out["candles"]] == [3.2, 4.2, 5.2]
|
|
assert out["last"] == "2026-06-01 10:20:00"
|
|
|
|
assert not m.build_candles_api_payload(settings, "BTC", 5)["ok"]
|
|
assert not m.build_candles_api_payload(settings, "TRX", 7)["ok"]
|
|
assert not m.build_candles_api_payload(settings, "TRX", "abc")["ok"]
|