refactor: DeepCoin 1·2단계 파이프라인으로 구조 재편

레거시 분석·매칭·운영 코드를 정리하고 src/deepcoin 기반으로 재구성한다.
1단계 GT는 2년 스윙·눌림목·돌파·다이버전스 타점을 차트에 표시하고,
2단계는 8개 매매 기법과 GT 정합 평가 스크립트를 추가한다.
.env와 GT JSON 산출물은 추적에서 제외한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-06-08 23:51:26 +09:00
parent 51f70076fb
commit df3c9aecb9
154 changed files with 4629 additions and 215122 deletions

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env python3
"""운영 전 누락 봉 증분 보완 (05/06 시작 시 자동 호출과 동일)."""
import argparse
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.data.ops_sync import ensure_ops_candles
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="운영 전 coins.db 누락 봉 동기화")
parser.add_argument(
"--force",
action="store_true",
help="최신 간격도 전부 재수집",
)
parser.add_argument(
"--verbose",
action="store_true",
help="API 수집 진행 로그 출력",
)
args = parser.parse_args()
ensure_ops_candles(force=args.force, verbose_download=args.verbose)

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env python3
"""01단계: WLD 봉 데이터 다운로드 → data/coins.db"""
"""0단계: 빗썸 캔들 데이터 수집 (download_candles.py 래퍼)."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.data.downloader import download
if __name__ == "__main__":
download()
target = Path(__file__).resolve().parent / "download_candles.py"
runpy.run_path(str(target), run_name="__main__")

View File

@@ -1,11 +1,123 @@
#!/usr/bin/env python3
"""Ground Truth: 매수·매도 정답 타점 JSON 생성."""
import runpy
"""1단계: Ground Truth — 사후 최적 스윙 매수·매도 타점 (1매수·1매도 레그)."""
from __future__ import annotations
import argparse
import logging
import sys
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src"
if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
from deepcoin.config import load_settings
from deepcoin.data.intervals import interval_label
from deepcoin.ground_truth.chart import render_ground_truth_chart
from deepcoin.ground_truth.ground_truth import GtParams, build_ground_truth, save_ground_truth
def _configure_logging(verbose: bool) -> None:
"""로깅 레벨을 설정한다."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def main() -> int:
"""CLI 진입점."""
parser = argparse.ArgumentParser(description="Ground Truth 스윙 레그 생성 (1단계)")
parser.add_argument("--interval", type=int, default=None, help="GT 인터벌(분)")
parser.add_argument("--days", type=int, default=None, help="GT·타점 기간(일). 기본 730(2년)")
parser.add_argument("--zigzag", type=float, default=None, help="ZigZag 되돌림 %%")
parser.add_argument("--min-leg", type=float, default=None, help="최소 레그 수익률 %%")
parser.add_argument("--no-chart", action="store_true", help="HTML 차트 생략")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
_configure_logging(args.verbose)
settings = load_settings()
params = GtParams(
interval_min=args.interval or settings.gt_interval_min,
lookback_days=args.days or settings.gt_lookback_days,
zigzag_reversal_pct=args.zigzag or settings.gt_zigzag_reversal_pct,
min_leg_pct=args.min_leg or settings.gt_min_leg_pct,
pullback_min_pct=settings.gt_pullback_min_pct,
pullback_local_order=settings.gt_pullback_local_order,
breakout_buffer_pct=settings.gt_breakout_buffer_pct,
breakout_consolidation_bars=settings.gt_breakout_consolidation_bars,
breakout_min_rally_pct=settings.gt_breakout_min_rally_pct,
div_local_order=settings.gt_div_local_order,
div_min_bars_between=settings.gt_div_min_bars_between,
div_min_rsi_diff=settings.gt_div_min_rsi_diff,
div_min_future_move_pct=settings.gt_div_min_future_move_pct,
)
logging.info(
"GT 생성: %s %s, %s일, ZigZag=%s%%, min_leg=%s%%, 초기=%s",
settings.symbol,
interval_label(params.interval_min),
params.lookback_days,
params.zigzag_reversal_pct,
params.min_leg_pct,
f"{settings.gt_initial_cash_krw:,.0f}",
)
result = build_ground_truth(
db_path=settings.db_path,
symbol=settings.symbol,
coin_name=settings.coin_name,
params=params,
initial_cash_krw=settings.gt_initial_cash_krw,
fee_rate=settings.gt_trading_fee_rate,
)
out_json = save_ground_truth(result, settings.ground_truth_file)
summary = result["summary"]
meta = result["meta"]
pnl = result["pnl"]
print("\n=== Ground Truth 완료 (1단계 벤치마크) ===")
print(f"대상: {meta['symbol']} ({meta['interval_label']})")
print(f"GT·수익 기간: {meta['data_from']} ~ {meta['data_to']} ({meta['bar_count']}봉)")
print(f"차트·타점: 최근 {settings.download_days}일 (2년)")
print(f"피벗: {meta['pivot_count']}개 → 레그: {summary['leg_count']}")
print(
f"매수 타점: {summary['buy_count']}"
f"(눌림 {summary.get('pullback_buy_count', 0)} / 돌파 {summary.get('breakout_buy_count', 0)} "
f"/ 다이버전스 {summary.get('divergence_buy_count', 0)}) "
f"/ 매도: {summary['sell_count']}개 (다이버전스 {summary.get('divergence_sell_count', 0)})"
)
print(f"레그 수익률 — 평균: {summary['avg_leg_pct']}%, 최대: {summary['max_leg_pct']}%")
period = ""
if pnl.get("period_from"):
period = f" ({pnl['period_from'][:10]} ~ {pnl['period_to'][:10]})"
print(f"\n=== 누적 수익{period} — 초기 {pnl['initial_cash_krw']:,.0f}원 ===")
print(f"최종 자산: {pnl['final_cash_krw']:,.0f}")
print(f"손익: {pnl['total_pnl_krw']:+,.0f}")
print(f"기간 수익률: {pnl['total_return_pct']:+.2f}%")
print(f"거래 레그: {pnl['legs_traded']}")
print(f"JSON: {out_json}")
if not args.no_chart:
chart_path = render_ground_truth_chart(
db_path=settings.db_path,
symbol=settings.symbol,
gt_result=result,
output_path=settings.ground_truth_chart_file,
chart_lookback_days=settings.download_days,
)
print(f"차트: {chart_path}")
return 0
from deepcoin.ground_truth.ground_truth import run_from_db
if __name__ == "__main__":
run_from_db()
raise SystemExit(main())

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env python3
"""03단계: 3분~일봉 전 기법 enrich (latest CSV)."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.analysis.general_analysis_enrich_runner import main
if __name__ == "__main__":
main()

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env python3
"""03b단계: Ground Truth 타점 MTF 기술적 스냅샷 CSV."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.analysis.general_analysis_runner import main
if __name__ == "__main__":
main()

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env python3
"""03c단계: GT 타점 MTF 프로필 분석 (3분~월봉, 매수/매도 대조)."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.matching.gt_mtf_profile import run_gt_mtf_profile
if __name__ == "__main__":
run_gt_mtf_profile()

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""2단계: Ground Truth 정합 매매 기법 실행 및 비교."""
from __future__ import annotations
import argparse
import logging
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 deepcoin.config import load_settings
from deepcoin.data.intervals import interval_label
from deepcoin.evaluation.report import (
build_comparison_report,
render_comparison_html,
save_comparison_report,
)
from deepcoin.techniques.base import TechniqueParams
from deepcoin.techniques.registry import list_technique_ids
from deepcoin.techniques.runner import (
load_ground_truth,
run_all_techniques,
save_technique_result,
)
def _configure_logging(verbose: bool) -> None:
"""로깅 레벨을 설정한다."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def main() -> int:
"""CLI 진입점."""
parser = argparse.ArgumentParser(description="매매 기법 실행 및 GT 정합 비교 (2단계)")
parser.add_argument(
"--techniques",
type=str,
default=None,
help="실행할 기법 ID (쉼표 구분). 기본: 전체",
)
parser.add_argument("--tolerance", type=int, default=None, help="GT 정합 허용 봉 수")
parser.add_argument("--no-report", action="store_true", help="비교 리포트 생략")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
_configure_logging(args.verbose)
settings = load_settings()
if not settings.ground_truth_file.exists():
logging.error("Ground Truth 파일 없음: %s — 먼저 02_ground_truth.py 실행", settings.ground_truth_file)
return 1
gt_result = load_ground_truth(settings.ground_truth_file)
technique_ids = None
if args.techniques:
technique_ids = [t.strip() for t in args.techniques.split(",") if t.strip()]
params = TechniqueParams(
interval_min=settings.gt_interval_min,
lookback_days=settings.gt_lookback_days,
min_leg_pct=settings.gt_min_leg_pct,
initial_cash_krw=settings.gt_initial_cash_krw,
fee_rate=settings.gt_trading_fee_rate,
extra={"reversal_pct": settings.gt_zigzag_reversal_pct},
)
tolerance = args.tolerance or settings.gt_align_tolerance_bars
logging.info(
"기법 실행: %s %s, %s일, tolerance=%s",
settings.symbol,
interval_label(params.interval_min),
params.lookback_days,
tolerance,
)
results = run_all_techniques(
db_path=settings.db_path,
symbol=settings.symbol,
params=params,
gt_result=gt_result,
tolerance_bars=tolerance,
technique_ids=technique_ids,
)
saved_paths: list[Path] = []
for result in results:
path = save_technique_result(result, settings.techniques_dir)
saved_paths.append(path)
align = result.alignment or {}
legs = align.get("legs", {})
print(
f" [{result.technique_id}] {result.technique_name}: "
f"레그 {result.summary.get('leg_count', 0)}개, "
f"수익 {result.pnl.get('total_return_pct', 0):+.1f}%, "
f"GT정합 score={align.get('score', 0)*100:.1f} "
f"(leg recall {legs.get('leg_recall', 0)*100:.0f}%)"
)
print(f"\n=== 2단계 기법 실행 완료 ({len(results)}개) ===")
print(f"저장: {settings.techniques_dir}/")
for path in saved_paths:
print(f" - {path.name}")
if not args.no_report:
report = build_comparison_report(results, gt_result, settings.symbol)
json_path = save_comparison_report(report, settings.analysis_report_json)
html_path = render_comparison_html(report, settings.analysis_report_html)
print(f"\n=== GT 정합 순위 (상위 3) ===")
gt_return = report["gt"]["return_pct"]
print(f"GT 벤치마크: {gt_return:+.1f}%")
for idx, row in enumerate(report["ranking"][:3], start=1):
print(
f" {idx}. {row['technique_name']}: "
f"score {row['score']*100:.1f}, "
f"수익 {row['tech_return_pct']:+.1f}%, "
f"leg recall {row['leg_recall']*100:.0f}%"
)
print(f"리포트 JSON: {json_path}")
print(f"리포트 HTML: {html_path}")
print(f"\n등록 기법: {', '.join(list_technique_ids())}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env python3
"""인과 GT leg 엔진 파라미터 캘리브레이션 (Option C +300% 경로)."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.ground_truth.causal_gt_calibrate import run_causal_gt_calibration
if __name__ == "__main__":
run_causal_gt_calibration()

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""Hybrid DD tier 임계값 train 캘리브레이션 (Option C 2차)."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
import pandas as pd
from config import CHART_LOOKBACK_DAYS, MATCH_PRIMARY_INTERVAL, SYMBOL
from deepcoin.data.mtf_bb import load_frames_from_db
from deepcoin.ground_truth.ground_truth import load_ground_truth
from deepcoin.ground_truth.hybrid_dd_calibrate import run_and_save_calibration
from deepcoin.matching.load_rules import load_matched_rules
from deepcoin.ops.monitor import Monitor
from deepcoin.paths import MATCHING_FIRE_OUTCOMES, MATCHING_HYBRID_DD_CALIBRATION_JSON
def main() -> None:
"""monitor 발화 + OHLC로 DD tier 캘리브레이션."""
outcomes = pd.read_csv(MATCHING_FIRE_OUTCOMES)
matched = load_matched_rules()
monitor_ids = {r["rule_id"] for r in matched.get("monitor_rules", [])}
fires = outcomes[outcomes["rule_id"].isin(monitor_ids)]
mon = Monitor(cooldown_file=None)
ohlc = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)[
MATCH_PRIMARY_INTERVAL
]
gt = load_ground_truth() or {}
mark = (gt.get("summary") or {}).get("mark_price")
result = run_and_save_calibration(
fires,
ohlc,
outcomes=outcomes,
last_price=float(mark) if mark else None,
)
best = result.get("best_params") or {}
metrics = result.get("best_metrics") or {}
print(f"[hybrid DD] 저장: {MATCHING_HYBRID_DD_CALIBRATION_JSON}")
print(
f"[hybrid DD] best dd_large={best.get('dd_large_pct')} "
f"dd_medium={best.get('dd_medium_pct')} "
f"train={metrics.get('train_pnl_pct')}% "
f"holdout={metrics.get('holdout_pnl_pct')}% "
f"full={metrics.get('full_pnl_pct')}%"
)
if __name__ == "__main__":
main()

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env python3
"""Simulation: GT 프로필 + 인과 EV 매칭 (규칙·monitor_rules)."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.matching.pipeline import main
if __name__ == "__main__":
main()

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env python3
"""Simulation: walk-forward·Go/No-Go 시뮬 리포트."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.matching.simulation import run_simulation_report
if __name__ == "__main__":
run_simulation_report()

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env python3
"""05: Ground Truth JSON → HTML 차트 (기본: JSON만 반영, --regenerate 시 02 재실행)."""
import argparse
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.ops import simulation
def main() -> None:
parser = argparse.ArgumentParser(description="Ground Truth HTML 차트")
parser.add_argument(
"--regenerate",
action="store_true",
help="DB에서 GT JSON을 다시 생성한 뒤 HTML 작성",
)
parser.add_argument(
"--no-browser",
action="store_true",
help="브라우저 자동 열기 생략",
)
args = parser.parse_args()
simulation.run_ground_truth_chart(
open_browser=not args.no_browser,
from_json=not args.regenerate,
)
if __name__ == "__main__":
main()

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env python3
"""05단계: WLD 실시간 모니터 루프 (04 규칙 알림 포함)."""
import argparse
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.ops.monitor_coin import MonitorCoin
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="WLD 모니터")
parser.add_argument("--once", action="store_true", help="1회만 출력 후 종료")
parser.add_argument("--no-rules", action="store_true", help="04 규칙 평가 생략")
args = parser.parse_args()
mon = MonitorCoin(cooldown_file=None, check_rules=not args.no_rules)
if args.once:
mon.monitor_wld()
else:
mon.run_schedule()

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env python3
"""3단계: 실거래 (monitor_rules + 빗썸 주문). LIVE_TRADING_ENABLED=1 필수."""
import argparse
import runpy
import sys
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.env_loader import ENV_FILE, env_status # noqa: E402
from config import (
GT_INITIAL_CASH_KRW,
LIVE_DAILY_KRW_MAX,
LIVE_DAILY_LOSS_LIMIT_KRW,
LIVE_MAX_TRADES_PER_DAY,
LIVE_TRADING_ENABLED,
MONITOR_LOOP_SLEEP_SEC,
)
from deepcoin.ops.live_trader import LiveTrader
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="WLD 실거래 (06)")
parser.add_argument("--once", action="store_true", help="1회만 실행")
args = parser.parse_args()
st = env_status()
print(
f"[06] 운영 설정 · LIVE=ON · "
f"초기₩{GT_INITIAL_CASH_KRW:,} · 일한도₩{LIVE_DAILY_KRW_MAX:,} · "
f"일손실₩{LIVE_DAILY_LOSS_LIMIT_KRW:,} · max_trades={LIVE_MAX_TRADES_PER_DAY}"
)
print(
f"[06] .env 로더={st.get('loader')} · 파일={ENV_FILE.name} · "
f"LIVE_TRADING_ENABLED(raw)={st.get('live_trading_enabled')!r}"
)
if st.get("loader") == "fallback":
print(
"주의: python-dotenv 미설치 — fallback 파서 사용. "
"권장: pip install python-dotenv (requirements.txt)"
)
if not LIVE_TRADING_ENABLED:
print(
"오류: LIVE_TRADING_ENABLED=0 — dry-run은 제거되었습니다. "
".env 에 LIVE_TRADING_ENABLED=1 설정 후 재실행하세요."
)
sys.exit(1)
trader = LiveTrader()
if args.once:
trader.run_once()
else:
trader.run_loop(MONITOR_LOOP_SLEEP_SEC)

View File

@@ -1,345 +0,0 @@
#!/usr/bin/env python3
"""실거래(06) 기동 전 설정·hybrid tier·규칙·한도 점검. LIVE_TRADING_ENABLED=1 필수."""
from __future__ import annotations
import argparse
import runpy
from datetime import datetime
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from config import ( # noqa: E402
CHART_LOOKBACK_DAYS,
GT_INITIAL_CASH_KRW,
GT_SIGNAL_CAUSAL,
LIVE_HYBRID_BOOTSTRAP_FIRES,
LIVE_COOLDOWN_MIN,
LIVE_DAILY_KRW_MAX,
LIVE_DAILY_LOSS_LIMIT_KRW,
LIVE_MAX_TRADES_PER_DAY,
LIVE_ORDER_KRW,
LIVE_TRADING_ENABLED,
MATCH_LIVE_CACHE_SEC,
MATCH_PRIMARY_INTERVAL,
MONITOR_ALERT_KRW_AMOUNT,
MONITOR_LOOP_SLEEP_SEC,
SIM_PRIMARY_SIZING,
SYMBOL,
TRADING_FEE_RATE,
)
from deepcoin.data.mtf_bb import load_frames_from_db
from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
from deepcoin.matching.live_eval import evaluate_live_rules
from deepcoin.ground_truth.causal_gt_hybrid import _close_series_from_df, hybrid_tier_scale
from deepcoin.ground_truth.gt_model import leg_entry_weights
from deepcoin.matching.position_sizing import compute_buy_amount_krw
from deepcoin.matching.load_rules import load_monitor_rules
from deepcoin.ops.hybrid_sim_execution import (
bootstrap_monitor_signals_from_outcomes,
build_live_signal_history,
plan_live_hit,
)
from deepcoin.ops.live_trader import LiveTrader
from deepcoin.ops.monitor import Monitor
def _plan_with_dd(
cash: float,
qty: float,
price: float,
dd_pct: float,
*,
enhanced: bool,
completed_leg_ret: dict[int, float] | None = None,
leg_id: int = 1,
) -> float:
"""drawdown %를 고정한 tier 매수 원화 (검증용)."""
weights = leg_entry_weights([price])
trade: dict = {"leg_id": leg_id, "drawdown_pct": dd_pct}
dd_params = load_hybrid_dd_params()
scale = hybrid_tier_scale(
trade,
completed_leg_ret=completed_leg_ret or {},
enhanced=enhanced,
dd_large_pct=dd_params.get("dd_large_pct"),
dd_medium_pct=dd_params.get("dd_medium_pct"),
)
return compute_buy_amount_krw(
cash,
qty,
price,
weights[0],
weights[0],
asset_pct_scale=scale,
fee_rate=TRADING_FEE_RATE,
ignore_weight_split=bool(trade.get("conviction_buy")),
)
def _print_header(title: str) -> None:
print(f"\n=== {title} ===")
def check_config() -> list[str]:
"""필수 설정 확인. 문제 목록 반환."""
issues: list[str] = []
_print_header("1. 설정")
print(f" SYMBOL={SYMBOL}")
print(f" GT_SIGNAL_CAUSAL={GT_SIGNAL_CAUSAL}")
print(f" LIVE_HYBRID_BOOTSTRAP_FIRES={LIVE_HYBRID_BOOTSTRAP_FIRES}")
print(f" LIVE_TRADING_ENABLED={LIVE_TRADING_ENABLED}")
print(f" SIM_PRIMARY_SIZING={SIM_PRIMARY_SIZING}")
dd = load_hybrid_dd_params()
print(f" hybrid DD: large={dd.get('dd_large_pct')}% medium={dd.get('dd_medium_pct')}%")
print(
f" LIVE 한도: daily_max={LIVE_DAILY_KRW_MAX:,} "
f"max_trades={LIVE_MAX_TRADES_PER_DAY} "
f"loss_limit={LIVE_DAILY_LOSS_LIMIT_KRW:,} "
f"cooldown={LIVE_COOLDOWN_MIN}min"
)
print(
f" 06 루프: sleep={MONITOR_LOOP_SLEEP_SEC}s · "
f"live_eval_cache={MATCH_LIVE_CACHE_SEC}s · bar={MATCH_PRIMARY_INTERVAL}m"
)
print(" 체결 엔진: sim_causal_hybrid (hybrid_sim_execution)")
print(f" GT_INITIAL_CASH_KRW=₩{GT_INITIAL_CASH_KRW:,}")
rules = load_monitor_rules()
print(f" monitor_rules={[r['rule_id'] for r in rules]}")
if not LIVE_TRADING_ENABLED:
issues.append("LIVE_TRADING_ENABLED=0 — 실거래 기동 불가")
if not GT_SIGNAL_CAUSAL:
issues.append("GT_SIGNAL_CAUSAL=0 — 인과 GT·hybrid 정합 권장(1)")
if not LIVE_HYBRID_BOOTSTRAP_FIRES:
issues.append("LIVE_HYBRID_BOOTSTRAP_FIRES=0 — 시뮬 이력 부트스트랩 꺼짐")
if len(rules) != 2:
issues.append(f"monitor_rules {len(rules)}개 (기대 2)")
expected = {"buy_compound_tight", "sell_mtf_cross_all_tf"}
got = {r["rule_id"] for r in rules}
if got != expected:
issues.append(f"monitor_rules 불일치: {got}")
return issues
def check_capital_alignment() -> list[str]:
"""초기 자금 40만 원 기준 원화 한도·알림 비율 점검."""
issues: list[str] = []
_print_header("1b. 초기 자금·비율 (운영)")
ic = int(GT_INITIAL_CASH_KRW)
expected: dict[str, int] = {
"GT_INITIAL_CASH_KRW": 400_000,
"MONITOR_ALERT_KRW_AMOUNT": int(ic * 0.10),
"LIVE_ORDER_KRW": int(ic * 0.10),
"LIVE_DAILY_LOSS_LIMIT_KRW": int(ic * 0.10),
"LIVE_DAILY_KRW_MAX": ic,
"LIVE_MAX_TRADES_PER_DAY": 15,
}
actual = {
"GT_INITIAL_CASH_KRW": ic,
"MONITOR_ALERT_KRW_AMOUNT": int(MONITOR_ALERT_KRW_AMOUNT),
"LIVE_ORDER_KRW": int(LIVE_ORDER_KRW),
"LIVE_DAILY_LOSS_LIMIT_KRW": int(LIVE_DAILY_LOSS_LIMIT_KRW),
"LIVE_DAILY_KRW_MAX": int(LIVE_DAILY_KRW_MAX),
"LIVE_MAX_TRADES_PER_DAY": int(LIVE_MAX_TRADES_PER_DAY),
}
for key, exp in expected.items():
got = actual[key]
ok = got == exp
mark = "OK" if ok else "WARN"
print(f" [{mark}] {key}={got:,} (기대 {exp:,})")
if not ok:
issues.append(f"{key}={got:,} ≠ 기대 {exp:,}")
return issues
def check_tier_sizing(df) -> list[str]:
"""hybrid vs conviction tier 금액 비교 (enhanced=False가 primary)."""
issues: list[str] = []
_print_header("2. hybrid tier 사이징 (시나리오)")
price = 487.0
ic = int(GT_INITIAL_CASH_KRW)
scenarios = [
("신규·소형DD(1%)", ic, 0.0, 1.0, {}),
("신규·대형DD(6%)", ic, 0.0, 6.0, {}),
("복리·과거leg+", ic * 2, ic / price * 0.5, 3.0, {0: 12.0}),
]
for label, cash, qty, dd, completed in scenarios:
hybrid_amt = _plan_with_dd(
cash, qty, price, dd, enhanced=False, completed_leg_ret=completed
)
conv_amt = _plan_with_dd(
cash, qty, price, dd, enhanced=True, completed_leg_ret=completed
)
print(
f" [{label}] cash={cash:,} hybrid={hybrid_amt:,} "
f"conviction={conv_amt:,} (conviction 배포 금지)"
)
if hybrid_amt <= 0:
issues.append(f"hybrid 금액 0: {label}")
return issues
def check_hybrid_bootstrap(df) -> list[str]:
"""fire_outcomes 부트스트랩 건수·plan_live_hit 동작."""
issues: list[str] = []
_print_header("2b. hybrid 부트스트랩 (sim_causal_hybrid)")
boot = bootstrap_monitor_signals_from_outcomes()
print(f" fire_outcomes monitor 발화: {len(boot)}")
if LIVE_HYBRID_BOOTSTRAP_FIRES and len(boot) < 100:
issues.append(f"bootstrap 발화 {len(boot)}건 — 04_match_rules 재실행 필요")
if df is None or df.empty or not boot:
return issues
hist = build_live_signal_history([])
last_buy = [s for s in boot if s["side"] == "buy"][-1:]
if last_buy:
plan = plan_live_hit(hist, last_buy[0], df, approved_buy_rules=None)
print(
f" 최근 매수 샘플 {last_buy[0]['dt']}: "
f"ok={plan.ok}{plan.amount_krw:,.0f} ({plan.message})"
)
return issues
def check_live_limits() -> None:
"""실거래 일한도 vs hybrid tier (plan_live_hit)."""
_print_header("3. 실거래 한도 vs hybrid tier")
mon = Monitor(cooldown_file=None)
frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
df = frames.get(MATCH_PRIMARY_INTERVAL)
price = 487.0
if df is not None and not df.empty:
close_s = _close_series_from_df(df)
if not close_s.empty:
price = float(close_s.iloc[-1])
hist = build_live_signal_history([])
dt = str(df.index[-1]) if df is not None and not df.empty else "2026-06-01 12:00:00"
planned = plan_live_hit(
hist,
{
"dt": dt,
"rule_id": "buy_compound_tight",
"side": "buy",
"close": price,
},
df,
approved_buy_rules=None,
).amount_krw
trader = LiveTrader()
ok, reason = trader._can_trade("buy_compound_tight", planned)
print(f" 현재가={price:,.0f} · hybrid planned={planned:,}")
print(f" LIVE_DAILY_KRW_MAX={LIVE_DAILY_KRW_MAX:,} → _can_trade={ok} ({reason or 'OK'})")
if planned > LIVE_DAILY_KRW_MAX:
print(
" WARN: hybrid 1회 매수액이 일한도 초과 가능 — "
"시뮬 대비 실거래 체결액이 작아질 수 있음"
)
def check_live_eval() -> None:
"""현재 시점 규칙 발화."""
_print_header("4. 현재 발화 (live_eval)")
fired = evaluate_live_rules(force_refresh=True)
if not fired:
print(" 발화 없음 (정상 — 신호 대기)")
return
for hit in fired:
print(f" {hit['side']} {hit['rule_id']} @ {hit['dt']} close={hit['close']}")
def run_live_once() -> None:
"""06 1회 실거래 루프 (발화 시 주문)."""
_print_header("5. 06_execute_live --once")
LiveTrader().run_once()
def write_verification_report(issues: list[str], out_path: Path) -> None:
"""운영 점검 결과를 docs/05_ops에 기록."""
out_path.parent.mkdir(parents=True, exist_ok=True)
status = "PASS" if not issues else "WARN"
lines = [
"# Live 운영 점검",
"",
f"- 일시: {datetime.now():%Y-%m-%d %H:%M:%S}",
f"- 결과: **{status}**",
"",
"## Check",
"",
f"- GT_SIGNAL_CAUSAL={GT_SIGNAL_CAUSAL}",
f"- LIVE_TRADING_ENABLED={LIVE_TRADING_ENABLED}",
"- monitor_rules: buy_compound_tight, sell_mtf_cross_all_tf",
f"- hybrid DD: {load_hybrid_dd_params()}",
"",
]
if issues:
lines.append("### 이슈")
lines.append("")
for i in issues:
lines.append(f"- {i}")
lines.append("")
lines.extend(
[
"## Act",
"",
"```bash",
"python scripts/06_execute_live.py",
"```",
"",
"## Kill switch",
"",
"- 06 프로세스 중지",
"- 빗썸 앱 수동 청산",
"",
]
)
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"\n[저장] {out_path}")
def main() -> int:
"""실거래 기동 전 점검."""
parser = argparse.ArgumentParser(description="06 실거래 점검")
parser.add_argument(
"--once",
action="store_true",
help="점검 후 06_execute_live 1회 실행 (실주문 가능)",
)
args = parser.parse_args()
print("[06_verify] 실거래 점검 시작")
if not LIVE_TRADING_ENABLED:
print("오류: LIVE_TRADING_ENABLED=0 — .env 에 1 설정 필요")
return 1
issues = check_config()
issues.extend(check_capital_alignment())
mon = Monitor(cooldown_file=None)
frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
df = frames.get(MATCH_PRIMARY_INTERVAL)
if df is None or df.empty:
issues.append("3m OHLC 없음 — 01_download 필요")
else:
issues.extend(check_tier_sizing(df))
issues.extend(check_hybrid_bootstrap(df))
check_live_limits()
check_live_eval()
if args.once:
run_live_once()
out = (
Path(__file__).resolve().parents[1]
/ "docs"
/ "05_ops"
/ f"live_verification_{datetime.now():%Y%m%d}.md"
)
write_verification_report(issues, out)
_print_header("요약")
if issues:
for i in issues:
print(f" WARN: {i}")
return 1
print(" 운영 설정 PASS — 06_execute_live 기동 가능")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,82 +0,0 @@
#!/usr/bin/env python3
"""
매일 지정 시각(기본 19:00 KST) 최근 24시간 수익률 텔레그램 발송.
사용:
python scripts/07_daily_pnl_telegram.py --once # 즉시 1회 발송
python scripts/07_daily_pnl_telegram.py # 스케줄 루프 (상시)
python scripts/07_daily_pnl_telegram.py --record # 스냅샷만 기록
Windows 작업 스케줄러: 매일 19:00에 --once 실행도 가능.
"""
from __future__ import annotations
import argparse
import runpy
import sys
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from config import ( # noqa: E402
DAILY_PNL_REPORT_ENABLED,
DAILY_PNL_REPORT_HOUR,
DAILY_PNL_REPORT_MINUTE,
DAILY_PNL_REPORT_TZ,
)
from deepcoin.ops.monitor import Monitor # noqa: E402
from deepcoin.ops.portfolio_report import ( # noqa: E402
append_portfolio_snapshot,
fetch_portfolio_snapshot,
run_schedule_loop,
send_daily_pnl_report,
)
def main() -> int:
"""
CLI 진입점.
Returns:
0 성공, 1 비활성/오류.
"""
parser = argparse.ArgumentParser(description="일일 24h 수익률 텔레그램")
parser.add_argument(
"--once",
action="store_true",
help="즉시 1회 발송 후 종료",
)
parser.add_argument(
"--record",
action="store_true",
help="스냅샷만 기록 (텔레그램 없음)",
)
args = parser.parse_args()
if not DAILY_PNL_REPORT_ENABLED:
print("DAILY_PNL_REPORT_ENABLED=0 — 종료")
return 1
print(
f"[07] DAILY_PNL · {DAILY_PNL_REPORT_HOUR:02d}:"
f"{DAILY_PNL_REPORT_MINUTE:02d} {DAILY_PNL_REPORT_TZ}"
)
mon = Monitor(cooldown_file=None)
if args.record:
snap = fetch_portfolio_snapshot(mon)
append_portfolio_snapshot(snap)
print(f"[07] snapshot total=₩{snap['total_asset_krw']:,.0f}")
return 0
if args.once:
send_daily_pnl_report(mon)
return 0
run_schedule_loop(mon)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,55 +0,0 @@
# scripts — CLI (Ground Truth · Simulation · Operations)
`_bootstrap.py``.env`를 로드합니다. 설계: [docs/reference/ARCHITECTURE.md](../docs/reference/ARCHITECTURE.md)
## 데이터 (공통)
| 스크립트 | 설명 |
|----------|------|
| `01_download.py` | OHLCV 적재 |
| `00_sync_ops.py` | 운영 전 DB 동기화 |
## Ground Truth
| 스크립트 | 설명 |
|----------|------|
| `02_ground_truth.py` | 정답 타점 JSON |
| `03_analyze_enrich.py` | 10TF 지표 enrich |
| `03_analyze_trades.py` | GT 타점 MTF 스냅샷 CSV |
| `03_gt_mtf_profile.py` | GT MTF 프로필 |
| `05_chart_truth.py` | GT HTML 차트 |
## Simulation
| 스크립트 | 설명 |
|----------|------|
| `04_match_rules.py` | 규칙·`matched_rules.json` |
| `04_simulation_report.py` | Go/No-Go 리포트 |
| `04_hybrid_dd_calibrate.py` | hybrid DD tier (필요 시) |
| `04_causal_gt_calibrate.py` | 인과 GT leg (필요 시) |
## Operations
| 스크립트 | 설명 |
|----------|------|
| `06_verify_live.py` | 기동 전 점검 |
| `check_balance.py` | 빗썸 잔고 |
| `06_execute_live.py` | 실거래 |
| `05_run_monitor.py` | 알림만 (선택) |
| `07_daily_pnl_telegram.py` | 매일 19:00 24h 수익률 텔레그램 (`--once` 즉시) |
## 검증
| 스크립트 | 설명 |
|----------|------|
| `verify_env.py` | `.env` 검증 |
| `test_buy_sell_rehearsal.py` | hybrid 체결 제약 |
## 재구축
```bash
python scripts/01_download.py
python scripts/02_ground_truth.py
python scripts/03_analyze_enrich.py && python scripts/03_analyze_trades.py && python scripts/03_gt_mtf_profile.py
python scripts/04_match_rules.py && python scripts/04_simulation_report.py
```

View File

@@ -1,12 +0,0 @@
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
# .env → config 일관 로드
from deepcoin.env_loader import load_project_env # noqa: E402
# 프로젝트 .env가 OS에 남은 LIVE_* 등보다 우선 (실거래 설정 일치)
load_project_env(override=True)

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env python3
"""빗썸 실계좌 잔고 조회 (운영 점검용)."""
from __future__ import annotations
import runpy
import sys
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from config import GT_INITIAL_CASH_KRW, LIVE_TRADING_ENABLED, SYMBOL # noqa: E402
from deepcoin.ops.monitor import Monitor # noqa: E402
def main() -> int:
"""
KRW·거래 심볼 잔고를 출력합니다.
Returns:
0 성공, 1 API 오류.
"""
print(f"LIVE_TRADING_ENABLED={int(LIVE_TRADING_ENABLED)}")
print(f"GT_INITIAL_CASH_KRW=₩{GT_INITIAL_CASH_KRW:,}")
m = Monitor(cooldown_file=None)
raw = m.getBalances()
if isinstance(raw, dict) and raw.get("error"):
print("API_ERROR", raw.get("error"))
return 1
if not isinstance(raw, list):
print("UNEXPECTED_RESPONSE", type(raw))
return 1
krw = next((x for x in raw if x.get("currency") == "KRW"), None)
coin = next((x for x in raw if x.get("currency") == SYMBOL), None)
if krw:
bal = float(krw["balance"])
print(f"KRW available=₩{bal:,.0f}")
if bal < float(GT_INITIAL_CASH_KRW):
print(f" [WARN] 가용 KRW < 초기자금 ₩{GT_INITIAL_CASH_KRW:,}")
if coin:
print(f"{SYMBOL} qty={float(coin['balance']):.6f}")
else:
print(f"{SYMBOL} qty=0")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""빗썸 WLD(또는 SYMBOL) 최근 N일 캔들 수집 스크립트."""
from __future__ import annotations
import argparse
import logging
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 dataclasses import replace
from deepcoin.config import load_settings
from deepcoin.data.candle_store import CandleStore
from deepcoin.data.downloader import CandleDownloader
from deepcoin.data.intervals import interval_label
def _configure_logging(verbose: bool) -> None:
"""로깅 레벨을 설정한다."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def main() -> int:
"""CLI 진입점."""
parser = argparse.ArgumentParser(description="빗썸 캔들 데이터 수집")
parser.add_argument(
"--days",
type=int,
default=None,
help="수집 일수 (기본: .env DOWNLOAD_DAYS 또는 730)",
)
parser.add_argument(
"--intervals",
type=str,
default=None,
help="쉼표 구분 인터벌. 분봉=분, 일=1440, 주=10080, 월=43200",
)
parser.add_argument("-v", "--verbose", action="store_true", help="디버그 로그")
args = parser.parse_args()
_configure_logging(args.verbose)
settings = load_settings()
if args.intervals:
settings = replace(
settings,
download_intervals=[
int(x.strip()) for x in args.intervals.split(",") if x.strip()
],
)
days = args.days or settings.download_days
logging.getLogger(__name__).info(
"대상=%s DB=%s days=%s intervals=%s",
settings.market,
settings.db_path,
days,
settings.download_intervals,
)
store = CandleStore(settings.db_path)
try:
downloader = CandleDownloader(settings)
results = downloader.download_all(store, days=days)
print("\n=== 수집 완료 ===")
for result in results:
count, min_dt, max_dt = store.get_range(settings.symbol, result.interval_min)
min_s = min_dt.strftime("%Y-%m-%d %H:%M:%S") if min_dt else "-"
max_s = max_dt.strftime("%Y-%m-%d %H:%M:%S") if max_dt else "-"
flag = "OK" if result.reached_target else "PARTIAL"
label = interval_label(result.interval_min)
print(
f"[{flag}] {label} ({result.interval_min}) | "
f"requests={result.requests} upsert={result.saved_rows} "
f"db_rows={count} range={min_s} ~ {max_s}"
)
finally:
store.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,194 +0,0 @@
#!/usr/bin/env python3
"""
40만 원 기준 매수·매도 리허설 (DB 없이 synthetic + hybrid replay).
"""
from __future__ import annotations
import runpy
import sys
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
import pandas as pd
from config import GT_INITIAL_CASH_KRW, TRADING_FEE_RATE
from deepcoin.ops.hybrid_sim_execution import (
HybridSimPortfolio,
hit_key,
plan_live_hit,
replay_hybrid_signals,
sort_hits_sim_order,
)
def _mini_ohlc() -> pd.DataFrame:
"""drawdown 계산용 최소 3m OHLC."""
idx = pd.date_range("2026-06-01 09:00:00", periods=200, freq="3min")
close = pd.Series([500.0 - i * 0.1 for i in range(200)], index=idx, dtype=float)
return pd.DataFrame(
{"Open": close, "High": close + 2, "Low": close - 2, "Close": close},
index=idx,
)
def test_sort_buy_before_sell() -> None:
"""동일 시각 buy·sell → buy 먼저."""
hits = [
{"dt": "2026-06-01 12:00:00", "rule_id": "sell_mtf_cross_all_tf", "side": "sell", "close": 500.0},
{"dt": "2026-06-01 12:00:00", "rule_id": "buy_compound_tight", "side": "buy", "close": 500.0},
]
ordered = sort_hits_sim_order(hits)
assert ordered[0]["side"] == "buy", ordered
print(" [OK] 동일 시각 buy → sell 순서")
def test_sell_without_holdings() -> None:
"""보유 없이 매도만 → 모의 보유 없음."""
ohlc = _mini_ohlc()
approved = None
hist = [
{
"dt": "2026-06-01 12:00:00",
"rule_id": "sell_mtf_cross_all_tf",
"side": "sell",
"close": 500.0,
}
]
portfolio, results = replay_hybrid_signals(hist, ohlc, approved_buy_rules=approved)
key = hit_key(hist[0])
res = results[key]
assert not res.ok and "보유 없음" in res.message, res
assert portfolio.qty < 1e-9 and portfolio.cash_krw == float(GT_INITIAL_CASH_KRW)
print(" [OK] 보유 없음 매도 스킵")
def test_buy_then_partial_sell() -> None:
"""매수 후 분할 매도 1회."""
ohlc = _mini_ohlc()
approved = None
dt_buy = str(ohlc.index[50])
dt_sell = str(ohlc.index[80])
price_buy = float(ohlc.loc[ohlc.index[50], "Close"])
price_sell = float(ohlc.loc[ohlc.index[80], "Close"])
hist = [
{
"dt": dt_buy,
"rule_id": "buy_compound_tight",
"side": "buy",
"close": price_buy,
},
{
"dt": dt_sell,
"rule_id": "sell_mtf_cross_all_tf",
"side": "sell",
"close": price_sell,
},
]
portfolio, results = replay_hybrid_signals(hist, ohlc, approved_buy_rules=approved)
buy_res = results[hit_key(hist[0])]
sell_res = results[hit_key(hist[1])]
assert buy_res.ok, buy_res.message
assert portfolio.qty > 0 or sell_res.ok, (portfolio.qty, sell_res)
if sell_res.ok:
assert sell_res.sell_qty > 0 and sell_res.amount_krw > 0
assert portfolio.cash_krw > float(GT_INITIAL_CASH_KRW) * 0.5
print(
f" [OK] 매수 ₩{buy_res.amount_krw:,.0f} → 매도 "
f"ok={sell_res.ok} qty={sell_res.sell_qty:.4f} 현금=₩{portfolio.cash_krw:,.0f}"
)
def test_unapproved_buy_excluded_when_filter_set() -> None:
"""approved_buy_rules 지정 시에만 미포함 매수 제외 (시뮬 기본은 필터 없음)."""
ohlc = _mini_ohlc()
hist = [
{
"dt": str(ohlc.index[60]),
"rule_id": "buy_fake_rule",
"side": "buy",
"close": 500.0,
},
]
approved = {"buy_compound_tight"}
sized_hist = replay_hybrid_signals(hist, ohlc, approved_buy_rules=approved)[0]
assert sized_hist.qty < 1e-9
print(" [OK] approved_buy_rules 지정 시 미승인 매수 제외")
def test_plan_live_matches_replay() -> None:
"""plan_live_hit == replay 마지막 건."""
ohlc = _mini_ohlc()
approved = None
hist = []
hit = {
"dt": str(ohlc.index[70]),
"rule_id": "buy_compound_tight",
"side": "buy",
"close": float(ohlc["Close"].iloc[70]),
}
plan = plan_live_hit(hist, hit, ohlc, approved_buy_rules=approved)
hist.append(hit)
_, results = replay_hybrid_signals(hist, ohlc, approved_buy_rules=approved)
replay_res = results[hit_key(hit)]
assert plan.amount_krw == replay_res.amount_krw, (plan, replay_res)
assert plan.ok == replay_res.ok
print(f" [OK] plan_live_hit ≡ replay (₩{plan.amount_krw:,.0f})")
def test_initial_cash_400k_large_buy() -> None:
"""40만·대형 DD 시 매수액 ≤ 가용현금."""
ohlc = _mini_ohlc()
approved = None
hit = {
"dt": str(ohlc.index[100]),
"rule_id": "buy_compound_tight",
"side": "buy",
"close": float(ohlc["Close"].iloc[100]),
}
plan = plan_live_hit([], hit, ohlc, approved_buy_rules=approved)
assert plan.ok
assert 0 < plan.amount_krw <= GT_INITIAL_CASH_KRW
fee = plan.amount_krw * TRADING_FEE_RATE
assert plan.amount_krw + fee <= GT_INITIAL_CASH_KRW + 1
print(f" [OK] 40만 대형 tier 매수 ₩{plan.amount_krw:,.0f} (≤{GT_INITIAL_CASH_KRW:,})")
def test_apply_buy_insufficient_cash() -> None:
"""현금 부족 시 apply_buy 실패."""
p = HybridSimPortfolio()
p.cash_krw = 10_000.0
ok = p.apply_buy(50_000, 500.0, leg_id=1)
assert not ok
print(" [OK] 현금 부족 매수 거부")
def main() -> int:
"""리허설 실행."""
print(f"[리허설] GT_INITIAL_CASH_KRW=₩{GT_INITIAL_CASH_KRW:,}")
print(" approved_buy_rules: None (sim_causal_hybrid 동일)")
fails = 0
tests = [
test_sort_buy_before_sell,
test_sell_without_holdings,
test_buy_then_partial_sell,
test_unapproved_buy_excluded_when_filter_set,
test_plan_live_matches_replay,
test_initial_cash_400k_large_buy,
test_apply_buy_insufficient_cash,
]
for fn in tests:
try:
fn()
except AssertionError as e:
print(f" [FAIL] {fn.__name__}: {e}")
fails += 1
except Exception as e:
print(f" [ERROR] {fn.__name__}: {e}")
fails += 1
print(f"\n[결과] {'PASS' if fails == 0 else f'FAIL ({fails})'}")
return 1 if fails else 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,218 +0,0 @@
#!/usr/bin/env python3
"""`.env` 완전성 및 config 로드 점검."""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
ENV_FILE = ROOT / ".env"
CONFIG_FILE = ROOT / "config.py"
# config.py에서 읽는 환경 변수 키 (코드 기본값이 있는 항목)
CONFIG_ENV_KEYS = {
"BITHUMB_API_URL",
"BITHUMB_API_CANDLE_COUNT",
"BITHUMB_MINUTE_INTERVALS",
"HTS_API_RETRY_SLEEP_SEC",
"SYMBOL",
"COIN_NAME",
"DAILY_INTERVAL_MIN",
"ENTRY_INTERVAL",
"TREND_INTERVAL_1H",
"TREND_INTERVAL_1D",
"ALL_INTERVALS",
"DOWNLOAD_INTERVALS",
"GENERAL_ANALYSIS_INTERVALS",
"TIMING_INTERVALS",
"TREND_INTERVALS",
"INTERVAL_PREFIX",
"BB_PERIOD",
"BB_STD",
"BB_MIN_WIDTH_PCT",
"RSI_PERIOD",
"DISPARITY_PERIODS",
"DISPARITY_OVERBOUGHT",
"DISPARITY_OVERSOLD",
"MACD_FAST",
"MACD_SLOW",
"MACD_SIGNAL",
"STOCH_K_PERIOD",
"STOCH_D_PERIOD",
"STOCH_SMOOTH_K",
"STOCH_OVERSOLD",
"STOCH_OVERBOUGHT",
"TREND_RANGE_MA_GAP_PCT",
"ALIGN_RSI_OVERSOLD",
"ALIGN_RSI_OVERBOUGHT",
"ALIGN_RSI_CONFLICT_TIMING_LOW",
"ALIGN_RSI_CONFLICT_TIMING_HIGH",
"ALIGN_RSI_CONFLICT_TREND_LOW",
"ALIGN_RSI_CONFLICT_TREND_HIGH",
"ALIGN_BB_POS_LOW",
"ALIGN_BB_POS_HIGH",
"DOWNLOAD_MONTHS",
"INCREMENTAL_OVERLAP_BARS",
"DOWNLOAD_BACKFILL_EXTRA_BARS",
"DOWNLOAD_MIN_INCREMENTAL_BARS",
"DOWNLOAD_DAILY_EXTRA_DAYS",
"CHART_LOOKBACK_DAYS",
"DB_READ_LIMIT_DEFAULT",
"DB_ROW_WARMUP_BARS",
"DB_ROW_MIN_DAILY_BARS",
"DB_ROW_DAILY_PADDING_DAYS",
"DB_PATH",
"GROUND_TRUTH_FILE",
"GT_UNLIMITED_CHRONOLOGICAL_DAYS",
"GT_MIN_SWING_PCT",
"GT_PIVOT_ORDER",
"GT_MIN_BARS_BETWEEN",
"GT_MAX_ROUND_TRIPS",
"GT_SELECTION_MODE",
"GT_MIN_LEG_PCT",
"GT_BUY_MIN_SWING_PCT",
"GT_BUY_BB_MAX",
"GT_BUY_MIN_BARS",
"GT_MAX_BUYS_PER_LEG",
"GT_MAX_SELLS_PER_LEG",
"GT_SELL_SPLIT_GAP_PCT",
"GT_MARKER_SIZE_MIN",
"GT_MARKER_SIZE_MAX",
"GT_INITIAL_CASH_KRW",
"TRADING_FEE_RATE",
"MONITOR_LOOP_SLEEP_SEC",
"MONITOR_POOL_WORKERS",
"MONITOR_DEFAULT_INTERVAL",
"MONITOR_API_RETRIES",
"MONITOR_API_BONG_COUNT",
"MONITOR_SLEEP_AFTER_REQUEST_SEC",
"MONITOR_SLEEP_RATE_LIMIT_SEC",
"MONITOR_SLEEP_BETWEEN_CHUNKS_SEC",
"MONITOR_API_CHUNK_BARS",
"MONITOR_MA_WINDOWS",
"MONITOR_NORM_WINDOW",
"MONITOR_TELEGRAM_BATCH_SIZE",
"GA_COL_PREFIX",
"LOOKBACK_BARS",
"CONTEXT_TAIL_ROWS",
"GA_DEFAULT_TAIL_EXPORT",
"GA_PATTERN_TOLERANCE_PCT",
"GA_VP_BINS",
"GA_VP_VALUE_AREA_PCT",
"GA_HV_ROLLING_BARS",
"GA_HV_PERCENTILE_WINDOW",
"GA_HV_ANNUALIZE_SQRT",
"GA_DIVERGENCE_LOOKBACK",
"GA_SMA_PERIODS",
"GA_EMA_SPANS",
"GA_ATR_PERIOD",
"GA_KELTNER_ATR_MULT",
"GA_AO_FAST",
"GA_AO_SLOW",
"GA_LINREG_WINDOW",
"GA_ADX_PERIOD",
"GA_ADX_TREND_THRESHOLD",
"GA_SUPERTREND_ATR_MULT",
"GA_VOL_SPIKE_MULT",
"GA_VOL_MA_WINDOW",
"GA_CCI_PERIOD",
"GA_WILLIAMS_PERIOD",
"GA_ROC_PERIOD",
"GA_MFI_PERIOD",
"GA_CMF_PERIOD",
"GA_DONCHIAN_PERIOD",
"GA_BB_SQUEEZE_WINDOW",
"GA_BB_SQUEEZE_QUANTILE",
"GA_PIVOT_ORDER",
"GA_PSAR_AF_START",
"GA_PSAR_AF_STEP",
"GA_PSAR_AF_MAX",
}
# 비어 있어도 되는 선택 항목 (현재는 모두 채움)
OPTIONAL_EMPTY = frozenset()
def parse_env_file(path: Path) -> dict[str, str]:
"""`.env` 키=값 파싱."""
out: dict[str, str] = {}
if not path.is_file():
return out
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, _, val = line.partition("=")
out[key.strip()] = val.strip()
return out
def main() -> int:
errors: list[str] = []
if not ENV_FILE.is_file():
errors.append(f".env 없음: {ENV_FILE}")
for e in errors:
print(f"FAIL: {e}")
return 1
env_vars = parse_env_file(ENV_FILE)
empty = [k for k, v in env_vars.items() if v == "" and k not in OPTIONAL_EMPTY]
missing = sorted(CONFIG_ENV_KEYS - set(env_vars.keys()))
extra = sorted(set(env_vars.keys()) - CONFIG_ENV_KEYS - {
"BITHUMB_ACCESS_KEY",
"BITHUMB_SECRET_KEY",
"COIN_TELEGRAM_BOT_TOKEN",
"COIN_TELEGRAM_CHAT_ID",
})
if empty:
errors.append(f"빈 값: {', '.join(empty)}")
if missing:
errors.append(f"config 대비 .env 누락: {', '.join(missing)}")
if extra:
print(f"WARN: config 미사용 .env 키: {', '.join(extra)}")
from deepcoin.env_loader import env_status, load_project_env
loaded = load_project_env(override=True)
if not loaded:
errors.append("load_project_env: .env 로드 실패 (python-dotenv 확인)")
import config # noqa: E402
checks = [
("SYMBOL", config.SYMBOL, env_vars.get("SYMBOL")),
("DB_PATH", config.DB_PATH, env_vars.get("DB_PATH")),
("BITHUMB_ACCESS_KEY", bool(config.BITHUMB_ACCESS_KEY), bool(env_vars.get("BITHUMB_ACCESS_KEY"))),
("LOOKBACK_BARS[3]", config.LOOKBACK_BARS.get(3), 120),
]
for name, got, expected in checks:
if got != expected and expected is not None:
errors.append(f"config 불일치 {name}: got={got!r} expected={expected!r}")
status = env_status()
print("env_status:", status)
print(f".env 키 수: {len(env_vars)} (config 필수 {len(CONFIG_ENV_KEYS)})")
print(f"SYMBOL={config.SYMBOL} DB_PATH={config.DB_PATH}")
print(f"BITHUMB_KEY set={bool(config.BITHUMB_ACCESS_KEY)} TELEGRAM set={bool(config.COIN_TELEGRAM_BOT_TOKEN)}")
if errors:
print("\n=== 점검 실패 ===")
for e in errors:
print(f" - {e}")
return 1
print("\n=== 점검 통과: .env → config 로드 정상 ===")
return 0
if __name__ == "__main__":
raise SystemExit(main())