feat: 운영 시작 시 누락 봉 증분 동기화 및 B-1 실거래 설정
05/06 시작 전 ops_sync로 지연 간격만 증분 보완하고, Phase B-1 live env·ncue 실행 래퍼를 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
18
.env
18
.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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -83,8 +83,6 @@ celerybeat-schedule
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
|
||||
12
README.md
12
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 데이터 없음")
|
||||
|
||||
191
deepcoin/data/ops_sync.py
Normal file
191
deepcoin/data/ops_sync.py
Normal file
@@ -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
|
||||
@@ -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} "
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 (주문 없음, 선택)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Live Phase A — dry-run 검증
|
||||
|
||||
- 일시: 2026-06-03 19:55:24
|
||||
- 일시: 2026-06-03 13:34:09
|
||||
- 결과: **PASS**
|
||||
|
||||
## Plan (목적)
|
||||
|
||||
10
environment.yml
Normal file
10
environment.yml
Normal file
@@ -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
|
||||
24
scripts/00_sync_ops.py
Normal file
24
scripts/00_sync_ops.py
Normal file
@@ -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)
|
||||
20
scripts/run.ps1
Normal file
20
scripts/run.ps1
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user