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:
@@ -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)
|
||||
@@ -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__")
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
137
scripts/03_run_techniques.py
Normal file
137
scripts/03_run_techniques.py
Normal 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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
96
scripts/download_candles.py
Normal file
96
scripts/download_candles.py
Normal 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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user