diff --git a/.env b/.env index 7d9f5d5..d0b33e5 100644 --- a/.env +++ b/.env @@ -176,14 +176,20 @@ SIM_FEE_STRESS_MULT=2.0 # 3분봉(MATCH_PRIMARY_INTERVAL=3) — 규칙·알림 쿨다운 1봉 MONITOR_ALERT_COOLDOWN_MIN=3 MONITOR_ALERT_KRW_AMOUNT=40000 -# Phase C: dry-run (시뮬 정합 — LIVE 한도·쿨다운 사실상 무제한, paper 모드에서 06은 검사 생략) -LIVE_TRADING_ENABLED=0 -# LIVE_* 원화 한도: GT_INITIAL_CASH_KRW(40만) 대비 — 일한도 10배, 손실한도 5%, 1회참고 10% +# Phase B-1: 실거래 (06_execute_live.py 만 빗썸 주문 — 05는 알림만) +LIVE_TRADING_ENABLED=1 +# LIVE_* 원화 한도: GT_INITIAL_CASH_KRW(40만) — B-1: 일한도 1배, 손실 10%, 1회참고 10% LIVE_ORDER_KRW=40000 LIVE_BUY_PCT_LARGE=1.0 LIVE_BUY_PCT_SMALL=0.05 -LIVE_DAILY_KRW_MAX=4000000 +LIVE_DAILY_KRW_MAX=400000 LIVE_COOLDOWN_MIN=3 -LIVE_MAX_TRADES_PER_DAY=999 -LIVE_DAILY_LOSS_LIMIT_KRW=20000 +LIVE_MAX_TRADES_PER_DAY=15 +LIVE_DAILY_LOSS_LIMIT_KRW=40000 LIVE_SLIPPAGE_PCT=0.05 +# 05/06 루프 시 봉 DB 증분 · live_eval 캐시(루프 주기와 동일) +MONITOR_PERSIST_CANDLES=1 +MATCH_LIVE_CACHE_SEC=180 +# 05/06 시작·루프마다 지연 봉 자동 보완 (간격당 허용 지연 = 간격분×OPS_SYNC_MAX_LAG_BARS) +OPS_SYNC_ON_START=1 +OPS_SYNC_MAX_LAG_BARS=2 diff --git a/.env.example b/.env.example index 58fec71..ea93c89 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ # DeepCoin — .env.example (비밀값 없음). 복사: cp .env.example .env +# Python: conda activate ncue && pip install -r requirements.txt BITHUMB_ACCESS_KEY= BITHUMB_SECRET_KEY= @@ -13,6 +14,9 @@ DOWNLOAD_INTERVALS=3,5,10,15,30,60,240,1440 DOWNLOAD_MONTHS=12 # 05/06 루프마다 API 봉을 coins.db에 증분 저장 (01과 동일 append_data) MONITOR_PERSIST_CANDLES=1 +# 05/06 시작 시 누락·지연 봉 자동 증분 (scripts/00_sync_ops.py 동일) +OPS_SYNC_ON_START=1 +OPS_SYNC_MAX_LAG_BARS=2 # 02 Ground Truth · 시뮬·dry-run·live 배분 공통 초기 자금 GT_INITIAL_CASH_KRW=400000 diff --git a/.gitignore b/.gitignore index f87d18c..0e492f3 100644 --- a/.gitignore +++ b/.gitignore @@ -83,8 +83,6 @@ celerybeat-schedule # SageMath parsed files *.sage.py -# dotenv -.env # virtualenv .venv diff --git a/README.md b/README.md index 083e3c1..c9d63e5 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Ground Truth·기술적 분석·규칙 매칭·알림·**실거래(선택)**까 | 단계 | 목적 | 실행 | |------|------|------| +| 00 동기화 | 운영 전 누락 봉 보완 (05/06 자동 포함) | `python scripts/00_sync_ops.py` | | 01 데이터 | 1년치 봉 적재 | `python scripts/01_download.py` | | 02 Ground Truth | 매수·매도 정답 타점 | `python scripts/02_ground_truth.py` | | 03 분석 | 8TF 기술 지표 enrich | `python scripts/03_analyze_enrich.py` | @@ -60,13 +61,24 @@ DeepCoin/ `config.py`와 `scripts/_bootstrap.py`가 프로젝트 루트 `.env`를 `python-dotenv`로 자동 로드합니다. 새 환경에서는 팀에서 `.env`를 전달받거나 기존 로컬 파일을 복사하세요. +### Python 환경 (conda `ncue`) + ```bash +conda activate ncue pip install -r requirements.txt ``` +Windows에서 스크립트 일괄 실행: + +```powershell +.\scripts\run.ps1 01_download.py +.\scripts\run.ps1 06_execute_live.py --once +``` + ## 빠른 시작 ```bash +conda activate ncue python scripts/01_download.py python scripts/02_ground_truth.py python scripts/03_analyze_enrich.py diff --git a/config.py b/config.py index 088507c..384ffc7 100644 --- a/config.py +++ b/config.py @@ -148,6 +148,13 @@ INCREMENTAL_OVERLAP_BARS = _getenv_int("INCREMENTAL_OVERLAP_BARS", "3") DOWNLOAD_BACKFILL_EXTRA_BARS = _getenv_int("DOWNLOAD_BACKFILL_EXTRA_BARS", "200") DOWNLOAD_MIN_INCREMENTAL_BARS = _getenv_int("DOWNLOAD_MIN_INCREMENTAL_BARS", "50") DOWNLOAD_DAILY_EXTRA_DAYS = _getenv_int("DOWNLOAD_DAILY_EXTRA_DAYS", "20") +# 05/06 시작 시 누락·지연 봉 자동 증분 (01_download와 동일 저장) +OPS_SYNC_ON_START = _getenv("OPS_SYNC_ON_START", "1").strip().lower() in ( + "1", + "true", + "yes", +) +OPS_SYNC_MAX_LAG_BARS = _getenv_int("OPS_SYNC_MAX_LAG_BARS", "2") DB_READ_LIMIT_DEFAULT = _getenv_int("DB_READ_LIMIT_DEFAULT", "7000") DB_ROW_WARMUP_BARS = _getenv_int("DB_ROW_WARMUP_BARS", "200") DB_ROW_MIN_DAILY_BARS = _getenv_int("DB_ROW_MIN_DAILY_BARS", "100") diff --git a/deepcoin/data/downloader.py b/deepcoin/data/downloader.py index a39f1ef..b5d6364 100644 --- a/deepcoin/data/downloader.py +++ b/deepcoin/data/downloader.py @@ -316,13 +316,21 @@ def download_symbol( symbol: str, interval: int, months: int, + *, + verbose: bool = True, + skip_backfill: bool = False, ) -> None: - """한 간격의 봉을 API로 받아 증분·백필 저장합니다.""" + """ + 한 간격의 봉을 API로 받아 증분·백필 저장합니다. + + Args: + skip_backfill: True면 운영 증분만(백필 생략). 05/06 시작 동기화용. + """ months = months_for_interval(interval, months) label = interval_label(interval) existing = get_row_count(symbol, interval) - if existing > 0: + if existing > 0 and not skip_backfill: backfill_before_earliest(monitor, symbol, interval, months) last_ts = get_last_timestamp(symbol, interval) @@ -341,7 +349,7 @@ def download_symbol( print(f" DB 기존 {existing}행 | API 목표 약 {target}봉") data = monitor.get_coin_more_data( - symbol, interval, bong_count=target, verbose=True + symbol, interval, bong_count=target, verbose=verbose ) if data is None or data.empty: print(" -> API 데이터 없음") diff --git a/deepcoin/data/ops_sync.py b/deepcoin/data/ops_sync.py new file mode 100644 index 0000000..50d6ef3 --- /dev/null +++ b/deepcoin/data/ops_sync.py @@ -0,0 +1,191 @@ +""" +05/06 운영 시작 전 coins.db 누락 봉을 증분 보완합니다. + +마지막 저장 시각이 현재보다 OPS_SYNC_MAX_LAG_BARS 이상 뒤처진 간격만 +01_download.download_symbol 과 동일 경로로 API 수집합니다. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime + +import pandas as pd + +from config import ( + COIN_NAME, + DB_PATH, + DOWNLOAD_MONTHS, + OPS_SYNC_MAX_LAG_BARS, + OPS_SYNC_ON_START, + SYMBOL, +) +from deepcoin.data.downloader import ( + download_jobs, + download_symbol, + get_last_timestamp, + get_row_count, + interval_label, +) +from deepcoin.ops.monitor import Monitor + +# 프로세스당 1회 상세 로그(루프마다 ensure 호출 시 스팸 방지) +_logged_fresh_once: bool = False + + +@dataclass +class OpsSyncResult: + """운영 전 DB 동기화 결과.""" + + synced: list[int] = field(default_factory=list) + skipped_fresh: list[int] = field(default_factory=list) + errors: dict[int, str] = field(default_factory=dict) + disabled: bool = False + + @property + def ok(self) -> bool: + """치명적 오류 없이 완료.""" + return not self.errors or len(self.synced) > 0 or len(self.skipped_fresh) > 0 + + +def lag_minutes_since_last( + last_ts: pd.Timestamp | None, interval_minutes: int +) -> float | None: + """ + 마지막 봉 시각 대비 경과 분. + + Returns: + last_ts 없으면 None. + """ + if last_ts is None: + return None + now = pd.Timestamp.now() + last = pd.Timestamp(last_ts) + if last.tzinfo is not None and now.tzinfo is None: + last = last.tz_localize(None) + return max(0.0, (now - last).total_seconds() / 60.0) + + +def is_interval_stale( + symbol: str, + interval: int, + *, + max_lag_bars: int = OPS_SYNC_MAX_LAG_BARS, + db_path: str = DB_PATH, +) -> tuple[bool, str]: + """ + 간격별 DB가 운영에 필요한 최신성을 만족하는지 판단. + + Returns: + (stale 여부, 사유 문자열) + """ + rows = get_row_count(symbol, interval, db_path=db_path) + last = get_last_timestamp(symbol, interval, db_path=db_path) + if rows == 0 or last is None: + return True, "데이터 없음" + lag = lag_minutes_since_last(last, interval) + if lag is None: + return True, "마지막 시각 없음" + threshold = float(interval * max_lag_bars) + if lag > threshold: + return True, f"지연 {lag:.0f}분 > 허용 {threshold:.0f}분" + return False, f"최신 (마지막 {last.strftime('%Y-%m-%d %H:%M:%S')})" + + +def list_stale_intervals( + symbol: str = SYMBOL, + *, + max_lag_bars: int = OPS_SYNC_MAX_LAG_BARS, + db_path: str = DB_PATH, +) -> list[tuple[int, str, str]]: + """ + 갱신이 필요한 (interval, label, reason) 목록. + + Returns: + download_jobs 순서와 동일하게 stale 항목만. + """ + out: list[tuple[int, str, str]] = [] + for interval, label in download_jobs(): + stale, reason = is_interval_stale( + symbol, interval, max_lag_bars=max_lag_bars, db_path=db_path + ) + if stale: + out.append((interval, label, reason)) + return out + + +def ensure_ops_candles( + symbol: str = SYMBOL, + months: int | None = None, + *, + force: bool = False, + verbose_download: bool = False, +) -> OpsSyncResult: + """ + 05/06 실행 직전 누락·지연 봉을 coins.db에 증분 적재합니다. + + Args: + symbol: 코인 코드. + months: 보관 개월( None 이면 DOWNLOAD_MONTHS ). + force: True면 최신 간격도 전부 재수집. + verbose_download: True면 download_symbol API 진행 로그 출력. + + Returns: + OpsSyncResult + """ + global _logged_fresh_once + result = OpsSyncResult() + if not OPS_SYNC_ON_START and not force: + result.disabled = True + print("[ops_sync] OPS_SYNC_ON_START=0 — 건너뜀") + return result + + months = months or DOWNLOAD_MONTHS + jobs = download_jobs() + stale = list_stale_intervals(symbol) if not force else [ + (iv, lb, "force") for iv, lb in jobs + ] + fresh = [ + iv for iv, _ in jobs if iv not in {s[0] for s in stale} + ] + result.skipped_fresh = fresh + + if not stale: + if not _logged_fresh_once or force: + print( + f"[ops_sync] {COIN_NAME} ({symbol}) DB 최신 · " + f"간격 {len(fresh)}개 (증분 다운로드 없음)" + ) + for iv in fresh: + last = get_last_timestamp(symbol, iv) + if last is not None: + print(f" [{interval_label(iv)}] ~ {last}") + _logged_fresh_once = True + return result + + print( + f"[ops_sync] {COIN_NAME} ({symbol}) 누락 보완 · " + f"갱신 {len(stale)}개 / 최신 {len(fresh)}개 (백필 생략·증분만)" + ) + monitor = Monitor(cooldown_file=None) + started = datetime.now() + for interval, label, reason in stale: + print(f"\n[ops_sync] --- {label} --- ({reason})") + try: + download_symbol( + monitor, + symbol, + interval, + months, + verbose=verbose_download, + skip_backfill=True, + ) + result.synced.append(interval) + except Exception as exc: + result.errors[interval] = str(exc) + print(f" [ops_sync] 오류 interval={interval}: {exc}") + + elapsed = datetime.now() - started + _logged_fresh_once = True + print(f"\n[ops_sync] 완료 (소요: {elapsed}) · 갱신={result.synced} 오류={len(result.errors)}") + return result diff --git a/deepcoin/ops/live_trader.py b/deepcoin/ops/live_trader.py index 480ecc7..e6fcb59 100644 --- a/deepcoin/ops/live_trader.py +++ b/deepcoin/ops/live_trader.py @@ -370,6 +370,9 @@ class LiveTrader(Monitor): def run_once(self) -> None: """1회: 규칙 평가 → 시뮬 hybrid 체결 → 텔레그램.""" + from deepcoin.data.ops_sync import ensure_ops_candles + + ensure_ops_candles() rules = load_monitor_rules() print( f"[06] {datetime.now():%Y-%m-%d %H:%M:%S} " diff --git a/deepcoin/ops/monitor_coin.py b/deepcoin/ops/monitor_coin.py index 3b544a1..34eaa45 100644 --- a/deepcoin/ops/monitor_coin.py +++ b/deepcoin/ops/monitor_coin.py @@ -29,6 +29,9 @@ class MonitorCoin(Monitor): def monitor_wld(self) -> None: """전 봉 BB·일목·추세 및 규칙 발화를 출력합니다.""" + from deepcoin.data.ops_sync import ensure_ops_candles + + ensure_ops_candles() print( "[{}] {} ({})".format( datetime.now().strftime("%Y-%m-%d %H:%M:%S"), diff --git a/docs/05_ops/DEPLOYMENT_CHECKLIST.md b/docs/05_ops/DEPLOYMENT_CHECKLIST.md index 26321f7..3985173 100644 --- a/docs/05_ops/DEPLOYMENT_CHECKLIST.md +++ b/docs/05_ops/DEPLOYMENT_CHECKLIST.md @@ -125,8 +125,9 @@ LIVE_COOLDOWN_MIN=3 ### 4.2 매일 실행 ```bash -# 프로젝트 루트, xavis conda -python scripts/01_download.py # 1일 1회 (봉 갱신) +# 프로젝트 루트, conda activate ncue (또는 scripts/run.ps1) +# 05/06 시작 시 OPS_SYNC_ON_START=1 이면 누락 봉 자동 증분 (00_sync_ops와 동일) +python scripts/01_download.py # 1일 1회 전체 점검 (선택) python scripts/06_verify_live_dryrun.py # tier·설정 점검 (1일 1회) python scripts/05_run_monitor.py # 상시 알림 (또는 --once 수동) python scripts/06_execute_live.py --once # dry-run (주문 없음, 선택) diff --git a/docs/05_ops/live_verification_20260603.md b/docs/05_ops/live_verification_20260603.md index bdc2dd5..d2672e2 100644 --- a/docs/05_ops/live_verification_20260603.md +++ b/docs/05_ops/live_verification_20260603.md @@ -1,6 +1,6 @@ # Live Phase A — dry-run 검증 -- 일시: 2026-06-03 19:55:24 +- 일시: 2026-06-03 13:34:09 - 결과: **PASS** ## Plan (목적) diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..aff9ce7 --- /dev/null +++ b/environment.yml @@ -0,0 +1,10 @@ +# DeepCoin 권장 conda 환경 (기존 ncue에 설치) +# 생성: conda env create -f environment.yml (이미 ncue가 있으면 activate 후 pip만) +name: ncue +channels: + - defaults +dependencies: + - python>=3.10 + - pip + - pip: + - -r requirements.txt diff --git a/scripts/00_sync_ops.py b/scripts/00_sync_ops.py new file mode 100644 index 0000000..00de21e --- /dev/null +++ b/scripts/00_sync_ops.py @@ -0,0 +1,24 @@ +#!/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) diff --git a/scripts/run.ps1 b/scripts/run.ps1 new file mode 100644 index 0000000..6a4cd7e --- /dev/null +++ b/scripts/run.ps1 @@ -0,0 +1,20 @@ +# DeepCoin CLI wrapper — conda 환경 ncue 고정 +# 사용: .\scripts\run.ps1 06_execute_live.py --once +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Script, + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Rest +) + +$ErrorActionPreference = "Stop" +$root = Split-Path $PSScriptRoot -Parent +Set-Location $root +$env:PYTHONUNBUFFERED = "1" + +$scriptPath = Join-Path $root "scripts" $Script +if (-not (Test-Path $scriptPath)) { + throw "스크립트 없음: $scriptPath" +} + +& conda run -n ncue --no-capture-output python $scriptPath @Rest