WLD DeepCoin 단계별 구조 재편 및 설정·문서 통합

로고스/루트 레거시를 제거하고 deepcoin 패키지·scripts 01~05 CLI·docs/reference로
데이터·GT·분석·매칭·운영 단계를 정리했다. config와 .env 기반 설정, trade_anaysis.html 동기화 포함.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-30 22:58:25 +09:00
parent e631a5701f
commit b52d61b777
76 changed files with 11552 additions and 4567 deletions

View File

@@ -1,36 +0,0 @@
# 빗썸 API
BITHUMB_ACCESS_KEY=
BITHUMB_SECRET_KEY=
# 텔레그램
COIN_TELEGRAM_BOT_TOKEN=
COIN_TELEGRAM_CHAT_ID=
# 쿨다운(초) — 3분 전략 (빈번 체결 완화)
BUY_COOLDOWN_SEC=1800
SELL_COOLDOWN_SEC=900
SIGNAL_EDGE_ONLY=true
TRADE_MIN_GAP_BARS=5
DISCOVER_MAX_TRADES=120
DISCOVER_TRADE_PENALTY_PCT=0.03
SELL_MIN_BB_POS=0.4
BUY_MAX_BB_POS_CHASE=0.55
# 매수 금액(KRW)
DEFAULT_BUY_KRW=30000
RANGE_BUY_KRW=15000
BREAKOUT_BUY_KRW=25000
# BB: 폭이 BB_MIN_WIDTH_PCT(%) 미만이면 매매 중단
BB_MIN_WIDTH_PCT=0.8
# downloader.py — coins.db 적재 개월 수
DOWNLOAD_MONTHS=6
DOWNLOAD_MONTHS_1M=2
USE_DISCOVERED_LIVE=true
# simulation.py
SIM_INITIAL_CASH_KRW=200000
SIM_MIN_ORDER_KRW=5000
TRADING_FEE_RATE=0.0005
RSI_BUY_MAX=42

8
.gitignore vendored
View File

@@ -86,8 +86,12 @@ celerybeat-schedule
# dotenv # dotenv
.env .env
# 백테스트·시뮬레이션 HTML (로컬 재생성) # docs 산출물 (로컬 재생성). reference/ 가이드는 Git 추적
reports/ docs/02_ground_truth/
docs/03_analysis/
docs/04_matching/
docs/05_ops/
docs/charts/
# virtualenv # virtualenv
.venv .venv

104
README.md
View File

@@ -1,64 +1,86 @@
# DeepCoin — WLD 전봉 BB·일목 조합 매매 # DeepCoin — WLD MTF 분석·정답·운영
빗썸 KRW-WLD 현물. **1, 3, 5, 10, 15, 30, 60, 240, 1440분** 모든 봉에서 빗썸 KRW-WLD. **1, 3, 5, 10, 15, 30, 60, 240, 1440분** 봉을 적재하고,
볼린저 밴드·일목균형표 **캔들 위치**를 분석하고, 봉 조합으로 매수·매도 규칙을 탐색합니다. Ground Truth·기술적 분석·(예정) 규칙 매칭·1분 모니터까지 **단계별 폴더**로 관리합니다.
## 구조 ## 로드맵
| 단계 | 목적 | 실행 |
|------|------|------|
| 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` |
| 03b 분석 | GT 타점 MTF 스냅샷 | `python scripts/03_analyze_trades.py` |
| 04 매칭 | GT 근접 규칙 선택 (예정) | `python scripts/04_match_rules.py` |
| 05 운영 | 차트·1분 모니터 | `scripts/05_chart_*.py`, `05_run_monitor.py` |
상세: [docs/reference/ROADMAP.md](docs/reference/ROADMAP.md)
## 디렉터리 구조
```text ```text
downloader.py → coins.db (전 간격 증분) DeepCoin/
indicators.py → BB·일목 계산 ├── .env, config.py
candle_features.py → 봉별 위치 특징 → 3분 타임라인 행렬 ├── scripts/ # ★ 단계별 CLI (유일한 진입점)
combination_analyzer.py → 조합 분석·combination_report.json ├── deepcoin/
rule_discovery.py → discovered_rules.json │ ├── api/bithumb.py # 빗썸 API
strategy.py → 실시간 evaluate_discovered_live │ ├── data/ # 01 다운로드
monitor_coin.py → 실거래 루프 │ ├── ground_truth/ # 02 정답 타점
simulation.py → 백테스트·HTML 차트 │ ├── analysis/ # 03·03b 지표·스냅샷
│ ├── matching/ # 04 규칙 매칭 (예정)
│ └── ops/ # 05 모니터·차트
├── data/ # coins.db, ground_truth/, ops/
└── docs/
├── reference/ # 가이드·기법 명세 (Git)
└── 02~05, charts/ # 단계별 HTML·CSV (재생성)
``` ```
## 봉별 분석 항목 상세: [docs/reference/STRUCTURE.md](docs/reference/STRUCTURE.md) · [docs/README.md](docs/README.md)
### 볼린저 ## 환경 변수
- 이벤트: `cross_up_lower`, `cross_up_upper`, `inside_band`, `squeeze`
- 구간: `bb_zone_bottom` ~ `bb_zone_top` (%B)
### 일목균형표 | 파일 | 용도 |
- `ichi_above_cloud`, `ichi_below_cloud`, `ichi_in_cloud` |------|------|
- `ichi_tk_bull` / `ichi_tk_cross_up`, `ichi_cloud_bull` | `.env` | 전역 설정·API 키 (Git 제외, 프로젝트 루트에 필수) |
### 조합 `config.py``scripts/_bootstrap.py`가 프로젝트 루트 `.env``python-dotenv`로 자동 로드합니다. 새 환경에서는 팀에서 `.env`를 전달받거나 기존 로컬 파일을 복사하세요.
- 3분 기준 `merge_asof`로 모든 봉 특징을 한 행에 정렬
- `discover`가 AND/OR 조합으로 매수·매도 규칙 탐색
## 실행 순서
```bash ```bash
cp .env.example .env pip install -r requirements.txt
python downloader.py # 1분봉 2개월, 나머지 6개월
python simulation.py # analyze → discover → HTML (탐색 매수·매도 규칙 표시)
python monitor_coin.py # 실거래
``` ```
HTML 차트에는 `discovered_rules.json` 에서 찾은 **매수·매도 규칙**의 체결만 표시합니다. ## 빠른 시작
고급: `analyze`, `discover`, `compare`, `mtf`.
## 설정 (`config.py`) ```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/05_chart_truth.py
```
## 주요 설정 (`config.py` / `.env`)
| 항목 | 설명 | | 항목 | 설명 |
|------|------| |------|------|
| `ALL_INTERVALS` | 1,3,5,10,15,30,60,240,1440 | | `BITHUMB_ACCESS_KEY` | 빗썸 API (다운로드·시세) |
| `ENTRY_INTERVAL` | 조합 행렬 기준 3분 | | `DB_PATH` | `data/coins.db` (`.env`로 변경 가능) |
| `DOWNLOAD_MONTHS_1M` | 1분봉 보관 개월 (기본 2) | | `GROUND_TRUTH_FILE` | `data/ground_truth/ground_truth_trades.json` |
| `USE_DISCOVERED_LIVE` | 실거래에 discovered_rules 사용 | | `CHART_LOOKBACK_DAYS` | 기본 365일 |
| `DOWNLOAD_MONTHS` | 3분 이상 봉 12개월 |
| `MONITOR_LOOP_SLEEP_SEC` | 05 모니터 루프 주기(초) |
## 출력 파일 ## 산출물
| 파일 | 내용 | | 경로 | 내용 |
|------|------| |------|------|
| `combination_report.json` | 봉별 최신 위치·매수/매도 힌트 | | `data/coins.db` | 전 간격 OHLCV |
| `discovered_rules.json` | 탐색된 매매 규칙 | | `data/ground_truth/ground_truth_trades.json` | 정답 타점 |
| `reports/wld_bb_simulation.html` | 시뮬 차트 | | `docs/charts/wld_bb_chart.html` | 3분 BB 차트 |
| `docs/02_ground_truth/wld_ground_truth_chart.html` | 정답 차트 |
| `docs/03_analysis/latest/*_latest.csv` | 간격별 최근 봉 전 기법 |
| `docs/03_analysis/general_analysis_trades.csv` | GT 타점 MTF 스냅샷 |
## 면책 ## 면책
실거래 손실 책임은 사용자에게 있습니다. 실거래는 사용자 책임입니다. 본 저장소는 주문 실행을 포함하지 않습니다.

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,222 +0,0 @@
"""
모든 봉의 BB·일목 위치를 조합해 매수/매도 후보 규칙을 분석합니다.
python simulation.py analyze
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
import numpy as np
import pandas as pd
from candle_features import (
FEATURE_BOOL_COLS,
INTERVAL_LABELS,
build_master_feature_matrix,
describe_latest_position,
interval_prefix,
)
from config import ALL_INTERVALS, ENTRY_INTERVAL, SYMBOL
REPORT_FILE = Path(__file__).parent / "combination_report.json"
@dataclass
class CombinationReport:
"""봉 조합 분석 결과."""
generated_at: str
intervals_loaded: list[int]
latest_positions: list[dict]
buy_recommendations: list[str] = field(default_factory=list)
sell_recommendations: list[str] = field(default_factory=list)
buy_avoid: list[str] = field(default_factory=list)
top_buy_pairs: list[dict] = field(default_factory=list)
top_sell_pairs: list[dict] = field(default_factory=list)
suggested_rules: dict = field(default_factory=dict)
def _forward_return(close: pd.Series, bars: int = 20) -> pd.Series:
"""N봉 후 수익률 (%)."""
future = close.shift(-bars)
return (future - close) / close.replace(0, np.nan) * 100
def _predicate_keys(intervals: list[int]) -> list[str]:
keys: list[str] = []
for iv in intervals:
pfx = interval_prefix(iv)
for feat in FEATURE_BOOL_COLS:
keys.append(f"{pfx}:{feat}")
return keys
def analyze_forward_edge(
matrix: pd.DataFrame,
forward_bars: int = 20,
min_samples: int = 40,
) -> tuple[list[dict], list[dict], list[dict]]:
"""
단일 조건·2봉 조합별 미래 수익 통계 (학습용 힌트).
Returns:
(매수 유리 top, 매도/회피 top)
"""
close = matrix["Close"].astype(float)
fwd = _forward_return(close, forward_bars)
valid = fwd.notna()
base_mean = float(fwd[valid].mean()) if valid.any() else 0.0
singles: list[dict] = []
cols = [c for c in matrix.columns if any(c.startswith(f"{interval_prefix(iv)}_") for iv in ALL_INTERVALS)]
for col in cols:
mask = matrix[col].fillna(0).astype(bool) & valid
n = int(mask.sum())
if n < min_samples:
continue
avg = float(fwd[mask].mean())
singles.append(
{
"key": _col_to_key(col),
"column": col,
"count": n,
"avg_forward_pct": round(avg, 4),
"edge_vs_base": round(avg - base_mean, 4),
}
)
singles.sort(key=lambda x: x["edge_vs_base"], reverse=True)
buy_top = [s for s in singles if s["edge_vs_base"] > 0][:25]
sell_top = sorted(singles, key=lambda x: x["edge_vs_base"])[:15]
pairs: list[dict] = []
buy_cols = [s["column"] for s in buy_top[:12]]
for i, c1 in enumerate(buy_cols):
for c2 in buy_cols[i + 1 :]:
if c1.split("_")[0] == c2.split("_")[0]:
continue
mask = (
matrix[c1].fillna(0).astype(bool)
& matrix[c2].fillna(0).astype(bool)
& valid
)
n = int(mask.sum())
if n < min_samples // 2:
continue
avg = float(fwd[mask].mean())
pairs.append(
{
"keys": [_col_to_key(c1), _col_to_key(c2)],
"count": n,
"avg_forward_pct": round(avg, 4),
"edge_vs_base": round(avg - base_mean, 4),
}
)
pairs.sort(key=lambda x: x["edge_vs_base"], reverse=True)
return buy_top, pairs[:20], sell_top
def _col_to_key(col: str) -> str:
"""m3_cross_up_lower -> m3:cross_up_lower."""
for pfx in INTERVAL_LABELS.values():
if col.startswith(f"{pfx}_"):
return f"{pfx}:{col[len(pfx) + 1:]}"
return col
def build_recommendations(
buy_top: list[dict],
pair_top: list[dict],
sell_top: list[dict],
) -> tuple[list[str], list[str], list[str], dict]:
"""사람이 읽을 수 있는 권장·규칙 초안."""
buy_rec: list[str] = []
sell_rec: list[str] = []
avoid: list[str] = []
for s in buy_top[:8]:
buy_rec.append(
f"{s['key']}{s['count']}회, {s['avg_forward_pct']:+.2f}% ({s['edge_vs_base']:+.2f}%p)"
)
for p in pair_top[:5]:
buy_rec.append(
f"조합 {' + '.join(p['keys'])}{p['count']}회, {p['edge_vs_base']:+.2f}%p"
)
for s in sell_top[:6]:
if s["edge_vs_base"] < -0.05:
avoid.append(f"매수 회피: {s['key']} ({s['edge_vs_base']:.2f}%p)")
for s in sell_top[:5]:
if "cross_up_upper" in s["key"] or "above_upper" in s["key"] or "ichi_above" in s["key"]:
sell_rec.append(f"매도 후보: {s['key']}")
suggested: dict = {"buy_all": [], "buy_any": [], "sell_all": [], "sell_stop": []}
if pair_top:
suggested["buy_all"] = pair_top[0]["keys"]
elif buy_top:
suggested["buy_all"] = [buy_top[0]["key"]]
if sell_top:
for s in sell_top:
if "cross_up_upper" in s.get("key", ""):
suggested["sell_all"] = [s["key"]]
break
return buy_rec, sell_rec, avoid, suggested
def analyze_combinations(frames: dict[int, pd.DataFrame]) -> CombinationReport:
"""전체 봉 BB·일목 위치 분석 + 조합 매매 힌트."""
from datetime import datetime
loaded = sorted(frames.keys())
latest = [describe_latest_position(frames[iv], iv) for iv in ALL_INTERVALS if iv in frames]
print("\n=== 봉별 최신 BB·일목 위치 ===")
for p in latest:
print(
f" {p['label']:>6} | BB {p['bb_zone']:>6} ({p['bb_pos']:.2f}) {p['bb_state']:>16} | "
f"일목 {p['ichi_position']:>12} TK={p['ichi_tk']} 구름={p['ichi_cloud']}"
)
matrix = build_master_feature_matrix(frames).iloc[52:].copy()
print(f"\n특징 행렬: {len(matrix)}× {len(matrix.columns)}")
buy_top, pair_top, sell_top = analyze_forward_edge(matrix)
buy_rec, sell_rec, avoid, suggested = build_recommendations(buy_top, pair_top, sell_top)
print("\n=== 매수 유리 조건 (단일·상위) ===")
for line in buy_rec[:10]:
print(f" {line}")
print("\n=== 매수 회피 / 매도 참고 ===")
for line in avoid[:6]:
print(f" {line}")
for line in sell_rec[:5]:
print(f" {line}")
return CombinationReport(
generated_at=datetime.now().isoformat(timespec="seconds"),
intervals_loaded=loaded,
latest_positions=latest,
buy_recommendations=buy_rec,
sell_recommendations=sell_rec,
buy_avoid=avoid,
top_buy_pairs=pair_top,
suggested_rules=suggested,
)
def save_report(report: CombinationReport, path: Path = REPORT_FILE) -> None:
path.write_text(json.dumps(asdict(report), ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n저장: {path}")
def load_frames(monitor) -> dict[int, pd.DataFrame]:
from mtf_bb import load_frames_from_db
return load_frames_from_db(monitor, SYMBOL)

View File

@@ -1,331 +0,0 @@
{
"generated_at": "2026-05-29T13:55:17",
"intervals_loaded": [
1,
3,
5,
10,
15,
30,
60,
240,
1440
],
"latest_positions": [
{
"interval": 1,
"label": "1분",
"close": 425.0,
"bb_pos": 0.352,
"bb_zone": "mid",
"bb_state": "squeeze",
"ichi_position": "in_cloud",
"ichi_tk": "bear",
"ichi_cloud": "bull"
},
{
"interval": 3,
"label": "3분",
"close": 425.0,
"bb_pos": 0.638,
"bb_zone": "mid",
"bb_state": "inside_band",
"ichi_position": "above_cloud",
"ichi_tk": "bull",
"ichi_cloud": "bull"
},
{
"interval": 5,
"label": "5분",
"close": 425.0,
"bb_pos": 0.706,
"bb_zone": "high",
"bb_state": "inside_band",
"ichi_position": "above_cloud",
"ichi_tk": "bull",
"ichi_cloud": "bear"
},
{
"interval": 10,
"label": "10분",
"close": 425.0,
"bb_pos": 0.744,
"bb_zone": "high",
"bb_state": "inside_band",
"ichi_position": "in_cloud",
"ichi_tk": "bear",
"ichi_cloud": "bear"
},
{
"interval": 15,
"label": "15분",
"close": 425.0,
"bb_pos": 0.526,
"bb_zone": "mid",
"bb_state": "inside_band",
"ichi_position": "above_cloud",
"ichi_tk": "bear",
"ichi_cloud": "bear"
},
{
"interval": 30,
"label": "30분",
"close": 425.0,
"bb_pos": 0.448,
"bb_zone": "mid",
"bb_state": "inside_band",
"ichi_position": "above_cloud",
"ichi_tk": "bear",
"ichi_cloud": "bear"
},
{
"interval": 60,
"label": "60분",
"close": 425.0,
"bb_pos": 0.613,
"bb_zone": "mid",
"bb_state": "inside_band",
"ichi_position": "below_cloud",
"ichi_tk": "bull",
"ichi_cloud": "bear"
},
{
"interval": 240,
"label": "240분",
"close": 425.0,
"bb_pos": 0.317,
"bb_zone": "low",
"bb_state": "inside_band",
"ichi_position": "below_cloud",
"ichi_tk": "bear",
"ichi_cloud": "bear"
},
{
"interval": 1440,
"label": "일봉",
"close": 425.0,
"bb_pos": 0.417,
"bb_zone": "mid",
"bb_state": "inside_band",
"ichi_position": "below_cloud",
"ichi_tk": "bull",
"ichi_cloud": "bull"
}
],
"buy_recommendations": [
"m30:bullish — 410회, +1.08% (+1.04%p)",
"m60:above_upper — 480회, +1.00% (+0.96%p)",
"m60:cross_up_upper — 340회, +0.89% (+0.86%p)",
"m60:bullish — 960회, +0.76% (+0.72%p)",
"m240:hammer — 1588회, +0.70% (+0.66%p)",
"m240:cross_up_upper — 160회, +0.66% (+0.62%p)",
"m60:bb_zone_top — 1377회, +0.66% (+0.62%p)",
"m30:ichi_tk_cross_down — 148회, +0.62% (+0.59%p)",
"조합 m30:bullish + m60:above_upper — 50회, +2.48%p",
"조합 m60:above_upper + m30:body_strong — 50회, +2.44%p",
"조합 m30:bullish + d1:bb_zone_high — 130회, +2.39%p",
"조합 m30:bullish + m60:cross_up_upper — 30회, +2.26%p",
"조합 d1:bb_zone_high + m30:body_strong — 100회, +2.13%p"
],
"sell_recommendations": [],
"buy_avoid": [
"매수 회피: m240:cross_down_lower (-0.93%p)",
"매수 회피: m240:below_lower (-0.93%p)",
"매수 회피: m10:body_ratio (-0.63%p)",
"매수 회피: m15:cross_up_lower (-0.58%p)",
"매수 회피: m10:bullish (-0.54%p)",
"매수 회피: m240:cross_up_lower (-0.53%p)"
],
"top_buy_pairs": [
{
"keys": [
"m30:bullish",
"m60:above_upper"
],
"count": 50,
"avg_forward_pct": 2.5214,
"edge_vs_base": 2.4838
},
{
"keys": [
"m60:above_upper",
"m30:body_strong"
],
"count": 50,
"avg_forward_pct": 2.4777,
"edge_vs_base": 2.4401
},
{
"keys": [
"m30:bullish",
"d1:bb_zone_high"
],
"count": 130,
"avg_forward_pct": 2.4265,
"edge_vs_base": 2.3889
},
{
"keys": [
"m30:bullish",
"m60:cross_up_upper"
],
"count": 30,
"avg_forward_pct": 2.2987,
"edge_vs_base": 2.2611
},
{
"keys": [
"d1:bb_zone_high",
"m30:body_strong"
],
"count": 100,
"avg_forward_pct": 2.1672,
"edge_vs_base": 2.1296
},
{
"keys": [
"m60:bb_zone_top",
"m30:body_strong"
],
"count": 80,
"avg_forward_pct": 2.0742,
"edge_vs_base": 2.0366
},
{
"keys": [
"m240:hammer",
"m30:body_strong"
],
"count": 139,
"avg_forward_pct": 1.9833,
"edge_vs_base": 1.9457
},
{
"keys": [
"m30:bullish",
"m240:hammer"
],
"count": 190,
"avg_forward_pct": 1.932,
"edge_vs_base": 1.8944
},
{
"keys": [
"m30:bullish",
"m60:bb_zone_top"
],
"count": 140,
"avg_forward_pct": 1.924,
"edge_vs_base": 1.8864
},
{
"keys": [
"m30:bullish",
"m60:bb_pos_high"
],
"count": 140,
"avg_forward_pct": 1.924,
"edge_vs_base": 1.8864
},
{
"keys": [
"m30:body_strong",
"m60:bb_pos_high"
],
"count": 90,
"avg_forward_pct": 1.8966,
"edge_vs_base": 1.859
},
{
"keys": [
"m60:bullish",
"m240:cross_up_upper"
],
"count": 20,
"avg_forward_pct": 1.8701,
"edge_vs_base": 1.8324
},
{
"keys": [
"m60:cross_up_upper",
"m30:body_strong"
],
"count": 20,
"avg_forward_pct": 1.731,
"edge_vs_base": 1.6934
},
{
"keys": [
"m60:cross_up_upper",
"m240:hammer"
],
"count": 180,
"avg_forward_pct": 1.5455,
"edge_vs_base": 1.5079
},
{
"keys": [
"m60:above_upper",
"m240:hammer"
],
"count": 300,
"avg_forward_pct": 1.4981,
"edge_vs_base": 1.4605
},
{
"keys": [
"m30:bullish",
"m60:hammer"
],
"count": 210,
"avg_forward_pct": 1.4713,
"edge_vs_base": 1.4337
},
{
"keys": [
"m60:bullish",
"m240:hammer"
],
"count": 420,
"avg_forward_pct": 1.3974,
"edge_vs_base": 1.3598
},
{
"keys": [
"m240:hammer",
"m60:bb_zone_top"
],
"count": 480,
"avg_forward_pct": 1.3812,
"edge_vs_base": 1.3436
},
{
"keys": [
"m60:cross_up_upper",
"d1:bb_zone_high"
],
"count": 180,
"avg_forward_pct": 1.3333,
"edge_vs_base": 1.2956
},
{
"keys": [
"m60:bullish",
"m30:body_strong"
],
"count": 90,
"avg_forward_pct": 1.3228,
"edge_vs_base": 1.2852
}
],
"top_sell_pairs": [],
"suggested_rules": {
"buy_all": [
"m30:bullish",
"m60:above_upper"
],
"buy_any": [],
"sell_all": [],
"sell_stop": []
}
}

300
config.py
View File

@@ -1,92 +1,264 @@
""" """
전역 설정 (WLD 월드코인, 3분 BB MTF 전략). 전역 설정 (WLD). 값은 PROJECT_ROOT/.env → OS 환경 변수 순으로 읽습니다.
""" """
from __future__ import annotations
import os import os
try: from deepcoin.env_loader import load_project_env
from dotenv import load_dotenv
load_project_env()
def _getenv(key: str, default: str = "") -> str:
return os.getenv(key, default)
def _getenv_int(key: str, default: str) -> int:
return int(_getenv(key, default))
def _getenv_float(key: str, default: str) -> float:
return float(_getenv(key, default))
def _parse_int_tuple(env_key: str, default: str) -> tuple[int, ...]:
raw = _getenv(env_key, default)
return tuple(int(x.strip()) for x in raw.split(",") if x.strip())
def _parse_int_set(env_key: str, default: str) -> frozenset[int]:
return frozenset(_parse_int_tuple(env_key, default))
def _parse_interval_map(env_key: str, default: str) -> dict[int, int]:
"""
'3:120,5:100'{3: 120, 5: 100}.
"""
raw = _getenv(env_key, default)
out: dict[int, int] = {}
for part in raw.split(","):
part = part.strip()
if ":" not in part:
continue
k, v = part.split(":", 1)
out[int(k.strip())] = int(v.strip())
return out
def _parse_str_map(env_key: str, default: str) -> dict[int, str]:
"""'3:m3,1440:d1'{3: 'm3', 1440: 'd1'}."""
raw = _getenv(env_key, default)
out: dict[int, str] = {}
for part in raw.split(","):
part = part.strip()
if ":" not in part:
continue
k, v = part.split(":", 1)
out[int(k.strip())] = v.strip()
return out
load_dotenv()
except ImportError:
pass
# --- API / 알림 --- # --- API / 알림 ---
COIN_TELEGRAM_BOT_TOKEN = os.getenv("COIN_TELEGRAM_BOT_TOKEN", "") BITHUMB_ACCESS_KEY = _getenv("BITHUMB_ACCESS_KEY")
COIN_TELEGRAM_CHAT_ID = os.getenv("COIN_TELEGRAM_CHAT_ID", "") BITHUMB_SECRET_KEY = _getenv("BITHUMB_SECRET_KEY")
BITHUMB_API_URL = _getenv("BITHUMB_API_URL", "https://api.bithumb.com")
BITHUMB_API_CANDLE_COUNT = _getenv_int("BITHUMB_API_CANDLE_COUNT", "200")
BITHUMB_MINUTE_INTERVALS = _parse_int_set(
"BITHUMB_MINUTE_INTERVALS", "1,3,5,10,15,30,60,240"
)
HTS_API_RETRY_SLEEP_SEC = _getenv_float("HTS_API_RETRY_SLEEP_SEC", "0.5")
COIN_TELEGRAM_BOT_TOKEN = _getenv("COIN_TELEGRAM_BOT_TOKEN")
COIN_TELEGRAM_CHAT_ID = _getenv("COIN_TELEGRAM_CHAT_ID")
# --- 거래 대상 --- # --- 거래 대상 ---
SYMBOL = "WLD" SYMBOL = _getenv("SYMBOL", "WLD")
COIN_NAME = "월드코인" COIN_NAME = _getenv("COIN_NAME", "월드코인")
KR_COINS: dict[str, str] = {SYMBOL: COIN_NAME}
KR_COINS: dict[str, str] = {
SYMBOL: COIN_NAME,
}
# --- 타임프레임 (분) --- # --- 타임프레임 (분) ---
TREND_INTERVAL_1H = 60 DAILY_INTERVAL_MIN = _getenv_int("DAILY_INTERVAL_MIN", "1440")
TREND_INTERVAL_1D = 1440 ENTRY_INTERVAL = _getenv_int("ENTRY_INTERVAL", "3")
TREND_INTERVAL_1H = _getenv_int("TREND_INTERVAL_1H", "60")
TREND_INTERVAL_1D = _getenv_int("TREND_INTERVAL_1D", "1440")
# --- 쿨다운(초) — 3분봉: 기본 30분/15분 (빈번 체결 완화) --- ALL_INTERVALS: tuple[int, ...] = _parse_int_tuple(
BUY_COOLDOWN_SEC = int(os.getenv("BUY_COOLDOWN_SEC", "1800")) "ALL_INTERVALS", "1,3,5,10,15,30,60,240,1440"
SELL_COOLDOWN_SEC = int(os.getenv("SELL_COOLDOWN_SEC", "900")) )
BUY_MINUTE_LIMIT = BUY_COOLDOWN_SEC DOWNLOAD_INTERVALS: tuple[int, ...] = _parse_int_tuple(
"DOWNLOAD_INTERVALS",
",".join(str(x) for x in ALL_INTERVALS),
)
GENERAL_ANALYSIS_INTERVALS: tuple[int, ...] = _parse_int_tuple(
"GENERAL_ANALYSIS_INTERVALS", "3,5,10,15,30,60,240,1440"
)
TIMING_INTERVALS: tuple[int, ...] = _parse_int_tuple(
"TIMING_INTERVALS", "3,5,10,15"
)
TREND_INTERVALS: tuple[int, ...] = _parse_int_tuple(
"TREND_INTERVALS", "60,240,1440"
)
# 매수·매도 신호는 조건이 False→True로 바뀐 봉에서만 (연속 참 방지) INTERVAL_PREFIX: dict[int, str] = _parse_str_map(
SIGNAL_EDGE_ONLY = os.getenv("SIGNAL_EDGE_ONLY", "true").lower() in ("1", "true", "yes") "INTERVAL_PREFIX",
"1:m1,3:m3,5:m5,10:m10,15:m15,30:m30,60:m60,240:m240,1440:d1",
)
# 체결(매수·매도 공통) 후 최소 대기 봉 수 (3분봉 5봉 = 15분) # --- 볼린저 / RSI ---
TRADE_MIN_GAP_BARS = int(os.getenv("TRADE_MIN_GAP_BARS", "5")) BB_PERIOD = _getenv_int("BB_PERIOD", "20")
BB_STD = _getenv_float("BB_STD", "2")
BB_MIN_WIDTH_PCT = _getenv_float("BB_MIN_WIDTH_PCT", "0.8")
RSI_PERIOD = _getenv_int("RSI_PERIOD", "14")
# 규칙 탐색 시 거래 횟수 패널티 (학습 구간) # --- 이격도 ---
DISCOVER_MAX_TRADES = int(os.getenv("DISCOVER_MAX_TRADES", "120")) DISPARITY_PERIODS: tuple[int, ...] = _parse_int_tuple("DISPARITY_PERIODS", "5,20,60")
DISCOVER_TRADE_PENALTY_PCT = float(os.getenv("DISCOVER_TRADE_PENALTY_PCT", "0.03")) DISPARITY_OVERBOUGHT = _getenv_float("DISPARITY_OVERBOUGHT", "105")
DISPARITY_OVERSOLD = _getenv_float("DISPARITY_OVERSOLD", "95")
# 3분 BB 위치: 이 값 미만에서 상단돌파 매도 차단 (저점 익절 방지) # --- MACD / Stochastic ---
SELL_MIN_BB_POS = float(os.getenv("SELL_MIN_BB_POS", "0.4")) MACD_FAST = _getenv_int("MACD_FAST", "12")
MACD_SLOW = _getenv_int("MACD_SLOW", "26")
MACD_SIGNAL = _getenv_int("MACD_SIGNAL", "9")
STOCH_K_PERIOD = _getenv_int("STOCH_K_PERIOD", "14")
STOCH_D_PERIOD = _getenv_int("STOCH_D_PERIOD", "3")
STOCH_SMOOTH_K = _getenv_int("STOCH_SMOOTH_K", "3")
STOCH_OVERSOLD = _getenv_float("STOCH_OVERSOLD", "20")
STOCH_OVERBOUGHT = _getenv_float("STOCH_OVERBOUGHT", "80")
# 3분 BB 위치: 이 값 이상이면 단독 상단구간 매수 차단 (고점 추격 방지) # --- 추세 ---
BUY_MAX_BB_POS_CHASE = float(os.getenv("BUY_MAX_BB_POS_CHASE", "0.55")) TREND_RANGE_MA_GAP_PCT = _getenv_float("TREND_RANGE_MA_GAP_PCT", "0.5")
# --- 볼린저 (3분봉, 20, 2σ) --- # --- MTF 합성·정렬 ---
BB_PERIOD = 20 ALIGN_RSI_OVERSOLD = _getenv_float("ALIGN_RSI_OVERSOLD", "35")
BB_STD = 2 ALIGN_RSI_OVERBOUGHT = _getenv_float("ALIGN_RSI_OVERBOUGHT", "65")
BB_MIN_WIDTH_PCT = float(os.getenv("BB_MIN_WIDTH_PCT", "0.8")) ALIGN_RSI_CONFLICT_TIMING_LOW = _getenv_float("ALIGN_RSI_CONFLICT_TIMING_LOW", "40")
ALIGN_RSI_CONFLICT_TIMING_HIGH = _getenv_float("ALIGN_RSI_CONFLICT_TIMING_HIGH", "65")
ALIGN_RSI_CONFLICT_TREND_LOW = _getenv_float("ALIGN_RSI_CONFLICT_TREND_LOW", "40")
ALIGN_RSI_CONFLICT_TREND_HIGH = _getenv_float("ALIGN_RSI_CONFLICT_TREND_HIGH", "65")
ALIGN_BB_POS_LOW = _getenv_float("ALIGN_BB_POS_LOW", "0.2")
ALIGN_BB_POS_HIGH = _getenv_float("ALIGN_BB_POS_HIGH", "0.8")
# --- RSI / 거래량 (조합 필터) --- # --- 다운로드 / DB ---
RSI_PERIOD = 14 DOWNLOAD_MONTHS = _getenv_int("DOWNLOAD_MONTHS", "12")
RSI_BUY_MAX = float(os.getenv("RSI_BUY_MAX", "42")) DOWNLOAD_MONTHS_1M = _getenv_int("DOWNLOAD_MONTHS_1M", "6")
VOLUME_BUY_RATIO = float(os.getenv("VOLUME_BUY_RATIO", "1.0")) 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")
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")
DB_ROW_DAILY_PADDING_DAYS = _getenv_int("DB_ROW_DAILY_PADDING_DAYS", "30")
# --- 추세 / 레짐 ---
TREND_RANGE_MA_GAP_PCT = 0.5
# --- 주문 --- def _paths():
DEFAULT_BUY_KRW = int(os.getenv("DEFAULT_BUY_KRW", "30000")) from deepcoin.paths import (
RANGE_BUY_KRW = int(os.getenv("RANGE_BUY_KRW", "15000")) ANALYSIS_CAPABILITY_HTML,
ANALYSIS_LATEST_DIR,
ANALYSIS_REPORT_HTML,
ANALYSIS_TRADES_CSV,
resolve_db_path,
resolve_ground_truth_file,
)
# --- 수수료 (매수·매도 각각 적용, 시뮬레이션) --- return (
TRADING_FEE_RATE = float(os.getenv("TRADING_FEE_RATE", "0.0005")) resolve_db_path(),
resolve_ground_truth_file(),
ANALYSIS_TRADES_CSV,
ANALYSIS_REPORT_HTML,
ANALYSIS_CAPABILITY_HTML,
ANALYSIS_LATEST_DIR,
)
# --- coins.db (downloader.py 적재 간격, 분) ---
# 빗썸 분봉 API: 1,3,5,10,15,30,60,240 / 일봉 1440
ALL_INTERVALS: tuple[int, ...] = (1, 3, 5, 10, 15, 30, 60, 240, 1440)
DOWNLOAD_INTERVALS: tuple[int, ...] = ALL_INTERVALS
DOWNLOAD_MONTHS = int(os.getenv("DOWNLOAD_MONTHS", "6"))
# 1분봉은 용량·API 부담으로 기본 2개월 (환경변수로 조정)
DOWNLOAD_MONTHS_1M = int(os.getenv("DOWNLOAD_MONTHS_1M", "2"))
DB_PATH = "coins.db"
# 규칙 탐색·조합 분석 기준 타임라인 _db, _gt, _a_csv, _a_html, _a_cap, _a_latest = _paths()
ENTRY_INTERVAL = 3 DB_PATH = _getenv("DB_PATH", str(_db))
GROUND_TRUTH_PATH = _gt
REPORTS_ANALYSIS_TRADES_CSV = _a_csv
REPORTS_ANALYSIS_REPORT_HTML = _a_html
REPORTS_ANALYSIS_CAPABILITY_HTML = _a_cap
REPORTS_ANALYSIS_LATEST_DIR = _a_latest
GROUND_TRUTH_FILE = _getenv("GROUND_TRUTH_FILE", str(_gt))
# 실시간: discovered_rules + 전 봉 BB·일목 조합 (False면 mtf_bb_policy) # --- 차트 ---
USE_DISCOVERED_LIVE = os.getenv("USE_DISCOVERED_LIVE", "true").lower() in ("1", "true", "yes") CHART_LOOKBACK_DAYS = _getenv_int("CHART_LOOKBACK_DAYS", "365")
GT_UNLIMITED_CHRONOLOGICAL_DAYS = _getenv_int("GT_UNLIMITED_CHRONOLOGICAL_DAYS", "300")
# --- 시뮬레이션 --- # --- Ground Truth ---
SIM_INITIAL_CASH_KRW = int(os.getenv("SIM_INITIAL_CASH_KRW", "200000")) GT_MIN_SWING_PCT = _getenv_float("GT_MIN_SWING_PCT", "4.0")
SIM_MIN_ORDER_KRW = int(os.getenv("SIM_MIN_ORDER_KRW", "5000")) GT_PIVOT_ORDER = _getenv_int("GT_PIVOT_ORDER", "20")
GT_MIN_BARS_BETWEEN = _getenv_int("GT_MIN_BARS_BETWEEN", "30")
GT_MAX_ROUND_TRIPS = _getenv_int("GT_MAX_ROUND_TRIPS", "24")
GT_SELECTION_MODE = _getenv("GT_SELECTION_MODE", "split_buy_peak_sell")
GT_MIN_LEG_PCT = _getenv_float("GT_MIN_LEG_PCT", "8.0")
GT_BUY_MIN_SWING_PCT = _getenv_float("GT_BUY_MIN_SWING_PCT", "3.0")
GT_BUY_BB_MAX = _getenv_float("GT_BUY_BB_MAX", "0.45")
GT_BUY_MIN_BARS = _getenv_int("GT_BUY_MIN_BARS", "24")
GT_MAX_BUYS_PER_LEG = _getenv_int("GT_MAX_BUYS_PER_LEG", "12")
GT_MAX_SELLS_PER_LEG = _getenv_int("GT_MAX_SELLS_PER_LEG", "2")
GT_SELL_SPLIT_GAP_PCT = _getenv_float("GT_SELL_SPLIT_GAP_PCT", "2.5")
GT_MARKER_SIZE_MIN = _getenv_int("GT_MARKER_SIZE_MIN", "10")
GT_MARKER_SIZE_MAX = _getenv_int("GT_MARKER_SIZE_MAX", "32")
GT_INITIAL_CASH_KRW = _getenv_int("GT_INITIAL_CASH_KRW", "1000000")
TRADING_FEE_RATE = _getenv_float("TRADING_FEE_RATE", "0.0005")
# --- 실행 --- # --- 모니터 / API 수집 ---
MONITOR_LOOP_SLEEP_SEC = 10 MONITOR_LOOP_SLEEP_SEC = _getenv_int("MONITOR_LOOP_SLEEP_SEC", "10")
COOLDOWN_FILE = "coins_buy_time.json" MONITOR_POOL_WORKERS = _getenv_int("MONITOR_POOL_WORKERS", "12")
MONITOR_DEFAULT_INTERVAL = _getenv_int("MONITOR_DEFAULT_INTERVAL", "60")
MONITOR_API_RETRIES = _getenv_int("MONITOR_API_RETRIES", "3")
MONITOR_API_BONG_COUNT = _getenv_int("MONITOR_API_BONG_COUNT", "3000")
MONITOR_SLEEP_AFTER_REQUEST_SEC = _getenv_float("MONITOR_SLEEP_AFTER_REQUEST_SEC", "0.5")
MONITOR_SLEEP_RATE_LIMIT_SEC = _getenv_float("MONITOR_SLEEP_RATE_LIMIT_SEC", "5")
MONITOR_SLEEP_BETWEEN_CHUNKS_SEC = _getenv_float("MONITOR_SLEEP_BETWEEN_CHUNKS_SEC", "0.3")
MONITOR_API_CHUNK_BARS = _getenv_int("MONITOR_API_CHUNK_BARS", "200")
MONITOR_MA_WINDOWS: tuple[int, ...] = _parse_int_tuple(
"MONITOR_MA_WINDOWS", "5,20,40,120,200,240,720,1440"
)
MONITOR_NORM_WINDOW = _getenv_int("MONITOR_NORM_WINDOW", "20")
MONITOR_TELEGRAM_BATCH_SIZE = _getenv_int("MONITOR_TELEGRAM_BATCH_SIZE", "20")
# --- general_analysis ---
GA_COL_PREFIX = _getenv("GA_COL_PREFIX", "ga_")
LOOKBACK_BARS: dict[int, int] = _parse_interval_map(
"LOOKBACK_BARS",
"3:120,5:100,10:80,15:60,30:50,60:40,240:30,1440:60",
)
CONTEXT_TAIL_ROWS: dict[int, int] = _parse_interval_map(
"CONTEXT_TAIL_ROWS",
"3:6000,5:5000,10:4000,15:3000,30:2000,60:1500,240:800,1440:500",
)
GA_DEFAULT_TAIL_EXPORT = _getenv_int("GA_DEFAULT_TAIL_EXPORT", "200")
GA_PATTERN_TOLERANCE_PCT = _getenv_float("GA_PATTERN_TOLERANCE_PCT", "2.5")
GA_VP_BINS = _getenv_int("GA_VP_BINS", "30")
GA_VP_VALUE_AREA_PCT = _getenv_float("GA_VP_VALUE_AREA_PCT", "0.70")
GA_HV_ROLLING_BARS = _getenv_int("GA_HV_ROLLING_BARS", "20")
GA_HV_PERCENTILE_WINDOW = _getenv_int("GA_HV_PERCENTILE_WINDOW", "120")
GA_HV_ANNUALIZE_SQRT = _getenv_float("GA_HV_ANNUALIZE_SQRT", "339.41148133")
GA_DIVERGENCE_LOOKBACK = _getenv_int("GA_DIVERGENCE_LOOKBACK", "10")
GA_SMA_PERIODS: tuple[int, ...] = _parse_int_tuple("GA_SMA_PERIODS", "5,20,60,120")
GA_EMA_SPANS: tuple[int, ...] = _parse_int_tuple("GA_EMA_SPANS", "12,26")
GA_ATR_PERIOD = _getenv_int("GA_ATR_PERIOD", "14")
GA_KELTNER_ATR_MULT = _getenv_float("GA_KELTNER_ATR_MULT", "2")
GA_AO_FAST = _getenv_int("GA_AO_FAST", "5")
GA_AO_SLOW = _getenv_int("GA_AO_SLOW", "34")
GA_LINREG_WINDOW = _getenv_int("GA_LINREG_WINDOW", "20")
GA_ADX_PERIOD = _getenv_int("GA_ADX_PERIOD", "14")
GA_ADX_TREND_THRESHOLD = _getenv_float("GA_ADX_TREND_THRESHOLD", "25")
GA_SUPERTREND_ATR_MULT = _getenv_float("GA_SUPERTREND_ATR_MULT", "3")
GA_VOL_SPIKE_MULT = _getenv_float("GA_VOL_SPIKE_MULT", "1.8")
GA_VOL_MA_WINDOW = _getenv_int("GA_VOL_MA_WINDOW", "20")
GA_CCI_PERIOD = _getenv_int("GA_CCI_PERIOD", "20")
GA_WILLIAMS_PERIOD = _getenv_int("GA_WILLIAMS_PERIOD", "14")
GA_ROC_PERIOD = _getenv_int("GA_ROC_PERIOD", "10")
GA_MFI_PERIOD = _getenv_int("GA_MFI_PERIOD", "14")
GA_CMF_PERIOD = _getenv_int("GA_CMF_PERIOD", "20")
GA_DONCHIAN_PERIOD = _getenv_int("GA_DONCHIAN_PERIOD", "20")
GA_BB_SQUEEZE_WINDOW = _getenv_int("GA_BB_SQUEEZE_WINDOW", "50")
GA_BB_SQUEEZE_QUANTILE = _getenv_float("GA_BB_SQUEEZE_QUANTILE", "0.2")
GA_PIVOT_ORDER = _getenv_int("GA_PIVOT_ORDER", "3")
GA_PSAR_AF_START = _getenv_float("GA_PSAR_AF_START", "0.02")
GA_PSAR_AF_STEP = _getenv_float("GA_PSAR_AF_STEP", "0.02")
GA_PSAR_AF_MAX = _getenv_float("GA_PSAR_AF_MAX", "0.2")

File diff suppressed because it is too large Load Diff

0
data/ops/.gitkeep Normal file
View File

14
deepcoin/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
"""
DeepCoin 패키지 — WLD MTF 분석·정답·운영 단계.
"""
from pathlib import Path
import sys
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(_PROJECT_ROOT))
from deepcoin.paths import ensure_dirs
ensure_dirs()

View File

@@ -0,0 +1,6 @@
# analysis — 03·03b 기술적 분석
- **03 enrich**: `general_analysis_enrich_runner.py` — 봉 전구간 지표·패턴 → `docs/03_analysis/latest/`
- **03b GT 스냅샷**: `general_analysis_runner.py` — 정답 매수·매도 시점 MTF 상태 → `general_analysis_trades.csv`
실행은 `scripts/03_analyze_enrich.py`, `scripts/03_analyze_trades.py`만 사용합니다.

View File

View File

@@ -0,0 +1,153 @@
"""
general_analysis MTF 합성·정렬 점수.
"""
from __future__ import annotations
from typing import Any
import pandas as pd
from config import (
ALIGN_BB_POS_HIGH,
ALIGN_BB_POS_LOW,
ALIGN_RSI_CONFLICT_TIMING_HIGH,
ALIGN_RSI_CONFLICT_TIMING_LOW,
ALIGN_RSI_CONFLICT_TREND_HIGH,
ALIGN_RSI_CONFLICT_TREND_LOW,
ALIGN_RSI_OVERBOUGHT,
ALIGN_RSI_OVERSOLD,
)
from deepcoin.analysis.general_analysis_config import TIMING_INTERVALS, TREND_INTERVALS
from deepcoin.analysis.general_analysis_core import ga_col, interval_tf_prefix
def general_analysis_mtf_scores(
prefixed_row: dict[str, Any],
) -> dict[str, float | int | str]:
"""
간격 접두사가 붙은 스냅샷 행에서 MTF 합성 점수 계산.
Args:
prefixed_row: m3_ga_rsi 형태 flat dict.
Returns:
ga_align_* 점수.
"""
rsi_oversold = 0
rsi_overbought = 0
trend_up = 0
trend_down = 0
n_timing = 0
n_trend = 0
conflict = 0
for interval in TIMING_INTERVALS:
p = interval_tf_prefix(interval)
rk = f"{p}_RSI"
if rk in prefixed_row and prefixed_row[rk] is not None:
n_timing += 1
rsi = float(prefixed_row[rk])
if rsi < ALIGN_RSI_OVERSOLD:
rsi_oversold += 1
if rsi > ALIGN_RSI_OVERBOUGHT:
rsi_overbought += 1
for interval in TREND_INTERVALS:
p = interval_tf_prefix(interval)
sk = f"{p}_{ga_col('struct_trend')}"
if sk in prefixed_row:
n_trend += 1
t = prefixed_row[sk]
if t == "up":
trend_up += 1
elif t == "down":
trend_down += 1
m3_rsi = prefixed_row.get("m3_RSI")
d1_rsi = prefixed_row.get("d1_RSI")
if m3_rsi is not None and d1_rsi is not None:
if (
float(m3_rsi) < ALIGN_RSI_CONFLICT_TIMING_LOW
and float(d1_rsi) > ALIGN_RSI_CONFLICT_TREND_HIGH
):
conflict = 1
if (
float(m3_rsi) > ALIGN_RSI_CONFLICT_TIMING_HIGH
and float(d1_rsi) < ALIGN_RSI_CONFLICT_TREND_LOW
):
conflict = 1
timing_buy_align = rsi_oversold / max(len(TIMING_INTERVALS), 1)
timing_sell_align = rsi_overbought / max(len(TIMING_INTERVALS), 1)
return {
"ga_align_rsi_oversold_tf": rsi_oversold,
"ga_align_rsi_overbought_tf": rsi_overbought,
"ga_align_trend_up_tf": trend_up,
"ga_align_trend_down_tf": trend_down,
"ga_align_timing_buy_score": round(timing_buy_align, 3),
"ga_align_timing_sell_score": round(timing_sell_align, 3),
"ga_align_trend_score": round(
(trend_up - trend_down) / max(n_trend, 1), 3
),
"ga_align_mtf_conflict": conflict,
}
def general_analysis_mtf_vote_latest(
frames_enriched: dict[int, pd.DataFrame],
) -> dict[str, float | int | str]:
"""
각 TF 최신 완성봉 지표로 TF 가중 투표·필터 점수 산출.
Args:
frames_enriched: interval → enrich된 DataFrame.
Returns:
ga_vote_* 점수 (접두사 없음, ga_col로 감쌀 것).
"""
votes_buy = 0
votes_sell = 0
trend_ok = 0
n = 0
for interval in TIMING_INTERVALS:
df = frames_enriched.get(interval)
if df is None or df.empty:
continue
row = df.iloc[-1]
n += 1
rsi = row.get("RSI")
if rsi is not None and not pd.isna(rsi):
if float(rsi) < ALIGN_RSI_OVERSOLD:
votes_buy += 1
if float(rsi) > ALIGN_RSI_OVERBOUGHT:
votes_sell += 1
bb_pos = row.get("bb_pos")
if bb_pos is not None and float(bb_pos) < ALIGN_BB_POS_LOW:
votes_buy += 1
if bb_pos is not None and float(bb_pos) > ALIGN_BB_POS_HIGH:
votes_sell += 1
for interval in TREND_INTERVALS:
df = frames_enriched.get(interval)
if df is None or df.empty:
continue
row = df.iloc[-1]
st = row.get(ga_col("struct_trend"), "range")
if st == "up":
trend_ok += 1
elif st == "down":
trend_ok -= 1
return {
"vote_timing_buy": votes_buy,
"vote_timing_sell": votes_sell,
"vote_trend_score": trend_ok,
"vote_tf_used": n,
}
def general_analysis_vote_columns() -> list[str]:
return ["vote_timing_buy", "vote_timing_sell", "vote_trend_score", "vote_tf_used"]

View File

@@ -0,0 +1,114 @@
"""
general_analysis 캔들·차트 변환 (Heikin-Ashi, 복수봉 패턴).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from deepcoin.analysis.general_analysis_core import ga_col
def general_analysis_apply_candles(df: pd.DataFrame) -> pd.DataFrame:
"""
단일·복수 봉 캔들 패턴 및 Heikin-Ashi 컬럼 추가.
Args:
df: OHLCV.
Returns:
ga_* 캔들 컬럼이 추가된 DataFrame.
"""
out = df.copy()
o = out["Open"].astype(float)
h = out["High"].astype(float)
l = out["Low"].astype(float)
c = out["Close"].astype(float)
rng = (h - l).replace(0, np.nan)
body = (c - o).abs()
out[ga_col("body_ratio")] = (body / rng).fillna(0).clip(0, 1)
upper_wick = h - np.maximum(o, c)
lower_wick = np.minimum(o, c) - l
out[ga_col("upper_wick_ratio")] = (upper_wick / rng).fillna(0).clip(0, 1)
out[ga_col("lower_wick_ratio")] = (lower_wick / rng).fillna(0).clip(0, 1)
out[ga_col("bullish")] = (c > o).astype(int)
out[ga_col("bearish")] = (c < o).astype(int)
out[ga_col("hammer")] = (
(out[ga_col("lower_wick_ratio")] > 0.45) & (out[ga_col("body_ratio")] < 0.35)
).astype(int)
out[ga_col("shooting_star")] = (
(out[ga_col("upper_wick_ratio")] > 0.45) & (out[ga_col("body_ratio")] < 0.35)
).astype(int)
out[ga_col("doji")] = (out[ga_col("body_ratio")] < 0.1).astype(int)
prev_o, prev_c = o.shift(1), c.shift(1)
out[ga_col("bullish_engulfing")] = (
(c > o) & (prev_c < prev_o) & (c >= prev_o) & (o <= prev_c)
).astype(int)
out[ga_col("bearish_engulfing")] = (
(c < o) & (prev_c > prev_o) & (c <= prev_o) & (o >= prev_c)
).astype(int)
o2, c2 = o.shift(2), c.shift(2)
mid1 = (o.shift(1) + c.shift(1)) / 2
out[ga_col("morning_star")] = (
(c2 < o2)
& (abs(c.shift(1) - o.shift(1)) < rng.shift(1) * 0.15)
& (c > o)
& (c > mid1)
).astype(int)
out[ga_col("evening_star")] = (
(c2 > o2)
& (abs(c.shift(1) - o.shift(1)) < rng.shift(1) * 0.15)
& (c < o)
& (c < mid1)
).astype(int)
out[ga_col("three_white_soldiers")] = (
(c > o)
& (c.shift(1) > o.shift(1))
& (c.shift(2) > o.shift(2))
& (c > c.shift(1))
& (c.shift(1) > c.shift(2))
).astype(int)
out[ga_col("three_black_crows")] = (
(c < o)
& (c.shift(1) < o.shift(1))
& (c.shift(2) < o.shift(2))
& (c < c.shift(1))
& (c.shift(1) < c.shift(2))
).astype(int)
# Heikin-Ashi
ha_close = (o + h + l + c) / 4
ha_open = ha_close.copy()
ha_open.iloc[0] = (o.iloc[0] + c.iloc[0]) / 2
for i in range(1, len(out)):
ha_open.iloc[i] = (ha_open.iloc[i - 1] + ha_close.iloc[i - 1]) / 2
out[ga_col("ha_close")] = ha_close
out[ga_col("ha_open")] = ha_open
out[ga_col("ha_bull")] = (ha_close > ha_open).astype(int)
out[ga_col("ha_trend_up")] = (
(ha_close > ha_close.shift(1)) & (ha_close.shift(1) > ha_close.shift(2))
).astype(int)
return out
def general_analysis_candle_columns() -> list[str]:
return [
"body_ratio",
"hammer",
"shooting_star",
"doji",
"bullish_engulfing",
"bearish_engulfing",
"morning_star",
"evening_star",
"three_white_soldiers",
"three_black_crows",
"ha_bull",
"ha_trend_up",
]

View File

@@ -0,0 +1,159 @@
"""
general_analysis 차트 유형 (캔들·선·바·Renko·P&F).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from deepcoin.analysis.general_analysis_core import ga_col
def _renko_direction_series(close: pd.Series, brick: pd.Series) -> pd.Series:
"""ATR 기반 브릭 크기로 Renko 방향 (+1/-1/0) 시계열."""
n = len(close)
direction = pd.Series(0, index=close.index, dtype=int)
if n < 2:
return direction
price = float(close.iloc[0])
for i in range(1, n):
b = float(brick.iloc[i]) if not np.isnan(brick.iloc[i]) else float(close.diff().abs().median())
if b < 1e-9:
b = 1e-9
c = float(close.iloc[i])
if c >= price + b:
steps = int((c - price) // b)
direction.iloc[i] = 1
price += steps * b
elif c <= price - b:
steps = int((price - c) // b)
direction.iloc[i] = -1
price -= steps * b
return direction
def general_analysis_apply_chart_bars(df: pd.DataFrame) -> pd.DataFrame:
"""
봉 단위 차트 파생 컬럼 (선 기울기, Renko, P&F).
Args:
df: OHLCV (+ ga_atr_14 권장).
Returns:
ga_chart_* 시계열 컬럼 추가.
"""
out = df.copy()
c = out["Close"].astype(float)
h = out["High"].astype(float)
l = out["Low"].astype(float)
out[ga_col("chart_line_slope_1")] = c.diff()
out[ga_col("chart_bar_range_pct")] = (h - l) / c.replace(0, np.nan) * 100
from config import GA_ATR_PERIOD
brick = (
out[ga_col("atr_14")]
if ga_col("atr_14") in out.columns
else (h - l).rolling(GA_ATR_PERIOD).mean()
)
brick = brick.fillna(c.diff().abs().rolling(20).median()).replace(0, np.nan).bfill()
renko_dir = _renko_direction_series(c, brick)
out[ga_col("chart_renko_dir")] = renko_dir
out[ga_col("chart_renko_up")] = (renko_dir == 1).astype(int)
box = brick.fillna(1.0)
pnf = pd.Series(0, index=out.index, dtype=int)
col = 0
for i in range(1, len(c)):
b = float(box.iloc[i])
move = c.iloc[i] - c.iloc[i - 1]
if move >= b:
col += 1
pnf.iloc[i] = 1
elif move <= -b:
col -= 1
pnf.iloc[i] = -1
out[ga_col("chart_pnf_col")] = pnf
if "Volume" in out.columns:
v = out["Volume"].astype(float)
out[ga_col("chart_vol_spike")] = (v > v.rolling(20).mean() * 1.8).astype(int)
if ga_col("ha_trend_up") in out.columns:
out[ga_col("chart_ha_trend")] = out[ga_col("ha_trend_up")]
elif ga_col("ha_bull") in out.columns:
out[ga_col("chart_ha_trend")] = out[ga_col("ha_bull")]
return out
def general_analysis_chart_metrics(df: pd.DataFrame) -> dict[str, object]:
"""
lookback 구간 차트 요약 (마지막 봉 스냅샷용).
Returns:
ga_chart_* dict.
"""
res: dict[str, object] = {
"chart_type_candle": 1,
"chart_line_slope": 0.0,
"chart_bar_range_pct": 0.0,
"chart_ha_trend": 0,
"chart_renko_brick_up_ratio": 0.5,
"chart_renko_dir": 0,
"chart_pnf_col": 0,
"chart_vol_spike": 0,
}
if df is None or len(df) < 5:
return {ga_col(k): v for k, v in res.items()}
row = df.iloc[-1]
c = df["Close"].astype(float)
res["chart_line_slope"] = float((c.iloc[-1] - c.iloc[0]) / max(len(c) - 1, 1))
res["chart_bar_range_pct"] = float(
(df["High"].iloc[-1] - df["Low"].iloc[-1]) / max(c.iloc[-1], 1e-9) * 100
)
if ga_col("chart_renko_dir") in df.columns:
rd = df[ga_col("chart_renko_dir")].astype(float)
up = (rd == 1).sum()
down = (rd == -1).sum()
res["chart_renko_brick_up_ratio"] = round(up / max(up + down, 1), 3)
res["chart_renko_dir"] = int(rd.iloc[-1])
else:
diff = c.diff().fillna(0)
up = (diff > 0).sum()
down = (diff < 0).sum()
res["chart_renko_brick_up_ratio"] = round(up / max(up + down, 1), 3)
if ga_col("chart_pnf_col") in df.columns:
res["chart_pnf_col"] = int(df[ga_col("chart_pnf_col")].iloc[-1])
if ga_col("chart_ha_trend") in df.columns:
res["chart_ha_trend"] = int(row[ga_col("chart_ha_trend")])
elif ga_col("ha_trend_up") in df.columns:
res["chart_ha_trend"] = int(row[ga_col("ha_trend_up")])
if ga_col("chart_vol_spike") in df.columns:
res["chart_vol_spike"] = int(row[ga_col("chart_vol_spike")])
elif "Volume" in df.columns:
v = df["Volume"].astype(float)
res["chart_vol_spike"] = int(v.iloc[-1] > v.iloc[-20:].mean() * 1.8)
return {ga_col(k): v for k, v in res.items()}
def general_analysis_chart_columns() -> list[str]:
return [
"chart_type_candle",
"chart_line_slope",
"chart_line_slope_1",
"chart_bar_range_pct",
"chart_ha_trend",
"chart_renko_brick_up_ratio",
"chart_renko_dir",
"chart_renko_up",
"chart_pnf_col",
"chart_vol_spike",
]

View File

@@ -0,0 +1,26 @@
"""
general_analysis MTF 설정 (config.py 재노출).
"""
from __future__ import annotations
from config import (
CONTEXT_TAIL_ROWS,
GA_COL_PREFIX,
GENERAL_ANALYSIS_INTERVALS,
GROUND_TRUTH_FILE,
INTERVAL_PREFIX,
LOOKBACK_BARS,
REPORTS_ANALYSIS_CAPABILITY_HTML,
REPORTS_ANALYSIS_LATEST_DIR,
REPORTS_ANALYSIS_REPORT_HTML,
REPORTS_ANALYSIS_TRADES_CSV,
TIMING_INTERVALS,
TREND_INTERVALS,
)
DEFAULT_TRADES_FILE = GROUND_TRUTH_FILE
DEFAULT_OUTPUT_CSV = str(REPORTS_ANALYSIS_TRADES_CSV)
DEFAULT_OUTPUT_HTML = str(REPORTS_ANALYSIS_REPORT_HTML)
DEFAULT_CAPABILITY_HTML = str(REPORTS_ANALYSIS_CAPABILITY_HTML)
DEFAULT_LATEST_DIR = str(REPORTS_ANALYSIS_LATEST_DIR)

View File

@@ -0,0 +1,98 @@
"""
general_analysis lookback 컨텍스트 특징 (패턴·파동·VP·하모닉) 봉별 적용.
"""
from __future__ import annotations
import pandas as pd
from deepcoin.analysis.general_analysis_config import CONTEXT_TAIL_ROWS, LOOKBACK_BARS
from deepcoin.analysis.general_analysis_core import ga_col
from deepcoin.analysis.general_analysis_harmonic import (
general_analysis_harmonic_columns,
general_analysis_harmonic_snapshot,
)
from deepcoin.analysis.general_analysis_patterns import general_analysis_apply_patterns_to_bars
from deepcoin.analysis.general_analysis_volume import (
general_analysis_volume_columns,
general_analysis_volume_snapshot,
)
from deepcoin.analysis.general_analysis_wave import general_analysis_apply_wave_to_bars
def general_analysis_apply_volume_to_bars(
df: pd.DataFrame,
interval: int,
tail_rows: int | None = None,
) -> pd.DataFrame:
"""Volume Profile 컬럼을 최근 봉에 롤링 적용."""
out = df.copy()
for k in general_analysis_volume_columns():
out[ga_col(k)] = 0.0 if k != "vp_in_value_area" else 0
lb = LOOKBACK_BARS.get(interval, 80)
n = len(out)
if n < lb + 1:
return out
if tail_rows is None:
tail_rows = CONTEXT_TAIL_ROWS.get(interval, 5000)
start = max(lb, n - tail_rows)
for i in range(start, n):
snap = general_analysis_volume_snapshot(out.iloc[i - lb : i])
idx = out.index[i]
for k, v in snap.items():
out.at[idx, k] = v
return out
def general_analysis_apply_harmonic_to_bars(
df: pd.DataFrame,
interval: int,
tail_rows: int | None = None,
) -> pd.DataFrame:
"""하모닉 패턴 컬럼 롤링 적용."""
out = df.copy()
for k in general_analysis_harmonic_columns():
default = "none" if k == "harmonic_label" else 0
out[ga_col(k)] = default
lb = LOOKBACK_BARS.get(interval, 80)
n = len(out)
if n < lb + 1:
return out
if tail_rows is None:
tail_rows = CONTEXT_TAIL_ROWS.get(interval, 5000)
start = max(lb, n - tail_rows)
for i in range(start, n):
snap = general_analysis_harmonic_snapshot(out.iloc[i - lb : i])
idx = out.index[i]
for k, v in snap.items():
out.at[idx, k] = v
return out
def general_analysis_apply_context_features(
df: pd.DataFrame,
interval: int,
tail_rows: int | None = None,
) -> pd.DataFrame:
"""
패턴·파동·VP·하모닉 lookback 라벨을 봉 시계열에 병합.
Args:
df: general_analysis_enrich_bars 1단계 결과.
interval: 분봉 간격(분).
tail_rows: 롤링 적용 봉 수 상한.
Returns:
컨텍스트 ga_* 컬럼이 추가된 DataFrame.
"""
out = general_analysis_apply_patterns_to_bars(df, interval, tail_rows)
out = general_analysis_apply_wave_to_bars(out, interval, tail_rows)
out = general_analysis_apply_volume_to_bars(out, interval, tail_rows)
out = general_analysis_apply_harmonic_to_bars(out, interval, tail_rows)
return out

View File

@@ -0,0 +1,92 @@
"""
general_analysis 공통 유틸 (슬라이스·피벗·컬럼 접두사).
"""
from __future__ import annotations
from typing import Any
import numpy as np
import pandas as pd
from deepcoin.analysis.general_analysis_config import GA_COL_PREFIX, LOOKBACK_BARS
def ga_col(name: str) -> str:
"""general_analysis 출력 컬럼명."""
return f"{GA_COL_PREFIX}{name}"
def interval_tf_prefix(interval: int) -> str:
"""간격 접두사 (m3, d1)."""
from deepcoin.analysis.general_analysis_config import INTERVAL_PREFIX
return INTERVAL_PREFIX.get(interval, f"m{interval}")
def prefixed_snapshot(
row: pd.Series,
interval: int,
keys: list[str] | tuple[str, ...],
) -> dict[str, Any]:
"""한 봉의 ga_ 컬럼을 {m3_ga_rsi: ...} 형태로 변환."""
p = interval_tf_prefix(interval)
out: dict[str, Any] = {}
for k in keys:
col = ga_col(k)
if col in row.index:
v = row[col]
out[f"{p}_{col}"] = None if pd.isna(v) else v
return out
def slice_to_timestamp(df: pd.DataFrame, ts: pd.Timestamp) -> pd.DataFrame:
"""타점 시각 이전 완성봉만 (해당 시각 봉 미포함)."""
if df.empty:
return df
if not isinstance(df.index, pd.DatetimeIndex):
df = df.copy()
df.index = pd.to_datetime(df.index)
ts = pd.Timestamp(ts)
if ts.tzinfo is not None and df.index.tz is None:
ts = ts.tz_localize(None)
return df[df.index < ts].copy()
def lookback_slice(df: pd.DataFrame, interval: int, end_ts: pd.Timestamp) -> pd.DataFrame:
"""타점 직전 lookback 구간."""
sliced = slice_to_timestamp(df, end_ts)
n = LOOKBACK_BARS.get(interval, 80)
if len(sliced) > n:
return sliced.iloc[-n:].copy()
return sliced
def find_pivots(
highs: np.ndarray,
lows: np.ndarray,
order: int = 3,
) -> tuple[list[int], list[int]]:
"""국소 고점·저점 인덱스 (양쪽 order 봉보다 극값)."""
peak_idx: list[int] = []
trough_idx: list[int] = []
n = len(highs)
if n < order * 2 + 1:
return peak_idx, trough_idx
for i in range(order, n - order):
if highs[i] >= highs[i - order : i + order + 1].max():
peak_idx.append(i)
if lows[i] <= lows[i - order : i + order + 1].min():
trough_idx.append(i)
return peak_idx, trough_idx
def last_row_dict(df: pd.DataFrame, cols: list[str]) -> dict[str, Any]:
"""마지막 봉의 지정 컬럼 dict."""
if df.empty:
return {ga_col(c): None for c in cols}
row = df.iloc[-1]
return {
ga_col(c): (None if c not in row.index or pd.isna(row[c]) else row[c])
for c in cols
}

View File

@@ -0,0 +1,148 @@
"""
general_analysis 봉 데이터 전구간 enrich + 최신봉·기법 점검 리포트.
python scripts/03_analyze_enrich.py
python scripts/03_analyze_enrich.py --interval 1440
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pandas as pd
from config import CHART_LOOKBACK_DAYS, GA_DEFAULT_TAIL_EXPORT, SYMBOL
from deepcoin.analysis.general_analysis_align import (
general_analysis_mtf_scores,
general_analysis_mtf_vote_latest,
)
from deepcoin.analysis.general_analysis_config import (
DEFAULT_CAPABILITY_HTML,
DEFAULT_LATEST_DIR,
GENERAL_ANALYSIS_INTERVALS,
)
from deepcoin.analysis.general_analysis_core import ga_col, interval_tf_prefix
from deepcoin.analysis.general_analysis_pipeline import general_analysis_enrich_bars
from deepcoin.ops.monitor import Monitor
from deepcoin.data.mtf_bb import load_frames_from_db
def _latest_row_summary(df: pd.DataFrame, prefix: str) -> dict[str, object]:
"""최신 봉의 ga_·핵심 레거시 컬럼 요약."""
if df.empty:
return {}
row = df.iloc[-1]
out: dict[str, object] = {"dt": str(df.index[-1]), "tf": prefix}
for c in df.columns:
if c.startswith("ga_") or c in ("RSI", "bb_pos", "macd_hist", "stoch_k"):
v = row[c]
if pd.isna(v):
continue
out[c] = v
return out
def write_capability_html(
summaries: dict[str, dict[str, object]],
vote: dict[str, object],
path: Path,
) -> None:
"""기법 컬럼 존재 여부·최신값 요약 HTML."""
rows = ""
for tf, snap in summaries.items():
ga_cols = [k for k in snap if str(k).startswith("ga_")]
rows += f"<tr><td>{tf}</td><td>{snap.get('dt','')}</td><td>{len(ga_cols)}</td></tr>"
vote_rows = "".join(f"<li><code>{k}</code>: {v}</li>" for k, v in vote.items())
html = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8"/>
<title>general_analysis 기법 점검</title>
<style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f5f5f5; }}
table {{ border-collapse: collapse; width: 100%; background: #fff; }}
th, td {{ border: 1px solid #e2e8f0; padding: 8px; font-size: 0.88rem; }}
th {{ background: #e2e8f0; }}
</style></head><body>
<h1>general_analysis 기법 점검 ({SYMBOL})</h1>
<p>3분~일봉 enrich 완료. 최신 봉 기준 컬럼 수·MTF 투표.</p>
<h2>간격별 ga_ 컬럼 수</h2>
<table><thead><tr><th>TF</th><th>최신 시각</th><th>ga_ 컬럼 수</th></tr></thead>
<tbody>{rows}</tbody></table>
<h2>MTF 투표 (최신 봉)</h2>
<ul>{vote_rows}</ul>
<p>상세 CSV: <code>{DEFAULT_LATEST_DIR}/</code></p>
</body></html>"""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(html, encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(description="general_analysis 8TF enrich")
parser.add_argument("--interval", type=int, default=0, help="단일 간격만 (0=전체)")
parser.add_argument(
"--tail-export",
type=int,
default=GA_DEFAULT_TAIL_EXPORT,
help="CSV 저장 최근 N봉",
)
args = parser.parse_args()
from deepcoin.paths import ANALYSIS_CAPABILITY_HTML, ANALYSIS_LATEST_DIR
intervals = (
(args.interval,)
if args.interval > 0
else GENERAL_ANALYSIS_INTERVALS
)
print(f"=== general_analysis enrich {SYMBOL} ===")
mon = Monitor(cooldown_file=None)
frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
if not frames:
raise RuntimeError("coins.db 데이터 없음")
enriched: dict[int, pd.DataFrame] = {}
summaries: dict[str, dict[str, object]] = {}
out_dir = ANALYSIS_LATEST_DIR
out_dir.mkdir(parents=True, exist_ok=True)
for iv in intervals:
raw = frames.get(iv)
if raw is None or raw.empty:
print(f" skip {iv}: no data")
continue
p = interval_tf_prefix(iv)
print(f" [{p}] enrich {len(raw)} bars...")
ef = general_analysis_enrich_bars(raw, iv, full_context=True)
enriched[iv] = ef
tail = ef.tail(args.tail_export)
csv_path = out_dir / f"{p}_latest.csv"
tail.to_csv(csv_path, encoding="utf-8-sig")
print(f" -> {csv_path} ({len(tail)} rows x {len(tail.columns)} cols)")
summaries[p] = _latest_row_summary(ef, p)
flat_vote: dict[str, object] = {}
if len(enriched) >= 2:
for k, v in general_analysis_mtf_vote_latest(enriched).items():
flat_vote[ga_col(k)] = v
prefixed = {}
for iv, ef in enriched.items():
p = interval_tf_prefix(iv)
row = ef.iloc[-1]
for c in ("RSI", "bb_pos"):
if c in row.index:
prefixed[f"{p}_{c}"] = row[c]
st = row.get(ga_col("struct_trend"))
if st is not None:
prefixed[f"{p}_{ga_col('struct_trend')}"] = st
flat_vote.update(general_analysis_mtf_scores(prefixed))
write_capability_html(summaries, flat_vote, ANALYSIS_CAPABILITY_HTML)
print(f"점검 리포트: {cap_path}")
print("완료.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,72 @@
"""
general_analysis 하모닉 패턴 (Gartley, Bat 근사).
"""
from __future__ import annotations
import numpy as np
from deepcoin.analysis.general_analysis_core import find_pivots, ga_col
def _ratio(a: float, b: float) -> float:
if abs(b) < 1e-12:
return 0.0
return abs(a / b)
def _near(x: float, target: float, tol: float = 0.08) -> bool:
return abs(x - target) <= tol
def general_analysis_harmonic_snapshot(win) -> dict[str, object]:
"""
최근 5개 피벗으로 Gartley·Bat 유사 비율 검사.
Args:
win: OHLCV DataFrame.
Returns:
ga_harmonic_* dict.
"""
res: dict[str, object] = {
"harmonic_gartley": 0,
"harmonic_bat": 0,
"harmonic_label": "none",
}
if win is None or len(win) < 30:
return {ga_col(k): v for k, v in res.items()}
h = win["High"].astype(float).values
l = win["Low"].astype(float).values
peaks, troughs = find_pivots(h, l, order=2)
pivots = sorted([(i, "H", h[i]) for i in peaks] + [(i, "L", l[i]) for i in troughs])
if len(pivots) < 5:
return {ga_col(k): v for k, v in res.items()}
pts = pivots[-5:]
prices = [p[2] for p in pts]
xa = abs(prices[1] - prices[0])
ab = abs(prices[2] - prices[1])
bc = abs(prices[3] - prices[2])
cd = abs(prices[4] - prices[3])
if xa < 1e-9:
return {ga_col(k): v for k, v in res.items()}
r_ab = _ratio(ab, xa)
r_bc = _ratio(bc, ab) if ab > 1e-9 else 0.0
r_cd = _ratio(cd, bc) if bc > 1e-9 else 0.0
if _near(r_ab, 0.618) and 0.35 <= r_bc <= 0.95 and 1.1 <= r_cd <= 1.75:
res["harmonic_gartley"] = 1
res["harmonic_label"] = "gartley"
if _near(r_ab, 0.382) and 0.35 <= r_bc <= 0.95 and 1.5 <= r_cd <= 2.1:
res["harmonic_bat"] = 1
res["harmonic_label"] = "bat"
return {ga_col(k): v for k, v in res.items()}
def general_analysis_harmonic_columns() -> list[str]:
return ["harmonic_gartley", "harmonic_bat", "harmonic_label"]

View File

@@ -0,0 +1,382 @@
"""
general_analysis 확장 기술적 지표 (추세·모멘텀·변동성·거래량).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from config import (
BB_PERIOD,
BB_STD,
GA_ADX_PERIOD,
GA_ADX_TREND_THRESHOLD,
GA_AO_FAST,
GA_AO_SLOW,
GA_ATR_PERIOD,
GA_BB_SQUEEZE_QUANTILE,
GA_BB_SQUEEZE_WINDOW,
GA_CCI_PERIOD,
GA_CMF_PERIOD,
GA_DIVERGENCE_LOOKBACK,
GA_DONCHIAN_PERIOD,
GA_EMA_SPANS,
GA_HV_ANNUALIZE_SQRT,
GA_HV_PERCENTILE_WINDOW,
GA_HV_ROLLING_BARS,
GA_KELTNER_ATR_MULT,
GA_LINREG_WINDOW,
GA_MFI_PERIOD,
GA_PSAR_AF_MAX,
GA_PSAR_AF_START,
GA_PSAR_AF_STEP,
GA_ROC_PERIOD,
GA_SMA_PERIODS,
GA_SUPERTREND_ATR_MULT,
GA_VOL_MA_WINDOW,
GA_WILLIAMS_PERIOD,
)
from deepcoin.analysis.general_analysis_core import ga_col
from deepcoin.common.indicators import apply_bar_indicators
def _ema(series: pd.Series, span: int) -> pd.Series:
return series.ewm(span=span, adjust=False).mean()
def _parabolic_sar(
high: np.ndarray,
low: np.ndarray,
af_start: float = GA_PSAR_AF_START,
af_step: float = GA_PSAR_AF_STEP,
af_max: float = GA_PSAR_AF_MAX,
) -> tuple[np.ndarray, np.ndarray]:
"""
Parabolic SAR 시계열.
Returns:
(sar, bull_flag 0/1)
"""
n = len(high)
sar = np.zeros(n)
bull = np.ones(n, dtype=int)
if n < 2:
return sar, bull
is_bull = True
af = af_start
ep = high[0]
sar[0] = low[0]
for i in range(1, n):
prev_sar = sar[i - 1]
if is_bull:
sar[i] = prev_sar + af * (ep - prev_sar)
sar[i] = min(sar[i], low[i - 1], low[i] if i > 0 else low[i - 1])
if low[i] < sar[i]:
is_bull = False
sar[i] = ep
ep = low[i]
af = af_start
else:
if high[i] > ep:
ep = high[i]
af = min(af + af_step, af_max)
else:
sar[i] = prev_sar + af * (ep - prev_sar)
sar[i] = max(sar[i], high[i - 1], high[i] if i > 0 else high[i - 1])
if high[i] > sar[i]:
is_bull = True
sar[i] = ep
ep = high[i]
af = af_start
else:
if low[i] < ep:
ep = low[i]
af = min(af + af_step, af_max)
bull[i] = int(is_bull)
return sar, bull
def general_analysis_apply_indicators(df: pd.DataFrame) -> pd.DataFrame:
"""
기존 apply_bar_indicators 위에 ga_ 접두사 확장 지표를 추가합니다.
Args:
df: OHLCV.
Returns:
ga_* 컬럼이 추가된 DataFrame.
"""
out = apply_bar_indicators(df.copy())
o = out["Open"].astype(float)
h = out["High"].astype(float)
l = out["Low"].astype(float)
c = out["Close"].astype(float)
v = out["Volume"].astype(float)
# --- 추세: MA ---
sma_fast = GA_SMA_PERIODS[0] if GA_SMA_PERIODS else 5
sma_slow = GA_SMA_PERIODS[1] if len(GA_SMA_PERIODS) > 1 else 20
for p in GA_SMA_PERIODS:
ma = c.rolling(p).mean()
out[ga_col(f"sma_{p}")] = ma
out[ga_col(f"close_vs_sma_{p}_pct")] = (c / ma.replace(0, np.nan) - 1) * 100
ema_fast = GA_EMA_SPANS[0] if GA_EMA_SPANS else 12
ema_slow = GA_EMA_SPANS[1] if len(GA_EMA_SPANS) > 1 else 26
out[ga_col(f"ema_{ema_fast}")] = _ema(c, ema_fast)
out[ga_col(f"ema_{ema_slow}")] = _ema(c, ema_slow)
out[ga_col("golden_cross")] = (
(out[ga_col(f"sma_{sma_fast}")] > out[ga_col(f"sma_{sma_slow}")])
& (out[ga_col(f"sma_{sma_fast}")].shift(1) <= out[ga_col(f"sma_{sma_slow}")].shift(1))
).astype(int)
out[ga_col("death_cross")] = (
(out[ga_col(f"sma_{sma_fast}")] < out[ga_col(f"sma_{sma_slow}")])
& (out[ga_col(f"sma_{sma_fast}")].shift(1) >= out[ga_col(f"sma_{sma_slow}")].shift(1))
).astype(int)
# --- ATR / 변동성 ---
tr = pd.concat(
[
h - l,
(h - c.shift(1)).abs(),
(l - c.shift(1)).abs(),
],
axis=1,
).max(axis=1)
atr = tr.rolling(GA_ATR_PERIOD).mean()
out[ga_col("atr_14")] = atr
out[ga_col("atr_pct")] = atr / c.replace(0, np.nan) * 100
out[ga_col("bb_width_pct")] = out.get("BB_Width", (out["Upper"] - out["Lower"]) / out["MA"] * 100)
bw = out[ga_col("bb_width_pct")].astype(float)
out[ga_col("bb_squeeze")] = (
bw < bw.rolling(GA_BB_SQUEEZE_WINDOW).quantile(GA_BB_SQUEEZE_QUANTILE)
).astype(int)
dc = GA_DONCHIAN_PERIOD
out[ga_col("donchian_high_20")] = h.rolling(dc).max()
out[ga_col("donchian_low_20")] = l.rolling(dc).min()
out[ga_col("donchian_pos")] = (c - out[ga_col("donchian_low_20")]) / (
out[ga_col("donchian_high_20")] - out[ga_col("donchian_low_20")]
).replace(0, np.nan)
# --- 모멘텀: CCI, Williams %R ---
tp = (h + l + c) / 3
cci_period = GA_CCI_PERIOD
sma_tp = tp.rolling(cci_period).mean()
mad = tp.rolling(cci_period).apply(lambda x: np.abs(x - x.mean()).mean(), raw=True)
out[ga_col("cci_20")] = (tp - sma_tp) / (0.015 * mad.replace(0, np.nan))
out[ga_col("cci_oversold")] = (out[ga_col("cci_20")] < -100).astype(int)
out[ga_col("cci_overbought")] = (out[ga_col("cci_20")] > 100).astype(int)
hh = h.rolling(GA_WILLIAMS_PERIOD).max()
ll = l.rolling(GA_WILLIAMS_PERIOD).min()
out[ga_col("williams_r")] = (hh - c) / (hh - ll).replace(0, np.nan) * -100
out[ga_col("williams_oversold")] = (out[ga_col("williams_r")] < -80).astype(int)
out[ga_col("williams_overbought")] = (out[ga_col("williams_r")] > -20).astype(int)
div_lb = GA_DIVERGENCE_LOOKBACK
out[ga_col("roc_10")] = (c / c.shift(GA_ROC_PERIOD).replace(0, np.nan) - 1) * 100
# MFI
raw_mf = tp * v
pos_mf = raw_mf.where(tp > tp.shift(1), 0.0).rolling(GA_MFI_PERIOD).sum()
neg_mf = raw_mf.where(tp < tp.shift(1), 0.0).rolling(GA_MFI_PERIOD).sum()
mfr = pos_mf / neg_mf.replace(0, np.nan)
out[ga_col("mfi_14")] = 100 - (100 / (1 + mfr))
# MACD / RSI / Stoch 다이버전스
if "RSI" in out.columns and "macd_hist" in out.columns:
price_up = (c > c.shift(div_lb)).astype(int)
rsi_up = (out["RSI"] > out["RSI"].shift(div_lb)).astype(int)
macd_up = (out["macd_hist"] > out["macd_hist"].shift(div_lb)).astype(int)
out[ga_col("rsi_bull_div")] = ((price_up == 0) & (rsi_up == 1)).astype(int)
out[ga_col("rsi_bear_div")] = ((price_up == 1) & (rsi_up == 0)).astype(int)
out[ga_col("macd_bull_div")] = ((price_up == 0) & (macd_up == 1)).astype(int)
out[ga_col("macd_bear_div")] = ((price_up == 1) & (macd_up == 0)).astype(int)
if "stoch_k" in out.columns:
price_up = (c > c.shift(div_lb)).astype(int)
st_up = (out["stoch_k"] > out["stoch_k"].shift(div_lb)).astype(int)
out[ga_col("stoch_bull_div")] = ((price_up == 0) & (st_up == 1)).astype(int)
out[ga_col("stoch_bear_div")] = ((price_up == 1) & (st_up == 0)).astype(int)
# 봉 간 변화 (타점 Δ와 동일 정의, 전 구간)
if "RSI" in out.columns:
out[ga_col("rsi_delta_1")] = out["RSI"].diff()
if "macd_hist" in out.columns:
out[ga_col("macd_hist_delta_1")] = out["macd_hist"].diff()
if "stoch_k" in out.columns:
out[ga_col("stoch_k_delta_1")] = out["stoch_k"].diff()
# --- 거래량 ---
vol_ma = v.rolling(GA_VOL_MA_WINDOW).mean()
out[ga_col("vol_ma20")] = vol_ma
out[ga_col("vol_ratio")] = v / vol_ma.replace(0, np.nan)
out[ga_col("obv")] = (np.sign(c.diff().fillna(0)) * v).cumsum()
obv = out[ga_col("obv")].astype(float)
out[ga_col("obv_slope_10")] = obv - obv.shift(div_lb)
out[ga_col("obv_bull_div")] = (
(c < c.shift(div_lb)) & (obv > obv.shift(div_lb))
).astype(int)
out[ga_col("obv_bear_div")] = (
(c > c.shift(div_lb)) & (obv < obv.shift(div_lb))
).astype(int)
# CMF
mfv = ((c - l) - (h - c)) / (h - l).replace(0, np.nan) * v
out[ga_col("cmf_20")] = (
mfv.rolling(GA_CMF_PERIOD).sum() / v.rolling(GA_CMF_PERIOD).sum().replace(0, np.nan)
)
# Accumulation/Distribution Line
clv = ((c - l) - (h - c)) / (h - l).replace(0, np.nan)
out[ga_col("ad_line")] = (clv * v).cumsum()
ad = out[ga_col("ad_line")].astype(float)
out[ga_col("ad_slope_10")] = ad - ad.shift(div_lb)
# VWAP 근사 (누적, 세션 리셋 없음)
cum_vp = (tp * v).cumsum()
cum_v = v.cumsum().replace(0, np.nan)
out[ga_col("vwap")] = cum_vp / cum_v
out[ga_col("close_vs_vwap_pct")] = (c / out[ga_col("vwap")] - 1) * 100
# Keltner Channel
k_mid = _ema(c, BB_PERIOD)
out[ga_col("keltner_mid")] = k_mid
out[ga_col("keltner_upper")] = k_mid + GA_KELTNER_ATR_MULT * atr
out[ga_col("keltner_lower")] = k_mid - GA_KELTNER_ATR_MULT * atr
out[ga_col("keltner_pos")] = (c - out[ga_col("keltner_lower")]) / (
out[ga_col("keltner_upper")] - out[ga_col("keltner_lower")]
).replace(0, np.nan)
# Awesome Oscillator: median price SMA5 - SMA34
mp = (h + l) / 2
out[ga_col("ao")] = mp.rolling(GA_AO_FAST).mean() - mp.rolling(GA_AO_SLOW).mean()
out[ga_col("ao_bull")] = (
(out[ga_col("ao")] > 0) & (out[ga_col("ao")].shift(1) <= 0)
).astype(int)
out[ga_col("ao_bear")] = (
(out[ga_col("ao")] < 0) & (out[ga_col("ao")].shift(1) >= 0)
).astype(int)
# Historical Volatility (로그수익 20봉 표준편차, 연율화 계수 1=봉 단위)
log_ret = np.log(c / c.shift(1).replace(0, np.nan))
hv = log_ret.rolling(GA_HV_ROLLING_BARS).std() * GA_HV_ANNUALIZE_SQRT
out[ga_col("hv_20")] = hv
out[ga_col("hv_percentile")] = hv.rolling(GA_HV_PERCENTILE_WINDOW).apply(
lambda x: float((x[:-1] < x[-1]).mean()) if len(x) > 1 and not np.isnan(x[-1]) else 0.5,
raw=True,
)
# Parabolic SAR
sar, psar_bull = _parabolic_sar(h.values, l.values)
out[ga_col("psar")] = sar
out[ga_col("psar_bull")] = psar_bull
ps = pd.Series(psar_bull, index=out.index)
out[ga_col("psar_flip_bull")] = ((ps == 1) & (ps.shift(1) == 0)).astype(int)
out[ga_col("psar_flip_bear")] = ((ps == 0) & (ps.shift(1) == 1)).astype(int)
# ADX
up_move = h.diff()
down_move = -l.diff()
plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
atr_safe = atr.replace(0, np.nan)
plus_di = 100 * pd.Series(plus_dm, index=out.index).rolling(GA_ADX_PERIOD).mean() / atr_safe
minus_di = 100 * pd.Series(minus_dm, index=out.index).rolling(GA_ADX_PERIOD).mean() / atr_safe
dx = (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan) * 100
out[ga_col("adx_14")] = dx.rolling(GA_ADX_PERIOD).mean()
out[ga_col("plus_di")] = plus_di
out[ga_col("minus_di")] = minus_di
out[ga_col("adx_trending")] = (out[ga_col("adx_14")] > GA_ADX_TREND_THRESHOLD).astype(int)
# Supertrend 방향 (ATR 밴드)
hl2 = (h + l) / 2
upper = hl2 + GA_SUPERTREND_ATR_MULT * atr
lower = hl2 - GA_SUPERTREND_ATR_MULT * atr
out[ga_col("supertrend_bull")] = (c > lower).astype(int)
# Linear regression slope 20
def _lin_slope(y: np.ndarray) -> float:
if len(y) < 2:
return 0.0
x = np.arange(len(y))
coef = np.polyfit(x, y, 1)
return float(coef[0])
out[ga_col("linreg_slope_20")] = c.rolling(GA_LINREG_WINDOW).apply(_lin_slope, raw=True)
def _lin_r2(y: np.ndarray) -> float:
if len(y) < 3:
return 0.0
x = np.arange(len(y))
coef = np.polyfit(x, y, 1)
pred = coef[0] * x + coef[1]
ss_res = ((y - pred) ** 2).sum()
ss_tot = ((y - y.mean()) ** 2).sum()
if ss_tot < 1e-12:
return 0.0
return float(1 - ss_res / ss_tot)
out[ga_col("linreg_r2_20")] = c.rolling(GA_LINREG_WINDOW).apply(_lin_r2, raw=True)
return out
def general_analysis_indicator_columns() -> list[str]:
"""스냅샷용 ga_ 지표 컬럼 목록."""
return [
"sma_5",
"sma_20",
"sma_60",
"close_vs_sma_20_pct",
"golden_cross",
"death_cross",
"atr_14",
"atr_pct",
"bb_squeeze",
"donchian_pos",
"cci_20",
"cci_oversold",
"cci_overbought",
"williams_r",
"williams_oversold",
"williams_overbought",
"roc_10",
"mfi_14",
"rsi_bull_div",
"rsi_bear_div",
"macd_bull_div",
"macd_bear_div",
"stoch_bull_div",
"stoch_bear_div",
"rsi_delta_1",
"macd_hist_delta_1",
"stoch_k_delta_1",
"keltner_pos",
"ao",
"ao_bull",
"ao_bear",
"hv_20",
"hv_percentile",
"ad_line",
"ad_slope_10",
"vol_ratio",
"obv_slope_10",
"obv_bull_div",
"obv_bear_div",
"cmf_20",
"close_vs_vwap_pct",
"adx_14",
"adx_trending",
"supertrend_bull",
"linreg_slope_20",
"linreg_r2_20",
"psar",
"psar_bull",
"psar_flip_bull",
"psar_flip_bear",
]

View File

@@ -0,0 +1,302 @@
"""
general_analysis 차트·가격 패턴 (반전·지속·박스).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from config import GA_PATTERN_TOLERANCE_PCT, GA_PIVOT_ORDER
from deepcoin.analysis.general_analysis_core import find_pivots, ga_col, last_row_dict
def _pct_diff(a: float, b: float) -> float:
return abs(a - b) / max(abs(a), abs(b), 1e-9) * 100
def general_analysis_detect_patterns(win: pd.DataFrame) -> dict[str, int | float | str | None]:
"""
lookback 윈도우 마지막 봉 기준 패턴 라벨 (0/1 및 요약).
Args:
win: OHLCV (+ 지표 선택).
Returns:
ga_pattern_* 키 dict (접두사 없음, ga_col로 감쌈).
"""
res: dict[str, int | float | str | None] = {
"pattern_double_top": 0,
"pattern_double_bottom": 0,
"pattern_head_shoulders": 0,
"pattern_inv_head_shoulders": 0,
"pattern_triangle_sym": 0,
"pattern_triangle_asc": 0,
"pattern_triangle_desc": 0,
"pattern_flag_bull": 0,
"pattern_flag_bear": 0,
"pattern_wedge_rising": 0,
"pattern_wedge_falling": 0,
"pattern_rectangle": 0,
"pattern_channel_up": 0,
"pattern_channel_down": 0,
"pattern_measured_move": 0,
"pattern_rounding_top": 0,
"pattern_rounding_bottom": 0,
"pattern_gap_up": 0,
"pattern_gap_down": 0,
"pattern_v_bottom": 0,
"pattern_spike_top": 0,
"pattern_triple_top": 0,
"pattern_triple_bottom": 0,
"pattern_cup_handle": 0,
"pattern_keystone_bull": 0,
"pattern_keystone_bear": 0,
"pattern_island_top": 0,
"pattern_island_bottom": 0,
"pattern_label": "none",
}
if win is None or len(win) < 20:
return res
h = win["High"].astype(float).values
l = win["Low"].astype(float).values
c = win["Close"].astype(float).values
peaks, troughs = find_pivots(h, l, order=GA_PIVOT_ORDER)
tol = GA_PATTERN_TOLERANCE_PCT
if len(peaks) >= 3:
p1, p2, p3 = peaks[-3], peaks[-2], peaks[-1]
if (
_pct_diff(h[p1], h[p2]) < tol
and _pct_diff(h[p2], h[p3]) < tol
and p1 < p2 < p3
):
res["pattern_triple_top"] = 1
res["pattern_label"] = "triple_top"
if len(troughs) >= 3:
t1, t2, t3 = troughs[-3], troughs[-2], troughs[-1]
if (
_pct_diff(l[t1], l[t2]) < tol
and _pct_diff(l[t2], l[t3]) < tol
and t1 < t2 < t3
):
res["pattern_triple_bottom"] = 1
res["pattern_label"] = "triple_bottom"
if len(peaks) >= 2:
p1, p2 = peaks[-2], peaks[-1]
if _pct_diff(h[p1], h[p2]) < tol:
res["pattern_double_top"] = 1
if res["pattern_label"] == "none":
res["pattern_label"] = "double_top"
if len(troughs) >= 2:
t1, t2 = troughs[-2], troughs[-1]
if _pct_diff(l[t1], l[t2]) < tol:
res["pattern_double_bottom"] = 1
res["pattern_label"] = "double_bottom"
if len(peaks) >= 3:
i, j, k = peaks[-3], peaks[-2], peaks[-1]
if h[j] > h[i] and h[j] > h[k] and _pct_diff(h[i], h[k]) < tol * 1.5:
res["pattern_head_shoulders"] = 1
res["pattern_label"] = "head_shoulders"
if len(troughs) >= 3:
i, j, k = troughs[-3], troughs[-2], troughs[-1]
if l[j] < l[i] and l[j] < l[k] and _pct_diff(l[i], l[k]) < tol * 1.5:
res["pattern_inv_head_shoulders"] = 1
res["pattern_label"] = "inv_head_shoulders"
n = len(win)
x = np.arange(n)
high_slope = np.polyfit(x, h, 1)[0]
low_slope = np.polyfit(x, l, 1)[0]
if high_slope < 0 and low_slope > 0:
res["pattern_triangle_sym"] = 1
if res["pattern_label"] == "none":
res["pattern_label"] = "triangle_sym"
if high_slope < 0 and low_slope > 0 and low_slope > abs(high_slope) * 0.5:
res["pattern_triangle_asc"] = 1
if high_slope < 0 and low_slope < 0 and abs(high_slope) > abs(low_slope) * 0.5:
res["pattern_triangle_desc"] = 1
rng_pct = (h.max() - l.min()) / max(c[-1], 1e-9) * 100
if rng_pct < 8 and abs(high_slope) < c[-1] * 0.0001:
res["pattern_rectangle"] = 1
if res["pattern_label"] == "none":
res["pattern_label"] = "rectangle"
leg = max(n // 3, 5)
if n > leg * 2:
first_move = (c[leg] - c[0]) / max(c[0], 1e-9) * 100
channel = (c[-1] - c[-leg]) / max(c[-leg], 1e-9) * 100
if first_move > 5 and abs(channel) < 3:
res["pattern_flag_bull"] = 1
res["pattern_label"] = "flag_bull"
if first_move < -5 and abs(channel) < 3:
res["pattern_flag_bear"] = 1
res["pattern_label"] = "flag_bear"
if high_slope > 0 and low_slope > 0:
res["pattern_wedge_rising"] = 1
if high_slope < 0 and low_slope < 0:
res["pattern_wedge_falling"] = 1
if high_slope > 0 and low_slope > 0:
res["pattern_channel_up"] = 1
if high_slope < 0 and low_slope < 0:
res["pattern_channel_down"] = 1
if len(c) >= 15:
mid = len(c) // 2
first_half = c[:mid].mean()
second_half = c[mid:].mean()
if c[0] > c[mid] * 1.08 and c[-1] > c[mid] * 1.05:
res["pattern_v_bottom"] = 1
res["pattern_label"] = "v_bottom"
if c[0] < c[-1] * 0.92 and c.max() > c[0] * 1.1:
res["pattern_spike_top"] = 1
o = win["Open"].astype(float).values
gap_ups: list[int] = []
gap_downs: list[int] = []
for i in range(1, min(30, n)):
if l[i] > h[i - 1]:
res["pattern_gap_up"] = 1
gap_ups.append(i)
if h[i] < l[i - 1]:
res["pattern_gap_down"] = 1
gap_downs.append(i)
for gi in gap_ups:
for gd in gap_downs:
if gd > gi and h[gi] < l[gd]:
res["pattern_island_top"] = 1
res["pattern_label"] = "island_top"
if gd > gi and l[gi] > h[gd]:
res["pattern_island_bottom"] = 1
res["pattern_label"] = "island_bottom"
# 키리스톤: 상단 수평 + 하단 상승(역키리스톤) 또는 하단 수평 + 상단 하락
if abs(high_slope) < c[-1] * 0.00005 and low_slope > 0:
res["pattern_keystone_bull"] = 1
if res["pattern_label"] == "none":
res["pattern_label"] = "keystone_bull"
if abs(low_slope) < c[-1] * 0.00005 and high_slope < 0:
res["pattern_keystone_bear"] = 1
if res["pattern_label"] == "none":
res["pattern_label"] = "keystone_bear"
# 컵앤핸들: 전반 U자 + 후반 15% 소폭 조정
if n >= 40:
cup_len = int(n * 0.65)
handle_len = max(int(n * 0.15), 5)
cup = c[:cup_len]
handle = c[-handle_len:]
rim = float(max(cup[0], cup[-1]))
bottom = float(cup.min())
depth = rim - bottom
if depth > rim * 0.08 and float(cup[-1]) > bottom + depth * 0.5:
handle_pull = float(handle.max() - handle.min())
if handle_pull < depth * 0.5 and float(c[-1]) >= rim * 0.98:
res["pattern_cup_handle"] = 1
res["pattern_label"] = "cup_handle"
if len(c) >= 30:
ma = pd.Series(c).rolling(10).mean()
if float(ma.iloc[-1]) > float(ma.iloc[-15]) > float(ma.iloc[-30]):
res["pattern_rounding_bottom"] = 1
if float(ma.iloc[-1]) < float(ma.iloc[-15]) < float(ma.iloc[-30]):
res["pattern_rounding_top"] = 1
if len(peaks) >= 2 and len(troughs) >= 2:
leg_h = h[peaks[-1]] - l[troughs[-1]]
if leg_h > 0 and c[-1] >= l[troughs[-1]] + leg_h * 0.9:
res["pattern_measured_move"] = 1
return res
def general_analysis_pattern_snapshot(win: pd.DataFrame) -> dict[str, object]:
"""패턴 dict → ga_pattern_* 컬럼명."""
raw = general_analysis_detect_patterns(win)
return {ga_col(k): v for k, v in raw.items()}
def general_analysis_pattern_columns() -> list[str]:
return [
"pattern_double_top",
"pattern_double_bottom",
"pattern_head_shoulders",
"pattern_inv_head_shoulders",
"pattern_triangle_sym",
"pattern_triangle_asc",
"pattern_triangle_desc",
"pattern_flag_bull",
"pattern_flag_bear",
"pattern_wedge_rising",
"pattern_wedge_falling",
"pattern_rectangle",
"pattern_channel_up",
"pattern_channel_down",
"pattern_measured_move",
"pattern_rounding_top",
"pattern_rounding_bottom",
"pattern_gap_up",
"pattern_gap_down",
"pattern_v_bottom",
"pattern_spike_top",
"pattern_triple_top",
"pattern_triple_bottom",
"pattern_cup_handle",
"pattern_keystone_bull",
"pattern_keystone_bear",
"pattern_island_top",
"pattern_island_bottom",
"pattern_label",
]
def general_analysis_apply_patterns_to_bars(
df: pd.DataFrame,
interval: int,
tail_rows: int | None = None,
) -> pd.DataFrame:
"""
lookback 윈도우 패턴 라벨을 봉별 컬럼으로 채움 (최근 tail_rows만, 성능).
Args:
df: OHLCV (+ 선택적 지표).
interval: 분봉 간격.
tail_rows: None이면 전체(8천봉 이하) 또는 config tail.
Returns:
ga_pattern_* 컬럼이 추가된 DataFrame.
"""
from deepcoin.analysis.general_analysis_config import CONTEXT_TAIL_ROWS, LOOKBACK_BARS
out = df.copy()
lb = LOOKBACK_BARS.get(interval, 80)
keys = [k for k in general_analysis_pattern_columns() if k != "pattern_label"]
for k in keys:
out[ga_col(k)] = 0
out[ga_col("pattern_label")] = "none"
n = len(out)
if n < lb + 1:
return out
if tail_rows is None:
tail_rows = CONTEXT_TAIL_ROWS.get(interval, 5000)
start = max(lb, n - tail_rows)
for i in range(start, n):
win = out.iloc[i - lb : i]
det = general_analysis_detect_patterns(win)
idx = out.index[i]
for k, v in det.items():
out.at[idx, ga_col(k)] = v
return out

View File

@@ -0,0 +1,154 @@
"""
general_analysis 전체 파이프라인 (지표·캔들·한 봉 특징).
"""
from __future__ import annotations
import pandas as pd
from deepcoin.common.candle_features import compute_bar_features
from deepcoin.analysis.general_analysis_candles import (
general_analysis_apply_candles,
general_analysis_candle_columns,
)
from deepcoin.analysis.general_analysis_chart import (
general_analysis_apply_chart_bars,
general_analysis_chart_columns,
general_analysis_chart_metrics,
)
from deepcoin.analysis.general_analysis_context import general_analysis_apply_context_features
from deepcoin.analysis.general_analysis_harmonic import (
general_analysis_harmonic_columns,
general_analysis_harmonic_snapshot,
)
from deepcoin.analysis.general_analysis_volume import (
general_analysis_volume_columns,
general_analysis_volume_snapshot,
)
from deepcoin.analysis.general_analysis_core import ga_col, lookback_slice
from deepcoin.analysis.general_analysis_indicators import (
general_analysis_apply_indicators,
general_analysis_indicator_columns,
)
from deepcoin.analysis.general_analysis_patterns import (
general_analysis_pattern_columns,
general_analysis_pattern_snapshot,
)
from deepcoin.analysis.general_analysis_wave import (
general_analysis_wave_columns,
general_analysis_wave_snapshot,
)
def general_analysis_enrich_bars(
df: pd.DataFrame,
interval: int | None = None,
*,
full_context: bool = True,
) -> pd.DataFrame:
"""
OHLCV → candle_features + ga 지표 + 캔들 + 차트 + (선택) lookback 컨텍스트.
Args:
df: raw OHLCV.
interval: 분봉 간격. full_context=True일 때 필수.
full_context: 패턴·VP·파동·하모닉 롤링 적용.
Returns:
전체 특징 컬럼 DataFrame.
"""
base = compute_bar_features(df)
out = general_analysis_apply_indicators(base)
out = general_analysis_apply_candles(out)
out = general_analysis_apply_chart_bars(out)
if full_context and interval is not None:
out = general_analysis_apply_context_features(out, interval)
return out
def general_analysis_snapshot_at_bar(
enriched: pd.DataFrame,
ts: pd.Timestamp,
interval: int,
) -> dict[str, object]:
"""
타점 시각 직전 완성봉 + lookback 패턴·파동·차트 메타.
Args:
enriched: general_analysis_enrich_bars 결과.
ts: 타점 시각.
interval: 분봉 간격.
Returns:
flat dict (ga_ 키 + legacy bb/rsi where present).
"""
win = lookback_slice(enriched, interval, ts)
snap: dict[str, object] = {}
if win.empty:
return snap
row = win.iloc[-1]
legacy_cols = [
"bb_pos",
"RSI",
"macd_hist",
"stoch_k",
"stoch_d",
"macd_line",
"macd_signal",
"BB_Width",
]
for c in legacy_cols:
if c in row.index and not pd.isna(row[c]):
snap[c] = float(row[c]) if isinstance(row[c], (int, float)) else row[c]
for c in general_analysis_indicator_columns():
col = ga_col(c)
if col in row.index:
v = row[col]
snap[col] = None if pd.isna(v) else v
for c in general_analysis_candle_columns():
col = ga_col(c)
if col in row.index:
snap[col] = int(row[col]) if not pd.isna(row[col]) else 0
pat_cols = [ga_col(c) for c in general_analysis_pattern_columns()]
if pat_cols and pat_cols[0] in enriched.columns:
for col in pat_cols:
if col in row.index:
snap[col] = row[col]
else:
snap.update(general_analysis_pattern_snapshot(win))
wave_cols = [ga_col(c) for c in general_analysis_wave_columns()]
if wave_cols and wave_cols[0] in enriched.columns:
for col in wave_cols:
if col in row.index:
snap[col] = row[col]
else:
snap.update(general_analysis_wave_snapshot(win))
snap.update(general_analysis_volume_snapshot(win))
snap.update(general_analysis_harmonic_snapshot(win))
snap.update(general_analysis_chart_metrics(win))
return snap
def general_analysis_all_snapshot_keys() -> list[str]:
"""CSV 헤더용 전체 키 목록 (간격 접두사 제외)."""
keys = list(legacy_snapshot_keys())
keys += [ga_col(c) for c in general_analysis_indicator_columns()]
keys += [ga_col(c) for c in general_analysis_candle_columns()]
keys += [ga_col(c) for c in general_analysis_pattern_columns()]
keys += [ga_col(c) for c in general_analysis_wave_columns()]
keys += [ga_col(c) for c in general_analysis_chart_columns()]
keys += [ga_col(c) for c in general_analysis_volume_columns()]
keys += [ga_col(c) for c in general_analysis_harmonic_columns()]
return keys
def legacy_snapshot_keys() -> list[str]:
return ["bb_pos", "RSI", "macd_hist", "stoch_k", "stoch_d"]

View File

@@ -0,0 +1,73 @@
"""
general_analysis HTML 요약 리포트.
"""
from __future__ import annotations
from pathlib import Path
import pandas as pd
from deepcoin.analysis.general_analysis_config import DEFAULT_OUTPUT_CSV, DEFAULT_OUTPUT_HTML
def write_analysis_report(
csv_path: Path | str = DEFAULT_OUTPUT_CSV,
html_path: Path | str = DEFAULT_OUTPUT_HTML,
) -> Path:
"""
스냅샷 CSV를 읽어 모듈별 컬럼 수·샘플 테이블 HTML 생성.
Returns:
HTML 경로.
"""
df = pd.read_csv(csv_path)
html_out = Path(html_path)
html_out.parent.mkdir(parents=True, exist_ok=True)
modules = {
"지표 (ga_)": [c for c in df.columns if "_ga_" in c or c.startswith("ga_")],
"패턴": [c for c in df.columns if "ga_pattern_" in c],
"파동·구조": [c for c in df.columns if "ga_struct_" in c or "ga_elliott" in c or "ga_wyckoff" in c or "ga_fib_" in c],
"차트": [c for c in df.columns if "ga_chart_" in c],
"MTF 합성": [c for c in df.columns if "ga_align_" in c],
"레거시": [c for c in df.columns if c.endswith("_RSI") or c.endswith("_bb_pos")],
}
summary_rows = ""
for name, cols in modules.items():
summary_rows += f"<tr><td>{name}</td><td>{len(cols)}</td></tr>"
sample = df.head(5)[
["dt", "action", "price", "ga_align_timing_buy_score", "ga_align_mtf_conflict", "d1_RSI", "m3_RSI"]
].to_html(index=False, classes="tbl") if "d1_RSI" in df.columns else df.head(3).to_html(index=False)
buy_mean = df[df["action"] == "buy"]["ga_align_timing_buy_score"].mean() if "ga_align_timing_buy_score" in df.columns else 0
sell_mean = df[df["action"] == "sell"]["ga_align_timing_sell_score"].mean() if "ga_align_timing_sell_score" in df.columns else 0
content = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8"/>
<title>general_analysis 실행 리포트</title>
<style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f5f5f5; }}
table.tbl {{ border-collapse: collapse; width: 100%; background: #fff; font-size: 0.85rem; }}
th, td {{ border: 1px solid #e2e8f0; padding: 6px 8px; }}
th {{ background: #e2e8f0; }}
</style></head><body>
<h1>general_analysis 실행 리포트</h1>
<p>타점 {len(df)}건 · 컬럼 {len(df.columns)}개 · CSV: {csv_path}</p>
<h2>모듈별 컬럼 수</h2>
<table class="tbl"><thead><tr><th>모듈</th><th>컬럼 수</th></tr></thead>
<tbody>{summary_rows}</tbody></table>
<h2>MTF 합성 평균</h2>
<ul>
<li>매수 타점 timing_buy_score 평균: {buy_mean:.3f}</li>
<li>매도 타점 timing_sell_score 평균: {sell_mean:.3f}</li>
</ul>
<h2>샘플 5건</h2>
{sample}
<p>전체 데이터: <code>{csv_path}</code></p>
</body></html>"""
html_out.write_text(content, encoding="utf-8")
print(f"리포트: {html_out}")
return html_out

View File

@@ -0,0 +1,71 @@
"""
general_analysis 실행 진입점.
python scripts/03_analyze_trades.py
python scripts/03_analyze_trades.py --limit 20 # 테스트용 타점 수 제한
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from config import CHART_LOOKBACK_DAYS, SYMBOL
from deepcoin.analysis.general_analysis_config import (
DEFAULT_OUTPUT_CSV,
DEFAULT_OUTPUT_HTML,
DEFAULT_TRADES_FILE,
)
from deepcoin.analysis.general_analysis_report import write_analysis_report
from deepcoin.analysis.general_analysis_snapshot import export_trade_snapshots
from deepcoin.ops.monitor import Monitor
from deepcoin.data.mtf_bb import load_frames_from_db
def main() -> None:
"""ground truth 타점 MTF general_analysis 스냅샷 생성."""
parser = argparse.ArgumentParser(description="general_analysis MTF 타점 분석")
parser.add_argument("--limit", type=int, default=0, help="타점 수 제한 (0=전체)")
parser.add_argument("--trades", type=str, default=DEFAULT_TRADES_FILE)
parser.add_argument("--csv", type=str, default=DEFAULT_OUTPUT_CSV)
parser.add_argument("--html", type=str, default=DEFAULT_OUTPUT_HTML)
args = parser.parse_args()
from deepcoin.paths import REPORTS_ANALYSIS
trades_path = Path(args.trades)
data = json.loads(trades_path.read_text(encoding="utf-8"))
trades = data.get("trades") or []
if args.limit > 0:
trades = trades[: args.limit]
print(f"테스트 모드: 타점 {args.limit}건만")
print(f"=== general_analysis {SYMBOL} (lookback {CHART_LOOKBACK_DAYS}일) ===")
mon = Monitor(cooldown_file=None)
frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
if not frames:
raise RuntimeError("coins.db 데이터 없음")
# limit 시 임시 trades 파일
if args.limit > 0:
tmp = REPORTS_ANALYSIS / "_ga_trades_subset.json"
tmp.parent.mkdir(exist_ok=True)
subset = {**data, "trades": trades}
tmp.write_text(json.dumps(subset, ensure_ascii=False), encoding="utf-8")
trades_path = tmp
from deepcoin.analysis.general_analysis_snapshot import build_trade_mtf_snapshots
csv_path = Path(args.csv)
df = build_trade_mtf_snapshots(frames, trades)
csv_path.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(csv_path, index=False, encoding="utf-8-sig")
print(f"저장: {csv_path} ({len(df)}× {len(df.columns)}열)")
write_analysis_report(csv_path, Path(args.html))
print("완료.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,100 @@
"""
general_analysis ground truth 타점 MTF 스냅샷 생성.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pandas as pd
from deepcoin.analysis.general_analysis_align import general_analysis_mtf_scores
from deepcoin.analysis.general_analysis_config import (
DEFAULT_OUTPUT_CSV,
DEFAULT_TRADES_FILE,
GENERAL_ANALYSIS_INTERVALS,
)
from deepcoin.analysis.general_analysis_core import interval_tf_prefix
from deepcoin.analysis.general_analysis_pipeline import general_analysis_enrich_bars, general_analysis_snapshot_at_bar
from deepcoin.ground_truth.ground_truth import load_ground_truth
def _prefixed_snap(snap: dict[str, Any], interval: int) -> dict[str, Any]:
p = interval_tf_prefix(interval)
return {f"{p}_{k}": v for k, v in snap.items()}
def build_trade_mtf_snapshots(
frames: dict[int, pd.DataFrame],
trades: list[dict[str, Any]],
) -> pd.DataFrame:
"""
모든 타점에 대해 8개 간격 general_analysis 스냅샷.
Args:
frames: interval → OHLCV.
trades: ground_truth trades.
Returns:
wide DataFrame (1 row per trade).
"""
enriched: dict[int, pd.DataFrame] = {}
for iv in GENERAL_ANALYSIS_INTERVALS:
raw = frames.get(iv)
if raw is None or raw.empty:
continue
print(f" [GA] {interval_tf_prefix(iv)} 봉 지표 계산 ({len(raw)}봉)...")
enriched[iv] = general_analysis_enrich_bars(raw, iv, full_context=True)
rows: list[dict[str, Any]] = []
for i, t in enumerate(sorted(trades, key=lambda x: x["dt"])):
ts = pd.Timestamp(t["dt"])
row: dict[str, Any] = {
"trade_idx": i,
"dt": t["dt"],
"action": t["action"],
"price": t["price"],
"weight": t.get("weight", 1.0),
"leg_id": t.get("leg_id", 0),
"memo": t.get("memo", ""),
}
flat: dict[str, Any] = {}
for iv in GENERAL_ANALYSIS_INTERVALS:
ef = enriched.get(iv)
if ef is None:
continue
snap = general_analysis_snapshot_at_bar(ef, ts, iv)
flat.update(_prefixed_snap(snap, iv))
row.update(flat)
row.update(general_analysis_mtf_scores(flat))
rows.append(row)
if (i + 1) % 50 == 0:
print(f" 타점 스냅샷 {i + 1}/{len(trades)}")
return pd.DataFrame(rows)
def export_trade_snapshots(
frames: dict[int, pd.DataFrame],
trades_path: Path | str = DEFAULT_TRADES_FILE,
output_csv: Path | str = DEFAULT_OUTPUT_CSV,
) -> Path:
"""
CSV로 타점 MTF 스냅샷 저장.
Returns:
저장 경로.
"""
data = load_ground_truth(Path(trades_path))
if not data:
raise FileNotFoundError(f"정답 파일 없음: {trades_path}")
trades = data.get("trades") or []
print(f"타점 {len(trades)}× {len(GENERAL_ANALYSIS_INTERVALS)} TF general_analysis")
df = build_trade_mtf_snapshots(frames, trades)
out = Path(output_csv)
out.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(out, index=False, encoding="utf-8-sig")
print(f"저장: {out} ({len(df)}× {len(df.columns)}열)")
return out

View File

@@ -0,0 +1,92 @@
"""
general_analysis Volume Profile (POC, VAH, VAL).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from config import GA_VP_BINS, GA_VP_VALUE_AREA_PCT
from deepcoin.analysis.general_analysis_core import ga_col
def general_analysis_volume_profile(
win: pd.DataFrame,
bins: int | None = None,
value_area_pct: float | None = None,
) -> dict[str, float | int]:
"""
lookback 구간 가격-거래량 분포에서 POC·VAH·VAL 계산.
Args:
win: OHLCV.
bins: 가격 구간 수.
value_area_pct: value area 누적 비율 (기본 70%).
Returns:
ga_vp_* 키 dict (접두사 없음).
"""
res: dict[str, float | int] = {
"vp_poc": 0.0,
"vp_vah": 0.0,
"vp_val": 0.0,
"vp_close_vs_poc_pct": 0.0,
"vp_in_value_area": 0,
}
if bins is None:
bins = GA_VP_BINS
if value_area_pct is None:
value_area_pct = GA_VP_VALUE_AREA_PCT
if win is None or len(win) < 10 or "Volume" not in win.columns:
return res
h = win["High"].astype(float).values
l = win["Low"].astype(float).values
c = win["Close"].astype(float).values
v = win["Volume"].astype(float).values
tp = (h + l + c) / 3.0
lo, hi = float(l.min()), float(h.max())
if hi <= lo:
return res
edges = np.linspace(lo, hi, bins + 1)
hist = np.zeros(bins, dtype=float)
for i in range(len(tp)):
idx = int(np.clip(np.digitize(tp[i], edges) - 1, 0, bins - 1))
hist[idx] += v[i]
if hist.sum() <= 0:
return res
poc_idx = int(np.argmax(hist))
poc = float((edges[poc_idx] + edges[poc_idx + 1]) / 2)
res["vp_poc"] = poc
order = np.argsort(hist)[::-1]
cum = 0.0
selected: list[int] = []
total = hist.sum()
for idx in order:
selected.append(int(idx))
cum += hist[idx]
if cum >= total * value_area_pct:
break
sel_min, sel_max = min(selected), max(selected)
res["vp_val"] = float(edges[sel_min])
res["vp_vah"] = float(edges[sel_max + 1])
res["vp_close_vs_poc_pct"] = float((c[-1] / poc - 1) * 100) if poc else 0.0
res["vp_in_value_area"] = int(res["vp_val"] <= c[-1] <= res["vp_vah"])
return res
def general_analysis_volume_columns() -> list[str]:
return ["vp_poc", "vp_vah", "vp_val", "vp_close_vs_poc_pct", "vp_in_value_area"]
def general_analysis_volume_snapshot(win: pd.DataFrame) -> dict[str, object]:
"""Volume profile → ga_vp_*."""
return {ga_col(k): v for k, v in general_analysis_volume_profile(win).items()}

View File

@@ -0,0 +1,205 @@
"""
general_analysis 파동·시장 구조 (다우, 엘리어트 라이트, 피보나치, Wyckoff 태그).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from deepcoin.analysis.general_analysis_core import find_pivots, ga_col
def _fib_levels(low: float, high: float) -> dict[str, float]:
diff = high - low
return {
"fib_0": low,
"fib_382": low + diff * 0.382,
"fib_500": low + diff * 0.5,
"fib_618": low + diff * 0.618,
"fib_100": high,
"fib_1618": low + diff * 1.618,
}
def general_analysis_wave_snapshot(win: pd.DataFrame) -> dict[str, object]:
"""
lookback 윈도우 마지막 시점 파동·구조 스냅샷.
Args:
win: OHLCV.
Returns:
ga_wave_* / ga_struct_* / ga_fib_* dict.
"""
res: dict[str, object] = {
"struct_trend": "range",
"struct_hh": 0,
"struct_hl": 0,
"struct_lh": 0,
"struct_ll": 0,
"struct_bos_bull": 0,
"struct_bos_bear": 0,
"struct_choch": 0,
"elliott_wave_count": 0,
"elliott_phase": "unknown",
"wyckoff_phase": "unknown",
"fib_near_level": "none",
"ichi_trend": "neutral",
"pitchfork_bias": "neutral",
"pitchfork_dist_pct": 0.0,
"wyckoff_spring": 0,
"wyckoff_utad": 0,
}
if win is None or len(win) < 15:
return {ga_col(k): v for k, v in res.items()}
h = win["High"].astype(float).values
l = win["Low"].astype(float).values
c = win["Close"].astype(float).values
peaks, troughs = find_pivots(h, l, order=2)
# Dow HH/HL/LH/LL
if len(peaks) >= 2 and len(troughs) >= 2:
hh = int(h[peaks[-1]] > h[peaks[-2]])
hl = int(l[troughs[-1]] > l[troughs[-2]])
lh = int(h[peaks[-1]] < h[peaks[-2]])
ll = int(l[troughs[-1]] < l[troughs[-2]])
res["struct_hh"] = hh
res["struct_hl"] = hl
res["struct_lh"] = lh
res["struct_ll"] = ll
if hh and hl:
res["struct_trend"] = "up"
elif lh and ll:
res["struct_trend"] = "down"
if hh and c[-1] > h[peaks[-2]]:
res["struct_bos_bull"] = 1
if ll and c[-1] < l[troughs[-2]]:
res["struct_bos_bear"] = 1
if (hh and ll) or (lh and hl):
res["struct_choch"] = 1
# Elliott lite: pivot count in window
swings = len(peaks) + len(troughs)
res["elliott_wave_count"] = swings
if swings >= 5:
res["elliott_phase"] = "impulse_late"
elif swings >= 3:
res["elliott_phase"] = "corrective"
# Wyckoff lite
vol = win["Volume"].astype(float).values if "Volume" in win.columns else np.ones(len(c))
vol_ma = vol[-20:].mean() if len(vol) >= 20 else vol.mean()
price_range = (h[-20:].max() - l[-20:].min()) / max(c[-1], 1e-9) * 100
if price_range < 6 and vol[-1] < vol_ma * 1.2:
res["wyckoff_phase"] = "accumulation"
elif price_range < 6 and vol[-1] > vol_ma * 1.5 and c[-1] > c[-5]:
res["wyckoff_phase"] = "distribution"
if price_range < 8 and l[-1] < l[-5] and c[-1] > c[-2] and vol[-1] > vol_ma * 1.3:
res["wyckoff_spring"] = 1
if price_range < 8 and h[-1] > h[-5] and c[-1] < c[-2] and vol[-1] > vol_ma * 1.3:
res["wyckoff_utad"] = 1
# Andrews Pitchfork (3피벗 중앙선 대비 종가 위치)
pivots = sorted([(i, h[i]) for i in peaks] + [(i, l[i]) for i in troughs])
if len(pivots) >= 3:
p0, p1, p2 = pivots[-3], pivots[-2], pivots[-1]
y0 = p0[1]
y_mid = (p1[1] + p2[1]) / 2
x0, x2 = p0[0], p2[0]
if x2 != x0:
slope = (y_mid - y0) / (x2 - x0)
y_line = y0 + slope * (len(c) - 1 - x0)
dist_pct = (c[-1] - y_line) / max(c[-1], 1e-9) * 100
res["pitchfork_dist_pct"] = round(float(dist_pct), 3)
if dist_pct > 0.5:
res["pitchfork_bias"] = "above"
elif dist_pct < -0.5:
res["pitchfork_bias"] = "below"
# Fibonacci
hi, lo = float(h.max()), float(l.min())
levels = _fib_levels(lo, hi)
price = float(c[-1])
for name, lvl in levels.items():
if abs(price - lvl) / max(price, 1e-9) * 100 < 1.5:
res["fib_near_level"] = name.replace("fib_", "")
break
if "ichi_cloud_top" in win.columns:
row = win.iloc[-1]
ct = float(row.get("ichi_cloud_top", np.nan))
cb = float(row.get("ichi_cloud_bottom", np.nan))
if not np.isnan(ct) and price > ct:
res["ichi_trend"] = "above_cloud"
elif not np.isnan(cb) and price < cb:
res["ichi_trend"] = "below_cloud"
else:
res["ichi_trend"] = "in_cloud"
return {ga_col(k): v for k, v in res.items()}
def general_analysis_wave_columns() -> list[str]:
return [
"struct_trend",
"struct_hh",
"struct_hl",
"struct_lh",
"struct_ll",
"struct_bos_bull",
"struct_bos_bear",
"struct_choch",
"elliott_wave_count",
"elliott_phase",
"wyckoff_phase",
"fib_near_level",
"ichi_trend",
"pitchfork_bias",
"pitchfork_dist_pct",
"wyckoff_spring",
"wyckoff_utad",
]
def general_analysis_apply_wave_to_bars(
df: pd.DataFrame,
interval: int,
tail_rows: int | None = None,
) -> pd.DataFrame:
"""파동·구조 스냅샷을 최근 봉에 롤링 적용."""
from deepcoin.analysis.general_analysis_config import CONTEXT_TAIL_ROWS, LOOKBACK_BARS
out = df.copy()
lb = LOOKBACK_BARS.get(interval, 80)
for k in general_analysis_wave_columns():
col = ga_col(k)
if k == "struct_trend":
out[col] = "range"
elif k in ("elliott_phase", "wyckoff_phase"):
out[col] = "unknown"
elif k == "fib_near_level":
out[col] = "none"
elif k in ("ichi_trend", "pitchfork_bias"):
out[col] = "neutral"
elif k == "pitchfork_dist_pct":
out[col] = 0.0
else:
out[col] = 0
n = len(out)
if n < lb + 1:
return out
if tail_rows is None:
tail_rows = CONTEXT_TAIL_ROWS.get(interval, 5000)
start = max(lb, n - tail_rows)
for i in range(start, n):
snap = general_analysis_wave_snapshot(out.iloc[i - lb : i])
idx = out.index[i]
for k, v in snap.items():
out.at[idx, k] = v
return out

5
deepcoin/api/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""외부 API 연동 (빗썸 등)."""
from deepcoin.api.bithumb import HTS
__all__ = ["HTS"]

View File

@@ -1,4 +1,3 @@
import os
import pandas as pd import pandas as pd
import jwt import jwt
import uuid import uuid
@@ -15,13 +14,15 @@ class HTS:
bithumb = None bithumb = None
accessKey = "" accessKey = ""
secretKey = "" secretKey = ""
apiUrl = "https://api.bithumb.com" apiUrl = ""
def __init__(self): def __init__(self):
from config import BITHUMB_ACCESS_KEY, BITHUMB_API_URL, BITHUMB_SECRET_KEY
self.bithumb = None self.bithumb = None
self.accessKey = os.getenv("BITHUMB_ACCESS_KEY", "") self.accessKey = BITHUMB_ACCESS_KEY
self.secretKey = os.getenv("BITHUMB_SECRET_KEY", "") self.secretKey = BITHUMB_SECRET_KEY
self.apiUrl = "https://api.bithumb.com" self.apiUrl = BITHUMB_API_URL.rstrip("/")
def append(self, stock, df=None, data_1=None): def append(self, stock, df=None, data_1=None):
if df is not None: if df is not None:
@@ -141,7 +142,7 @@ class HTS:
return df return df
def getTickerList(self): def getTickerList(self):
url = "https://api.bithumb.com/v1/market/all?isDetails=false" url = f"{self.apiUrl}/v1/market/all?isDetails=false"
headers = {"accept": "application/json"} headers = {"accept": "application/json"}
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
@@ -149,7 +150,7 @@ class HTS:
return tickets return tickets
def getVirtual_asset_warning(self): def getVirtual_asset_warning(self):
url = "https://api.bithumb.com/v1/market/virtual_asset_warning" url = f"{self.apiUrl}/v1/market/virtual_asset_warning"
headers = {"accept": "application/json"} headers = {"accept": "application/json"}
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
warning_list = response.json() warning_list = response.json()

View File

View File

@@ -8,26 +8,21 @@ from __future__ import annotations
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from config import ALL_INTERVALS, ENTRY_INTERVAL from config import (
from indicators import add_bollinger, add_ichimoku ALL_INTERVALS,
from strategy import prepare_entry_df BB_MIN_WIDTH_PCT,
DISPARITY_PERIODS,
INTERVAL_LABELS: dict[int, str] = { ENTRY_INTERVAL,
1: "m1", INTERVAL_PREFIX,
3: "m3", STOCH_OVERBOUGHT,
5: "m5", STOCH_OVERSOLD,
10: "m10", )
15: "m15", from deepcoin.common.indicators import apply_bar_indicators, disparity_column
30: "m30",
60: "m60",
240: "m240",
1440: "d1",
}
def interval_prefix(interval: int) -> str: def interval_prefix(interval: int) -> str:
"""컬럼 접두사 (예: m3, d1).""" """컬럼 접두사 (예: m3, d1)."""
return INTERVAL_LABELS.get(interval, f"m{interval}") return INTERVAL_PREFIX.get(interval, f"m{interval}")
def interval_display(interval: int) -> str: def interval_display(interval: int) -> str:
@@ -73,6 +68,29 @@ BB_EVENT_FEATURES: tuple[str, ...] = (
"squeeze", "squeeze",
) )
MACD_STOCH_FEATURES: tuple[str, ...] = (
"macd_hist_positive",
"macd_hist_negative",
"macd_cross_up",
"macd_cross_down",
"stoch_oversold",
"stoch_overbought",
"stoch_cross_up",
"stoch_cross_down",
)
def _disparity_feature_names() -> tuple[str, ...]:
"""기간별 이격도 과매수·과매도 불리언 컬럼명."""
names: list[str] = []
for p in DISPARITY_PERIODS:
names.append(f"disparity_{p}_oversold")
names.append(f"disparity_{p}_overbought")
return tuple(names)
DISPARITY_FEATURES: tuple[str, ...] = _disparity_feature_names()
CANDLE_SHAPE_FEATURES: tuple[str, ...] = ( CANDLE_SHAPE_FEATURES: tuple[str, ...] = (
"body_strong", "body_strong",
"body_weak", "body_weak",
@@ -86,13 +104,15 @@ FEATURE_BOOL_COLS: tuple[str, ...] = (
BB_EVENT_FEATURES BB_EVENT_FEATURES
+ BB_ZONE_FEATURES + BB_ZONE_FEATURES
+ ICHI_FEATURES + ICHI_FEATURES
+ MACD_STOCH_FEATURES
+ DISPARITY_FEATURES
+ CANDLE_SHAPE_FEATURES + CANDLE_SHAPE_FEATURES
) )
def compute_bar_features(df: pd.DataFrame) -> pd.DataFrame: def compute_bar_features(df: pd.DataFrame) -> pd.DataFrame:
"""단일 봉 DataFrame에 BB·일목·캔들 위치 특징을 추가합니다.""" """단일 봉 DataFrame에 BB·일목·MACD·스토캐스틱·캔들 위치 특징을 추가합니다."""
out = add_bollinger(add_ichimoku(prepare_entry_df(df.copy()))) out = apply_bar_indicators(df.copy())
if len(out) < 2: if len(out) < 2:
return out return out
@@ -129,7 +149,7 @@ def compute_bar_features(df: pd.DataFrame) -> pd.DataFrame:
out["inside_band"] = ((c >= lower) & (c <= upper)).astype(int) out["inside_band"] = ((c >= lower) & (c <= upper)).astype(int)
out["bb_pos_low"] = (pos < 0.2).astype(int) out["bb_pos_low"] = (pos < 0.2).astype(int)
out["bb_pos_high"] = (pos > 0.8).astype(int) out["bb_pos_high"] = (pos > 0.8).astype(int)
out["squeeze"] = (out["BB_Width"] < 0.8).astype(int) out["squeeze"] = (out["BB_Width"] < BB_MIN_WIDTH_PCT).astype(int)
ct = out["ichi_cloud_top"].astype(float) ct = out["ichi_cloud_top"].astype(float)
cb = out["ichi_cloud_bottom"].astype(float) cb = out["ichi_cloud_bottom"].astype(float)
@@ -157,6 +177,38 @@ def compute_bar_features(df: pd.DataFrame) -> pd.DataFrame:
out["bullish"] = (c > o).astype(int) out["bullish"] = (c > o).astype(int)
out["bearish"] = (c < o).astype(int) out["bearish"] = (c < o).astype(int)
if "macd_hist" in out.columns:
mh = out["macd_hist"].astype(float)
prev_mh = mh.shift(1)
ml = out["macd_line"].astype(float)
ms = out["macd_signal"].astype(float)
prev_ml = ml.shift(1)
prev_ms = ms.shift(1)
out["macd_hist_positive"] = (mh > 0).astype(int)
out["macd_hist_negative"] = (mh < 0).astype(int)
out["macd_cross_up"] = ((prev_ml <= prev_ms) & (ml > ms)).astype(int)
out["macd_cross_down"] = ((prev_ml >= prev_ms) & (ml < ms)).astype(int)
if "stoch_k" in out.columns:
sk = out["stoch_k"].astype(float)
sd = out["stoch_d"].astype(float)
prev_sk = sk.shift(1)
prev_sd = sd.shift(1)
out["stoch_oversold"] = (sk <= STOCH_OVERSOLD).astype(int)
out["stoch_overbought"] = (sk >= STOCH_OVERBOUGHT).astype(int)
out["stoch_cross_up"] = ((prev_sk <= prev_sd) & (sk > sd)).astype(int)
out["stoch_cross_down"] = ((prev_sk >= prev_sd) & (sk < sd)).astype(int)
from config import DISPARITY_OVERBOUGHT, DISPARITY_OVERSOLD
for p in DISPARITY_PERIODS:
col = disparity_column(p)
if col not in out.columns:
continue
d = out[col].astype(float)
out[f"disparity_{p}_oversold"] = (d <= DISPARITY_OVERSOLD).astype(int)
out[f"disparity_{p}_overbought"] = (d >= DISPARITY_OVERBOUGHT).astype(int)
return out return out
@@ -178,7 +230,7 @@ def describe_latest_position(df: pd.DataFrame, interval: int) -> dict:
elif int(row.get("ichi_below_cloud", 0)): elif int(row.get("ichi_below_cloud", 0)):
ichi_pos = "below_cloud" ichi_pos = "below_cloud"
return { snap: dict = {
"interval": interval, "interval": interval,
"label": interval_display(interval), "label": interval_display(interval),
"close": float(row["Close"]), "close": float(row["Close"]),
@@ -189,6 +241,31 @@ def describe_latest_position(df: pd.DataFrame, interval: int) -> dict:
"ichi_tk": "bull" if int(row.get("ichi_tk_bull", 0)) else "bear", "ichi_tk": "bull" if int(row.get("ichi_tk_bull", 0)) else "bear",
"ichi_cloud": "bull" if int(row.get("ichi_cloud_bull", 0)) else "bear", "ichi_cloud": "bull" if int(row.get("ichi_cloud_bull", 0)) else "bear",
} }
if "macd_hist" in row.index and pd.notna(row["macd_hist"]):
snap["macd_hist"] = round(float(row["macd_hist"]), 4)
snap["macd_state"] = "bull" if float(row["macd_hist"]) > 0 else "bear"
if "stoch_k" in row.index and pd.notna(row["stoch_k"]):
sk = float(row["stoch_k"])
snap["stoch_k"] = round(sk, 1)
snap["stoch_d"] = round(float(row["stoch_d"]), 1)
if sk <= STOCH_OVERSOLD:
snap["stoch_zone"] = "oversold"
elif sk >= STOCH_OVERBOUGHT:
snap["stoch_zone"] = "overbought"
else:
snap["stoch_zone"] = "mid"
disp_vals: dict[int, float] = {}
for p in DISPARITY_PERIODS:
col = disparity_column(p)
if col in row.index and pd.notna(row[col]):
disp_vals[p] = round(float(row[col]), 2)
if disp_vals:
snap["disparity"] = disp_vals
primary_p = 20 if 20 in DISPARITY_PERIODS else DISPARITY_PERIODS[0]
snap["disparity_primary"] = disp_vals.get(
primary_p, next(iter(disp_vals.values()))
)
return snap
def _bb_event_label(row: pd.Series) -> str: def _bb_event_label(row: pd.Series) -> str:
@@ -213,11 +290,20 @@ def _merge_interval_features(
) -> pd.DataFrame: ) -> pd.DataFrame:
"""master_index 길이와 동일한 간격 특징만 반환.""" """master_index 길이와 동일한 간격 특징만 반환."""
pick = [c for c in FEATURE_BOOL_COLS if c in feat.columns] pick = [c for c in FEATURE_BOOL_COLS if c in feat.columns]
extra = [ numeric_cols = (
c "bb_pos",
for c in ("bb_pos", "body_ratio", "lower_wick_ratio", "ret_pct", "bb_width_pct") "body_ratio",
if c in feat.columns "lower_wick_ratio",
] "ret_pct",
"bb_width_pct",
"macd_line",
"macd_signal",
"macd_hist",
"stoch_k",
"stoch_d",
"RSI",
) + tuple(disparity_column(p) for p in DISPARITY_PERIODS)
extra = [c for c in numeric_cols if c in feat.columns]
if "bb_width_pct" not in feat.columns and "BB_Width" in feat.columns: if "bb_width_pct" not in feat.columns and "BB_Width" in feat.columns:
feat = feat.copy() feat = feat.copy()
feat["bb_width_pct"] = feat["BB_Width"] feat["bb_width_pct"] = feat["BB_Width"]

View File

@@ -0,0 +1,315 @@
"""
볼린저 밴드·일목·MACD·스토캐스틱·RSI·이격도 계산 (모든 봉 간격 공용).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from config import (
BB_PERIOD,
BB_STD,
DISPARITY_OVERBOUGHT,
DISPARITY_OVERSOLD,
DISPARITY_PERIODS,
MACD_FAST,
MACD_SIGNAL,
MACD_SLOW,
RSI_PERIOD,
STOCH_D_PERIOD,
STOCH_K_PERIOD,
STOCH_OVERBOUGHT,
STOCH_OVERSOLD,
STOCH_SMOOTH_K,
TREND_RANGE_MA_GAP_PCT,
)
Trend = str # "up" | "down" | "range"
def add_bollinger(
df: pd.DataFrame,
period: int = BB_PERIOD,
std_mult: float = BB_STD,
) -> pd.DataFrame:
"""
볼린저 밴드 컬럼을 추가합니다.
Args:
df: OHLCV DataFrame.
period: 중심선 기간.
std_mult: 표준편차 배수.
Returns:
MA, Upper, Lower, STD, bb_pos, BB_Width 가 추가된 DataFrame.
"""
out = df.copy()
if "MA" not in out.columns:
out["MA"] = out["Close"].rolling(period).mean()
if "Upper" not in out.columns or "Lower" not in out.columns:
std = out["Close"].rolling(period).std()
out["STD"] = std
out["Upper"] = out["MA"] + std_mult * std
out["Lower"] = out["MA"] - std_mult * std
ma = out["MA"].replace(0, np.nan)
band = (out["Upper"] - out["Lower"]).replace(0, np.nan)
out["bb_pos"] = ((out["Close"] - out["Lower"]) / band).clip(0, 1)
out["BB_Width"] = band / ma * 100
return out
def add_macd(
df: pd.DataFrame,
fast: int = MACD_FAST,
slow: int = MACD_SLOW,
signal_period: int = MACD_SIGNAL,
) -> pd.DataFrame:
"""
MACD(12,26,9) 라인·시그널·히스토그램을 추가합니다.
Args:
df: OHLCV (Close 필요).
fast: 단기 EMA 기간.
slow: 장기 EMA 기간.
signal_period: 시그널 EMA 기간.
Returns:
macd_line, macd_signal, macd_hist 컬럼이 추가된 DataFrame.
"""
out = df.copy()
close = out["Close"].astype(float)
ema_fast = close.ewm(span=fast, adjust=False).mean()
ema_slow = close.ewm(span=slow, adjust=False).mean()
out["macd_line"] = ema_fast - ema_slow
out["macd_signal"] = out["macd_line"].ewm(span=signal_period, adjust=False).mean()
out["macd_hist"] = out["macd_line"] - out["macd_signal"]
return out
def disparity_column(period: int) -> str:
"""이격도 컬럼명 (예: disparity_20)."""
return f"disparity_{period}"
def add_disparity(
df: pd.DataFrame,
periods: tuple[int, ...] | None = None,
) -> pd.DataFrame:
"""
이격도 = (종가 / SMA(n)) × 100. 100이면 이평선과 동일 위치.
Args:
df: OHLCV (Close 필요).
periods: SMA 기간 목록. None이면 config.DISPARITY_PERIODS.
Returns:
disparity_{n} 컬럼이 추가된 DataFrame.
"""
out = df.copy()
close = out["Close"].astype(float)
for p in periods or DISPARITY_PERIODS:
ma = close.rolling(p).mean()
out[disparity_column(p)] = (close / ma.replace(0, np.nan)) * 100.0
return out
def disparity_zone(value: float | None) -> str:
"""이격도 구간 라벨 (oversold / mid / overbought)."""
if value is None:
return "mid"
if value <= DISPARITY_OVERSOLD:
return "oversold"
if value >= DISPARITY_OVERBOUGHT:
return "overbought"
return "mid"
def add_stochastic(
df: pd.DataFrame,
k_period: int = STOCH_K_PERIOD,
d_period: int = STOCH_D_PERIOD,
smooth_k: int = STOCH_SMOOTH_K,
) -> pd.DataFrame:
"""
스토캐스틱 %%D를 추가합니다 (Slow Stochastic).
Args:
df: OHLCV (High, Low, Close 필요).
k_period: %K lookback.
d_period: %D SMA 기간.
smooth_k: %K SMA 평활 기간.
Returns:
stoch_k, stoch_d 컬럼이 추가된 DataFrame.
"""
out = df.copy()
h = out["High"].astype(float)
l = out["Low"].astype(float)
c = out["Close"].astype(float)
lowest = l.rolling(k_period).min()
highest = h.rolling(k_period).max()
denom = (highest - lowest).replace(0, np.nan)
raw_k = ((c - lowest) / denom) * 100.0
out["stoch_k"] = raw_k.rolling(smooth_k).mean()
out["stoch_d"] = out["stoch_k"].rolling(d_period).mean()
return out
def add_ichimoku(
df: pd.DataFrame,
tenkan: int = 9,
kijun: int = 26,
senkou_b_period: int = 52,
) -> pd.DataFrame:
"""
일목균형표 라인·구름 위치 컬럼 추가 (해당 봉 시점, 미래 데이터 미사용).
Returns:
ichi_tenkan, ichi_kijun, ichi_span_a, ichi_span_b,
ichi_cloud_top, ichi_cloud_bottom
"""
out = df.copy()
h = out["High"].astype(float)
l = out["Low"].astype(float)
c = out["Close"].astype(float)
out["ichi_tenkan"] = (h.rolling(tenkan).max() + l.rolling(tenkan).min()) / 2
out["ichi_kijun"] = (h.rolling(kijun).max() + l.rolling(kijun).min()) / 2
out["ichi_span_a"] = (out["ichi_tenkan"] + out["ichi_kijun"]) / 2
out["ichi_span_b"] = (h.rolling(senkou_b_period).max() + l.rolling(senkou_b_period).min()) / 2
out["ichi_cloud_top"] = np.maximum(out["ichi_span_a"], out["ichi_span_b"])
out["ichi_cloud_bottom"] = np.minimum(out["ichi_span_a"], out["ichi_span_b"])
return out
def prepare_entry_df(data: pd.DataFrame) -> pd.DataFrame:
"""
RSI·거래량 MA·BB 폭 등 보조 컬럼을 추가합니다.
Args:
data: BB(MA/Upper/Lower)가 계산된 OHLCV.
Returns:
RSI 등 컬럼이 추가된 DataFrame.
"""
df = data.copy()
delta = df["Close"].diff()
gain = delta.where(delta > 0, 0.0).rolling(RSI_PERIOD).mean()
loss = (-delta.where(delta < 0, 0.0)).rolling(RSI_PERIOD).mean()
rs = gain / loss.replace(0, np.nan)
df["RSI"] = 100 - (100 / (1 + rs))
df["VolMA5"] = df["Volume"].rolling(5).mean()
if "MA" in df.columns and "Upper" in df.columns and "Lower" in df.columns:
ma = df["MA"].replace(0, np.nan)
df["BB_Width"] = (df["Upper"] - df["Lower"]) / ma * 100
return df
def apply_bar_indicators(df: pd.DataFrame) -> pd.DataFrame:
"""
봉 분석·차트용 표준 지표 일괄 적용 (BB, 일목, RSI, MACD, 스토캐스틱, 이격도).
Args:
df: OHLCV DataFrame (datetime index).
Returns:
모든 지표 컬럼이 붙은 DataFrame.
"""
out = add_bollinger(df)
out = add_ichimoku(out)
out = prepare_entry_df(out)
out = add_disparity(out)
out = add_macd(out)
out = add_stochastic(out)
return out
def latest_indicator_snapshot(df: pd.DataFrame) -> dict[str, float | str | None]:
"""
최신 봉의 BB·RSI·MACD·스토캐스틱 요약 (모니터·로그용).
Args:
df: apply_bar_indicators 적용된 DataFrame.
Returns:
지표명→값 dict.
"""
if df.empty:
return {}
row = df.iloc[-1]
def _f(col: str) -> float | None:
if col not in row.index or pd.isna(row[col]):
return None
return round(float(row[col]), 4)
macd_hist = _f("macd_hist")
stoch_k = _f("stoch_k")
stoch_d = _f("stoch_d")
stoch_zone = "mid"
if stoch_k is not None:
if stoch_k <= STOCH_OVERSOLD:
stoch_zone = "oversold"
elif stoch_k >= STOCH_OVERBOUGHT:
stoch_zone = "overbought"
macd_state = "neutral"
if macd_hist is not None:
macd_state = "bull" if macd_hist > 0 else "bear"
disp: dict[str, float | None] = {}
for p in DISPARITY_PERIODS:
col = disparity_column(p)
disp[col] = _f(col)
primary = disparity_column(DISPARITY_PERIODS[0]) if DISPARITY_PERIODS else None
disp_primary = disp.get(primary) if primary else None
return {
"bb_pos": _f("bb_pos"),
"rsi": _f("RSI"),
"disparity": disp,
"disparity_primary": disp_primary,
"disparity_zone": disparity_zone(disp_primary),
"macd_line": _f("macd_line"),
"macd_signal": _f("macd_signal"),
"macd_hist": macd_hist,
"macd_state": macd_state,
"stoch_k": stoch_k,
"stoch_d": stoch_d,
"stoch_zone": stoch_zone,
}
def get_trend(df_1d: pd.DataFrame, df_1h: pd.DataFrame) -> Trend:
"""
일봉·1시간봉 기준 추세(up/down/range)를 반환합니다.
Args:
df_1d: 일봉 OHLCV+지표.
df_1h: 1시간봉 OHLCV+지표.
Returns:
추세 문자열.
"""
if len(df_1d) < 20 or len(df_1h) < 40:
return "range"
d_close = float(df_1d["Close"].iloc[-1])
d_ma20 = float(df_1d["MA20"].iloc[-1])
h_close = float(df_1h["Close"].iloc[-1])
h_ma20 = float(df_1h["MA20"].iloc[-1])
h_ma40 = float(df_1h["MA40"].iloc[-1])
if h_ma40 == 0:
return "range"
ma_gap_pct = abs(h_ma20 - h_ma40) / h_ma40 * 100
if ma_gap_pct < TREND_RANGE_MA_GAP_PCT:
return "range"
if d_close > d_ma20 and h_ma20 > h_ma40 and h_close > h_ma20:
return "up"
if d_close < d_ma20 and h_ma20 < h_ma40 and h_close < h_ma20:
return "down"
return "range"

View File

View File

@@ -14,28 +14,30 @@ import pandas as pd
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from config import ( from config import (
BITHUMB_MINUTE_INTERVALS,
COIN_NAME, COIN_NAME,
DAILY_INTERVAL_MIN,
DB_PATH, DB_PATH,
DOWNLOAD_BACKFILL_EXTRA_BARS,
DOWNLOAD_DAILY_EXTRA_DAYS,
DOWNLOAD_INTERVALS, DOWNLOAD_INTERVALS,
DOWNLOAD_MIN_INCREMENTAL_BARS,
DOWNLOAD_MONTHS, DOWNLOAD_MONTHS,
DOWNLOAD_MONTHS_1M, DOWNLOAD_MONTHS_1M,
INCREMENTAL_OVERLAP_BARS,
KR_COINS, KR_COINS,
SYMBOL, SYMBOL,
) )
from monitor import Monitor from deepcoin.ops.monitor import Monitor
BITHUMB_MINUTE_INTERVALS = {1, 3, 5, 10, 15, 30, 60, 240}
# 증분 시 마지막 봉 재확인용 겹침 봉 수
INCREMENTAL_OVERLAP_BARS = 3
def bong_count_for_months(interval_minutes: int, months: int) -> int: def bong_count_for_months(interval_minutes: int, months: int) -> int:
"""N개월치 봉 개수(여유분 포함).""" """N개월치 봉 개수(여유분 포함)."""
days = months * 30 days = months * 30
if interval_minutes >= 1440: if interval_minutes >= DAILY_INTERVAL_MIN:
return days + 20 return days + DOWNLOAD_DAILY_EXTRA_DAYS
bars_per_day = (24 * 60) // interval_minutes bars_per_day = (24 * 60) // interval_minutes
return days * bars_per_day + 200 return days * bars_per_day + DOWNLOAD_BACKFILL_EXTRA_BARS
def bong_count_since( def bong_count_since(
@@ -47,7 +49,7 @@ def bong_count_since(
last_ts = last_ts.tz_localize(None) last_ts = last_ts.tz_localize(None)
delta_min = max(0, (now - last_ts).total_seconds() / 60) delta_min = max(0, (now - last_ts).total_seconds() / 60)
bars = int(delta_min / interval_minutes) + overlap + 10 bars = int(delta_min / interval_minutes) + overlap + 10
return max(bars, 50) return max(bars, DOWNLOAD_MIN_INCREMENTAL_BARS)
def months_cutoff(months: int) -> pd.Timestamp: def months_cutoff(months: int) -> pd.Timestamp:
@@ -111,6 +113,25 @@ def ensure_table(cursor, table_name: str) -> None:
) )
def get_earliest_timestamp(
symbol: str, interval: int, db_path: str = DB_PATH
) -> pd.Timestamp | None:
"""테이블에 저장된 해당 심볼의 가장 오래된 봉 시각."""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
table_name = f"{symbol}_{interval}"
ensure_table(cursor, table_name)
cursor.execute(
f"SELECT MIN(ymdhms) FROM {table_name} WHERE CODE = ?",
(symbol,),
)
row = cursor.fetchone()
conn.close()
if row and row[0]:
return pd.Timestamp(row[0])
return None
def get_last_timestamp( def get_last_timestamp(
symbol: str, interval: int, db_path: str = DB_PATH symbol: str, interval: int, db_path: str = DB_PATH
) -> pd.Timestamp | None: ) -> pd.Timestamp | None:
@@ -244,18 +265,68 @@ def append_data(
return len(records), skipped return len(records), skipped
def backfill_before_earliest(
monitor: Monitor,
symbol: str,
interval: int,
months: int,
) -> int:
"""
DB 최초 봉보다 오래된 구간을 API로 채웁니다 (1 적재 필요).
Returns:
추가된 .
"""
months = months_for_interval(interval, months)
cutoff = months_cutoff(months)
earliest = get_earliest_timestamp(symbol, interval)
if earliest is None or earliest <= cutoff:
return 0
label = interval_label(interval)
# now부터 역순 수집이므로 cutoff까지 닿으려면 N개월 전체 봉 수가 필요
target = bong_count_for_months(interval, months)
print(
f" [백필] {label}{cutoff.date()} ~ {earliest} "
f"(API 역수집 약 {target}봉)"
)
data = monitor.get_coin_more_data(
symbol, interval, bong_count=target, verbose=True
)
if data is None or data.empty:
print(" -> 백필 API 데이터 없음")
return 0
if not isinstance(data.index, pd.DatetimeIndex):
data.index = pd.to_datetime(data.index)
hist = data[(data.index >= cutoff) & (data.index < earliest)].copy()
if hist.empty:
print(" -> 백필 대상 구간 없음")
return 0
inserted, skipped = append_data(symbol, interval, hist, last_ts=None)
print(f" -> 백필 추가 {inserted}행 (스킵 {skipped})")
return inserted
def download_symbol( def download_symbol(
monitor: Monitor, monitor: Monitor,
symbol: str, symbol: str,
interval: int, interval: int,
months: int, months: int,
) -> None: ) -> None:
"""한 간격의 봉을 API로 받아 증분 저장합니다.""" """한 간격의 봉을 API로 받아 증분·백필 저장합니다."""
months = months_for_interval(interval, months) months = months_for_interval(interval, months)
label = interval_label(interval) label = interval_label(interval)
last_ts = get_last_timestamp(symbol, interval)
existing = get_row_count(symbol, interval) existing = get_row_count(symbol, interval)
if existing > 0:
backfill_before_earliest(monitor, symbol, interval, months)
last_ts = get_last_timestamp(symbol, interval)
if last_ts is None: if last_ts is None:
target = bong_count_for_months(interval, months) target = bong_count_for_months(interval, months)
mode = "초기 적재" mode = "초기 적재"

47
deepcoin/data/mtf_bb.py Normal file
View File

@@ -0,0 +1,47 @@
"""
coins.db에서 전 간격 봉 데이터를 로드합니다.
"""
from __future__ import annotations
import pandas as pd
from config import DOWNLOAD_INTERVALS, SYMBOL
def interval_label(interval: int) -> str:
"""봉 간격 표시 라벨."""
if interval >= 1440:
return "일봉"
return f"{interval}"
def load_frames_from_db(
monitor,
symbol: str,
lookback_days: int | None = None,
) -> dict[int, pd.DataFrame]:
"""
coins.db에서 DOWNLOAD_INTERVALS 전부 로드·지표 계산.
Args:
monitor: Monitor 인스턴스.
symbol: 코인 심볼.
lookback_days: 지정 시 간격별로 해당 일수만큼 DB에서 더 많이 읽습니다.
Returns:
간격(분) → OHLCV+지표 DataFrame.
"""
frames: dict[int, pd.DataFrame] = {}
for iv in DOWNLOAD_INTERVALS:
db_max = None
if lookback_days is not None:
db_max = monitor.db_row_limit_for_interval(iv, lookback_days)
df = monitor.get_coin_some_data(symbol, iv, db_max_rows=db_max)
if df is None or df.empty:
print(f" [{interval_label(iv)}] DB/API 데이터 없음 — 스킵")
continue
df = monitor.calculate_technical_indicators(df)
frames[iv] = df
print(f" [{interval_label(iv)}] {len(df)}{df.index[0]} ~ {df.index[-1]}")
return frames

53
deepcoin/env_loader.py Normal file
View File

@@ -0,0 +1,53 @@
"""
DeepCoin .env 로드 (프로젝트 루트 기준).
config·HTS·스크립트 진입 전에 한 번 호출하면 cwd와 무관하게 동일한 설정을 사용합니다.
"""
from __future__ import annotations
from pathlib import Path
from deepcoin.paths import PROJECT_ROOT
_ENV_LOADED = False
ENV_FILE = PROJECT_ROOT / ".env"
def load_project_env(*, override: bool = False) -> bool:
"""
PROJECT_ROOT/.env 를 python-dotenv로 로드합니다.
Args:
override: True면 기존 OS 환경 변수를 .env 값으로 덮어씀.
Returns:
.env 파일이 존재해 로드 시도했으면 True, 없으면 False.
"""
global _ENV_LOADED
if _ENV_LOADED and not override:
return ENV_FILE.is_file()
try:
from dotenv import load_dotenv
except ImportError:
_ENV_LOADED = True
return False
if ENV_FILE.is_file():
load_dotenv(ENV_FILE, override=override)
_ENV_LOADED = True
return True
_ENV_LOADED = True
return False
def env_status() -> dict[str, str | bool]:
"""디버그용 .env 상태."""
return {
"project_root": str(PROJECT_ROOT),
"env_file": str(ENV_FILE),
"env_exists": ENV_FILE.is_file(),
"loaded": _ENV_LOADED,
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
# Phase 04 — Matching
Ground Truth 매수·매도 타점의 MTF 스냅샷(`docs/03_analysis/general_analysis_trades.csv`)과
실시간·최근 봉 상태를 비교해 **가장 근접한 기술적 프로파일** 및 **진입·청산 규칙**을 선택합니다.
예정 산출물:
- `docs/04_matching/rule_candidates.json`
- `docs/04_matching/similarity_report.html`
실행 (스텁):
```bash
python scripts/04_match_rules.py
```

View File

@@ -0,0 +1,3 @@
"""
04단계: Ground Truth에 근접한 기술적 상태·규칙 선택 (예정).
"""

View File

@@ -0,0 +1,31 @@
"""
04단계 스텁: GT 스냅샷과 현재 상태 유사도·규칙 후보 (구현 예정).
"""
from __future__ import annotations
from pathlib import Path
from deepcoin.paths import REPORTS_ANALYSIS, REPORTS_MATCHING, resolve_ground_truth_file
def run_match_stub() -> Path:
"""
입력 파일 존재 여부만 확인하고 04단계 안내를 출력합니다.
Returns:
matching 리포트 디렉터리.
"""
REPORTS_MATCHING.mkdir(parents=True, exist_ok=True)
gt = resolve_ground_truth_file()
csv = REPORTS_ANALYSIS / "general_analysis_trades.csv"
print("=== Phase 04 Matching (stub) ===")
print(f" ground truth: {gt} ({'OK' if gt.is_file() else 'MISSING'})")
print(f" analysis csv: {csv} ({'OK' if csv.is_file() else 'MISSING — run scripts/03_analyze_trades.py'})")
print(f" output dir: {REPORTS_MATCHING}")
print(" 구현 예정: 유사도·규칙 선택")
return REPORTS_MATCHING
if __name__ == "__main__":
run_match_stub()

0
deepcoin/ops/__init__.py Normal file
View File

View File

@@ -1,5 +1,5 @@
import pandas as pd import pandas as pd
from HTS2 import HTS from deepcoin.api.bithumb import HTS
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from datetime import datetime from datetime import datetime
import sqlite3 import sqlite3
@@ -17,15 +17,14 @@ import numpy as np
import os import os
from config import * from config import *
import strategy
class Monitor(HTS): class Monitor(HTS):
"""WLD 코인 모니터링 및 매매 실행.""" """WLD 코인 데이터·지표·시장 상태 출력."""
last_signal = None last_signal = None
cooldown_file = None cooldown_file = None
def __init__(self, cooldown_file='coins_buy_time.json') -> None: def __init__(self, cooldown_file: str | None = None) -> None:
HTS.__init__(self) HTS.__init__(self)
# 최근 매수 신호 저장용(파일은 [신규] 포맷으로 저장) # 최근 매수 신호 저장용(파일은 [신규] 포맷으로 저장)
self.last_signal: dict[str, str] = {} self.last_signal: dict[str, str] = {}
@@ -130,12 +129,12 @@ class Monitor(HTS):
payload = header + "\n" payload = header + "\n"
for i, message in enumerate(message_list): for i, message in enumerate(message_list):
payload += message payload += message
if i + 1 % 20 == 0: if i + 1 % MONITOR_TELEGRAM_BATCH_SIZE == 0:
pool = Pool(12) pool = Pool(MONITOR_POOL_WORKERS)
pool.map(self._send_coin_msg, [payload]) pool.map(self._send_coin_msg, [payload])
payload = '' payload = ''
if len(message_list) % 20 != 0: if len(message_list) % MONITOR_TELEGRAM_BATCH_SIZE != 0:
pool = Pool(12) pool = Pool(MONITOR_POOL_WORKERS)
pool.map(self._send_coin_msg, [payload]) pool.map(self._send_coin_msg, [payload])
# ------------- Indicators ------------- # ------------- Indicators -------------
@@ -143,8 +142,8 @@ class Monitor(HTS):
columns_to_normalize = ['Open', 'High', 'Low', 'Close', 'Volume'] columns_to_normalize = ['Open', 'High', 'Low', 'Close', 'Volume']
normalized_data = data.copy() normalized_data = data.copy()
for column in columns_to_normalize: for column in columns_to_normalize:
min_val = data[column].rolling(window=20).min() min_val = data[column].rolling(window=MONITOR_NORM_WINDOW).min()
max_val = data[column].rolling(window=20).max() max_val = data[column].rolling(window=MONITOR_NORM_WINDOW).max()
denominator = max_val - min_val denominator = max_val - min_val
normalized_data[f'{column}_Norm'] = np.where( normalized_data[f'{column}_Norm'] = np.where(
denominator != 0, denominator != 0,
@@ -167,154 +166,49 @@ class Monitor(HTS):
# 지표 다시 계산 # 지표 다시 계산
inv = self.normalize_data(inv) inv = self.normalize_data(inv)
inv['MA5'] = inv['Close'].rolling(window=5).mean() for w in MONITOR_MA_WINDOWS:
inv['MA20'] = inv['Close'].rolling(window=20).mean() inv[f"MA{w}"] = inv["Close"].rolling(window=w).mean()
inv['MA40'] = inv['Close'].rolling(window=40).mean() inv[f"Deviation{w}"] = (inv["Close"] / inv[f"MA{w}"]) * 100
inv['MA120'] = inv['Close'].rolling(window=120).mean() if len(MONITOR_MA_WINDOWS) >= 2:
inv['MA200'] = inv['Close'].rolling(window=200).mean() w_fast, w_slow = MONITOR_MA_WINDOWS[0], MONITOR_MA_WINDOWS[1]
inv['MA240'] = inv['Close'].rolling(window=240).mean() inv["golden_cross"] = (inv[f"MA{w_fast}"] > inv[f"MA{w_slow}"]) & (
inv['MA720'] = inv['Close'].rolling(window=720).mean() inv[f"MA{w_fast}"].shift(1) <= inv[f"MA{w_slow}"].shift(1)
inv['MA1440'] = inv['Close'].rolling(window=1440).mean() )
inv['Deviation5'] = (inv['Close'] / inv['MA5']) * 100 inv["MA"] = inv["Close"].rolling(window=BB_PERIOD).mean()
inv['Deviation20'] = (inv['Close'] / inv['MA20']) * 100 inv["STD"] = inv["Close"].rolling(window=BB_PERIOD).std()
inv['Deviation40'] = (inv['Close'] / inv['MA40']) * 100 inv["Upper"] = inv["MA"] + (BB_STD * inv["STD"])
inv['Deviation120'] = (inv['Close'] / inv['MA120']) * 100 inv["Lower"] = inv["MA"] - (BB_STD * inv["STD"])
inv['Deviation200'] = (inv['Close'] / inv['MA200']) * 100
inv['Deviation240'] = (inv['Close'] / inv['MA240']) * 100
inv['Deviation720'] = (inv['Close'] / inv['MA720']) * 100
inv['Deviation1440'] = (inv['Close'] / inv['MA1440']) * 100
inv['golden_cross'] = (inv['MA5'] > inv['MA20']) & (inv['MA5'].shift(1) <= inv['MA20'].shift(1))
inv['MA'] = inv['Close'].rolling(window=20).mean()
inv['STD'] = inv['Close'].rolling(window=20).std()
inv['Upper'] = inv['MA'] + (2 * inv['STD'])
inv['Lower'] = inv['MA'] - (2 * inv['STD'])
return inv return inv
def calculate_technical_indicators(self, data: pd.DataFrame) -> pd.DataFrame: def calculate_technical_indicators(self, data: pd.DataFrame) -> pd.DataFrame:
data = self.normalize_data(data) data = self.normalize_data(data)
data['MA5'] = data['Close'].rolling(window=5).mean() for w in MONITOR_MA_WINDOWS:
data['MA20'] = data['Close'].rolling(window=20).mean() data[f"MA{w}"] = data["Close"].rolling(window=w).mean()
data['MA40'] = data['Close'].rolling(window=40).mean() data[f"Deviation{w}"] = (data["Close"] / data[f"MA{w}"]) * 100
data['MA120'] = data['Close'].rolling(window=120).mean() if len(MONITOR_MA_WINDOWS) >= 2:
data['MA200'] = data['Close'].rolling(window=200).mean() w_fast, w_slow = MONITOR_MA_WINDOWS[0], MONITOR_MA_WINDOWS[1]
data['MA240'] = data['Close'].rolling(window=240).mean() data["golden_cross"] = (data[f"MA{w_fast}"] > data[f"MA{w_slow}"]) & (
data['MA720'] = data['Close'].rolling(window=720).mean() data[f"MA{w_fast}"].shift(1) <= data[f"MA{w_slow}"].shift(1)
data['MA1440'] = data['Close'].rolling(window=1440).mean() )
data['Deviation5'] = (data['Close'] / data['MA5']) * 100 data["MA"] = data["Close"].rolling(window=BB_PERIOD).mean()
data['Deviation20'] = (data['Close'] / data['MA20']) * 100 data["STD"] = data["Close"].rolling(window=BB_PERIOD).std()
data['Deviation40'] = (data['Close'] / data['MA40']) * 100 data["Upper"] = data["MA"] + (BB_STD * data["STD"])
data['Deviation120'] = (data['Close'] / data['MA120']) * 100 data["Lower"] = data["MA"] - (BB_STD * data["STD"])
data['Deviation200'] = (data['Close'] / data['MA200']) * 100
data['Deviation240'] = (data['Close'] / data['MA240']) * 100
data['Deviation720'] = (data['Close'] / data['MA720']) * 100
data['Deviation1440'] = (data['Close'] / data['MA1440']) * 100
data['golden_cross'] = (data['MA5'] > data['MA20']) & (data['MA5'].shift(1) <= data['MA20'].shift(1))
data['MA'] = data['Close'].rolling(window=20).mean()
data['STD'] = data['Close'].rolling(window=20).std()
data['Upper'] = data['MA'] + (2 * data['STD'])
data['Lower'] = data['MA'] - (2 * data['STD'])
from deepcoin.common.indicators import add_macd, add_stochastic
data = add_macd(data)
data = add_stochastic(data)
return data return data
# ------------- Strategy (strategy.py에 구현) ------------- def process_wld_market_status(self, symbol: str) -> None:
def annotate_signals(self, symbol: str, data: pd.DataFrame, simulation: bool | None = None) -> pd.DataFrame:
"""strategy.annotate_signals에 위임."""
return strategy.annotate_signals(
symbol, data, simulation=simulation, config=strategy.ACTIVE_CONFIG
)
def _is_in_cooldown(self, symbol: str, side: str) -> bool:
"""매수/매도 쿨다운 여부."""
if self.cooldown_file is None:
return False
last_dt = self.buy_cooldown.get(symbol, {}).get(side, {}).get("datetime")
if not last_dt:
return False
limit = BUY_COOLDOWN_SEC if side == "buy" else SELL_COOLDOWN_SEC
elapsed = (datetime.now() - last_dt).total_seconds()
if elapsed < limit:
print(f"{symbol}: {side} 쿨다운 중 (남은 시간: {limit - elapsed:.0f}초)")
return True
return False
def _record_trade(self, symbol: str, side: str, signal: str) -> None:
"""매매 기록 저장."""
if self.cooldown_file is None:
return
current_time = datetime.now()
self.last_signal[symbol] = signal
self.buy_cooldown.setdefault(symbol, {})[side] = {
"datetime": current_time,
"signal": signal,
}
self._save_buy_cooldown()
def execute_trade_signal(
self,
symbol: str,
trade: strategy.TradeSignal,
balances: dict | None = None,
) -> bool:
"""TradeSignal 1건에 대해 현물 매수 또는 매도를 실행합니다."""
try:
coin_name = KR_COINS.get(symbol, symbol)
signal_name = trade.signal
close = trade.close
if trade.action == "sell":
if self._is_in_cooldown(symbol, "sell"):
return False
available = 0.0
if balances and symbol in balances:
available = float(balances[symbol].get("balance", 0))
if available <= 0:
print(f"{symbol}: 매도 신호({signal_name}) — 보유 없음, 스킵")
return False
sell_amount = available * strategy.get_sell_ratio(symbol, signal_name)
if sell_amount <= 0:
return False
self.sellCoinMarket(symbol, 0, sell_amount)
self._record_trade(symbol, "sell", signal_name)
print(f"{coin_name} ({symbol}) [매도 {signal_name}] ₩{close:.4f}, 수량 {sell_amount:.6f}")
self.sendMsg(
f"[KRW-COIN]\n• 매도 {coin_name} ({symbol}): {signal_name}{close:.4f}"
)
return True
if self._is_in_cooldown(symbol, "buy"):
return False
buy_amount = strategy.get_buy_amount(
symbol, signal_name, close, trend=trade.trend
)
if strategy.should_double_buy(symbol, signal_name, pd.DataFrame()):
buy_amount *= 2
executed = self.buyCoinMarket(symbol, buy_amount)
self._record_trade(symbol, "buy", signal_name)
print(
f"{coin_name} ({symbol}) [매수 {signal_name}] ₩{close:.4f} "
f"({buy_amount} KRW, 추세={trade.trend})"
)
self.sendMsg(
self.format_message(
symbol, coin_name, close, signal_name, executed or buy_amount
)
)
return True
except Exception as e:
print(f"Error trading {symbol}: {str(e)}")
return False
def process_wld_mtf(self, symbol: str, balances: dict | None = None) -> None:
""" """
WLD: (1~1440) BB·일목 위치 조합 매매. WLD: BB·일목 위치·추세만 출력 (자동 매매 없음).
USE_DISCOVERED_LIVE=True: discovered_rules.json + combination 특징
False: mtf_bb_policy.json BB MTF
""" """
from config import USE_DISCOVERED_LIVE from deepcoin.common.candle_features import describe_latest_position
from mtf_bb import load_frames_from_db, load_policy, print_latest_states from deepcoin.common.indicators import get_trend
from candle_features import describe_latest_position from deepcoin.data.mtf_bb import load_frames_from_db
try: try:
frames = load_frames_from_db(self, symbol) frames = load_frames_from_db(self, symbol)
@@ -329,43 +223,29 @@ class Monitor(HTS):
if df_1h is None or df_1h.empty: if df_1h is None or df_1h.empty:
df_1h = frames.get(ENTRY_INTERVAL) df_1h = frames.get(ENTRY_INTERVAL)
trend = strategy.get_trend(df_1d, df_1h) trend = get_trend(df_1d, df_1h)
print(f"{symbol} 추세: {trend}") print(f"{symbol} 추세(참고): {trend}")
print("--- 봉별 BB·일목 위치 ---") print("--- 봉별 BB·일목 위치 ---")
for iv in sorted(frames.keys()): for iv in sorted(frames.keys()):
pos = describe_latest_position(frames[iv], iv) pos = describe_latest_position(frames[iv], iv)
macd_s = ""
if pos.get("macd_hist") is not None:
macd_s = f" | MACD {pos.get('macd_state', '-')} h={pos['macd_hist']}"
stoch_s = ""
if pos.get("stoch_k") is not None:
stoch_s = (
f" | Stoch K={pos['stoch_k']} D={pos.get('stoch_d')} "
f"{pos.get('stoch_zone', '')}"
)
disp_s = ""
if pos.get("disparity"):
parts = [f"{p}={v:.1f}" for p, v in sorted(pos["disparity"].items())]
disp_s = " | D.I. " + " ".join(parts)
print( print(
f" {pos['label']:>6} | BB {pos['bb_zone']} {pos['bb_state']:>16} | " f" {pos['label']:>6} | BB {pos['bb_zone']} {pos['bb_state']:>16} | "
f"일목 {pos['ichi_position']} TK={pos['ichi_tk']}" f"일목 {pos['ichi_position']} TK={pos['ichi_tk']}"
f"{macd_s}{stoch_s}{disp_s}"
) )
if USE_DISCOVERED_LIVE:
print("모드: 전봉 BB·일목 조합 (discovered_rules)")
trade = strategy.evaluate_discovered_live(
symbol, frames, df_1d, df_1h, balances or {}
)
else:
policy = load_policy() or strategy.ACTIVE_MTF_POLICY
cfg = strategy.ACTIVE_CONFIG
print_latest_states(frames, cfg)
print(
f"MTF 정책: {policy.name} | 매수={policy.buy_interval}분 | "
f"매도={policy.sell_interval}"
)
entry = frames.get(ENTRY_INTERVAL)
trade = strategy.evaluate(
symbol,
entry if entry is not None else frames[policy.buy_interval],
df_1h,
df_1d,
config=cfg,
frames=frames,
policy=policy,
)
if trade is None:
print("신호 없음")
return
self.execute_trade_signal(symbol, trade, balances=balances)
except Exception as e: except Exception as e:
print(f"Error processing {symbol}: {str(e)}") print(f"Error processing {symbol}: {str(e)}")
@@ -376,8 +256,8 @@ class Monitor(HTS):
balances: dict | None = None, balances: dict | None = None,
use_inverse: bool = False, use_inverse: bool = False,
) -> None: ) -> None:
"""하위 호환: MTF 전략으로 위임 (use_inverse 무시).""" """하위 호환: 시장 상태 출력으로 위임."""
self.process_wld_mtf(symbol, balances=balances) self.process_wld_market_status(symbol)
def load_balances_dict(self) -> dict: def load_balances_dict(self) -> dict:
"""getBalances() 결과를 currency 키 dict로 변환.""" """getBalances() 결과를 currency 키 dict로 변환."""
@@ -414,19 +294,36 @@ class Monitor(HTS):
return message return message
# ------------- Data fetch ------------- # ------------- Data fetch -------------
def get_coin_data(self, symbol: str, interval: int = 60, to: str | None = None, retries: int = 3) -> pd.DataFrame | None: def get_coin_data(
self,
symbol: str,
interval: int = MONITOR_DEFAULT_INTERVAL,
to: str | None = None,
retries: int = MONITOR_API_RETRIES,
) -> pd.DataFrame | None:
base = BITHUMB_API_URL.rstrip("/")
count = BITHUMB_API_CANDLE_COUNT
for attempt in range(retries): for attempt in range(retries):
try: try:
if to is None: if to is None:
if interval == 1440: if interval >= DAILY_INTERVAL_MIN:
url = ("https://api.bithumb.com/v1/candles/days?market=KRW-{}&count=200").format(symbol) url = f"{base}/v1/candles/days?market=KRW-{symbol}&count={count}"
else: else:
url = ("https://api.bithumb.com/v1/candles/minutes/{}?market=KRW-{}&count=200").format(interval, symbol) url = (
f"{base}/v1/candles/minutes/{interval}"
f"?market=KRW-{symbol}&count={count}"
)
else: else:
if interval == 1440: if interval >= DAILY_INTERVAL_MIN:
url = ("https://api.bithumb.com/v1/candles/days?market=KRW-{}&count=200&to={}").format(symbol, to) url = (
f"{base}/v1/candles/days?market=KRW-{symbol}"
f"&count={count}&to={to}"
)
else: else:
url = ("https://api.bithumb.com/v1/candles/minutes/{}?market=KRW-{}&count=200&to={}").format(interval, symbol, to) url = (
f"{base}/v1/candles/minutes/{interval}"
f"?market=KRW-{symbol}&count={count}&to={to}"
)
headers = {"accept": "application/json"} headers = {"accept": "application/json"}
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
json_data = json.loads(response.text) json_data = json.loads(response.text)
@@ -447,11 +344,11 @@ class Monitor(HTS):
if not data.empty: if not data.empty:
return data return data
print(f"No data received for {symbol}, attempt {attempt + 1}") print(f"No data received for {symbol}, attempt {attempt + 1}")
time.sleep(0.5) time.sleep(MONITOR_SLEEP_AFTER_REQUEST_SEC)
except Exception as e: except Exception as e:
print(f"Attempt {attempt + 1} failed for {symbol}: {str(e)}") print(f"Attempt {attempt + 1} failed for {symbol}: {str(e)}")
if attempt < retries - 1: if attempt < retries - 1:
time.sleep(5) time.sleep(MONITOR_SLEEP_RATE_LIMIT_SEC)
continue continue
return None return None
@@ -459,7 +356,7 @@ class Monitor(HTS):
self, self,
symbol: str, symbol: str,
interval: int, interval: int,
bong_count: int = 3000, bong_count: int = MONITOR_API_BONG_COUNT,
verbose: bool = False, verbose: bool = False,
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
@@ -488,8 +385,8 @@ class Monitor(HTS):
if verbose and (step == 1 or step % 5 == 0 or len(data) >= bong_count): if verbose and (step == 1 or step % 5 == 0 or len(data) >= bong_count):
label = "일봉" if interval >= 1440 else f"{interval}" label = "일봉" if interval >= 1440 else f"{interval}"
print(f" [{label}] 요청 {step}회 — 누적 {len(data)}/{bong_count}") print(f" [{label}] 요청 {step}회 — 누적 {len(data)}/{bong_count}")
time.sleep(0.3) time.sleep(MONITOR_SLEEP_BETWEEN_CHUNKS_SEC)
to = to - relativedelta(minutes=interval * 200) to = to - relativedelta(minutes=interval * MONITOR_API_CHUNK_BARS)
if data is None or data.empty: if data is None or data.empty:
return pd.DataFrame() return pd.DataFrame()
data = data.set_index("datetime") data = data.set_index("datetime")
@@ -498,13 +395,38 @@ class Monitor(HTS):
data["datetime"] = data.index data["datetime"] = data.index
return data return data
@staticmethod
def db_row_limit_for_interval(interval: int, lookback_days: int) -> int:
"""
lookback_days 구간 + 지표 워밍업을 담을 SQLite LIMIT( 개수) 계산합니다.
Args:
interval: 간격(). 1440이면 일봉.
lookback_days: 과거 조회 일수.
Returns:
LIMIT에 넣을 최대 .
"""
if interval >= DAILY_INTERVAL_MIN:
return max(
lookback_days + DB_ROW_DAILY_PADDING_DAYS,
DB_ROW_MIN_DAILY_BARS,
)
bars_per_day = max((24 * 60) // max(interval, 1), 1)
return bars_per_day * lookback_days + DB_ROW_WARMUP_BARS
def get_coin_saved_data( def get_coin_saved_data(
self, symbol: str, interval: int, data: pd.DataFrame, db_path: str = "coins.db" self,
symbol: str,
interval: int,
data: pd.DataFrame,
db_path: str = DB_PATH,
max_rows: int = DB_READ_LIMIT_DEFAULT,
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
coins.db에서 저장된 봉을 읽고, API로 받은 최신 봉을 DB에 반영합니다. coins.db에서 저장된 봉을 읽고, API로 받은 최신 봉을 DB에 반영합니다.
downloader.py로 미리 적재해 두면 장기 MA 계산에 유리합니다. scripts/01_download.py로 미리 적재해 두면 장기 MA 계산에 유리합니다.
""" """
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
@@ -548,7 +470,7 @@ class Monitor(HTS):
cursor.execute( cursor.execute(
f"SELECT Open, Close, High, Low, Volume, ymdhms AS datetime " f"SELECT Open, Close, High, Low, Volume, ymdhms AS datetime "
f"FROM (SELECT Open, Close, High, Low, Volume, ymdhms " f"FROM (SELECT Open, Close, High, Low, Volume, ymdhms "
f"FROM {table_name} ORDER BY ymdhms DESC LIMIT 7000) " f"FROM {table_name} ORDER BY ymdhms DESC LIMIT {int(max_rows)}) "
f"ORDER BY datetime" f"ORDER BY datetime"
) )
result = cursor.fetchall() result = cursor.fetchall()
@@ -569,11 +491,13 @@ class Monitor(HTS):
df["datetime"] = df.index df["datetime"] = df.index
return df return df
def get_coin_some_data(self, symbol: str, interval: int) -> pd.DataFrame: def get_coin_some_data(
self, symbol: str, interval: int, db_max_rows: int | None = None
) -> pd.DataFrame:
""" """
WLD 시세: API 최신 + coins.db 과거 + 1분봉 최신 1개를 합칩니다. WLD 시세: API 최신 + coins.db 과거 + 1분봉 최신 1개를 합칩니다.
DB가 비어 있으면 API·1분봉만 사용합니다. 과거 적재는 downloader.py 실행. DB가 비어 있으면 API·1분봉만 사용합니다. 과거 적재는 scripts/01_download.py 실행.
""" """
data = self.get_coin_data(symbol, interval) data = self.get_coin_data(symbol, interval)
if data is None or data.empty: if data is None or data.empty:
@@ -584,7 +508,10 @@ class Monitor(HTS):
data_1 = data_1.copy() data_1 = data_1.copy()
data_1.at[data_1.index[-1], "Volume"] = data_1["Volume"].iloc[-1] * 60 data_1.at[data_1.index[-1], "Volume"] = data_1["Volume"].iloc[-1] * 60
saved_data = self.get_coin_saved_data(symbol, interval, data) row_limit = DB_READ_LIMIT_DEFAULT if db_max_rows is None else int(db_max_rows)
saved_data = self.get_coin_saved_data(
symbol, interval, data, max_rows=row_limit
)
parts = [data] parts = [data]
if saved_data is not None and not saved_data.empty: if saved_data is not None and not saved_data.empty:
parts.append(saved_data) parts.append(saved_data)

View File

@@ -0,0 +1,34 @@
"""
WLD(월드코인) 실시간 모니터 — BB·일목 위치·추세 출력 (자동 매매 없음).
"""
from datetime import datetime
import time
from config import COIN_NAME, MONITOR_LOOP_SLEEP_SEC, SYMBOL
from deepcoin.ops.monitor import Monitor
class MonitorCoin(Monitor):
"""WLD 시장 상태 주기 출력."""
def monitor_wld(self) -> None:
"""전 봉 BB·일목·추세를 콘솔에 출력합니다."""
print(
"[{}] {} ({})".format(
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
COIN_NAME,
SYMBOL,
)
)
self.process_wld_market_status(SYMBOL)
def run_schedule(self) -> None:
"""MONITOR_LOOP_SLEEP_SEC 간격으로 상태를 출력합니다."""
while True:
self.monitor_wld()
time.sleep(MONITOR_LOOP_SLEEP_SEC)
if __name__ == "__main__":
MonitorCoin(cooldown_file=None).run_schedule()

595
deepcoin/ops/simulation.py Normal file
View File

@@ -0,0 +1,595 @@
"""
WLD 볼린저 밴드 차트.
python scripts/05_chart_bb.py
python scripts/05_chart_truth.py
python scripts/02_ground_truth.py
"""
from __future__ import annotations
import sys
import webbrowser
from pathlib import Path
import numpy as np
import pandas as pd
import plotly.graph_objs as go
from plotly.subplots import make_subplots
from config import (
CHART_LOOKBACK_DAYS,
COIN_NAME,
DISPARITY_OVERBOUGHT,
DISPARITY_OVERSOLD,
DISPARITY_PERIODS,
ENTRY_INTERVAL,
GROUND_TRUTH_FILE,
GT_INITIAL_CASH_KRW,
GT_MARKER_SIZE_MAX,
GT_MARKER_SIZE_MIN,
MACD_FAST,
MACD_SIGNAL,
MACD_SLOW,
STOCH_D_PERIOD,
STOCH_K_PERIOD,
SYMBOL,
TRADING_FEE_RATE,
TREND_INTERVAL_1D,
TREND_INTERVAL_1H,
)
from deepcoin.common.indicators import apply_bar_indicators, disparity_column, get_trend
from deepcoin.ops.monitor import Monitor
from deepcoin.data.mtf_bb import interval_label, load_frames_from_db
from deepcoin.paths import CHART_BB_HTML, CHART_TRUTH_HTML, resolve_ground_truth_file
OUTPUT_HTML = CHART_BB_HTML
TRUTH_HTML = CHART_TRUTH_HTML
GROUND_TRUTH_PATH = resolve_ground_truth_file()
REPORT_DIR = CHART_BB_HTML.parent
def interval_chart_label(interval_min: int) -> str:
"""차트 제목용 봉 라벨."""
if interval_min >= 1440:
return "일봉"
return f"{interval_min}분봉"
def _marker_sizes(trades: list[dict], action: str) -> list[float]:
"""비중(weight, 0~1)에 비례한 삼각형 크기."""
pts = [t for t in trades if t.get("action") == action]
if not pts:
return []
lo, hi = float(GT_MARKER_SIZE_MIN), float(GT_MARKER_SIZE_MAX)
return [
lo + (hi - lo) * min(max(float(t.get("weight", 1.0)), 0.05), 1.0)
for t in pts
]
def _add_truth_markers(fig, trades: list[dict], row: int = 1) -> None:
"""정답 매수·매도 마커 (삼각형 크기 = 비중)."""
for action, color, symbol, label in [
("buy", "#16a34a", "triangle-up", "정답 매수"),
("sell", "#dc2626", "triangle-down", "정답 매도"),
]:
pts = [t for t in trades if t.get("action") == action]
if not pts:
continue
sizes = _marker_sizes(trades, action)
fig.add_trace(
go.Scatter(
x=[pd.Timestamp(t["dt"]) for t in pts],
y=[t["price"] for t in pts],
mode="markers",
name=label,
legendgroup=label,
marker=dict(
symbol=symbol,
size=sizes,
sizemode="diameter",
color=color,
line=dict(width=1.5, color="#111"),
),
hovertext=[
f"{label}<br>{t['dt'][:16]}<br>₩{t['price']:,.0f}"
f"<br>비중 {float(t.get('weight', 1))*100:.0f}%"
f"<br>{t.get('memo', '')}"
for t in pts
],
hovertemplate="%{hovertext}<extra></extra>",
),
row=row,
col=1,
)
def build_chart_html(
df: pd.DataFrame,
trend: str,
interval_min: int = ENTRY_INTERVAL,
note: str = "",
truth_trades: list[dict] | None = None,
title_suffix: str = "BB 차트",
pnl_summary: dict | None = None,
) -> str:
"""BB·이격도·RSI·MACD·스토캐스틱·거래량 차트 HTML."""
df = apply_bar_indicators(df.copy())
iv_label = interval_chart_label(interval_min)
close_last = float(df["Close"].iloc[-1])
bb_pos = None
if "bb_pos" in df.columns and pd.notna(df["bb_pos"].iloc[-1]):
bb_pos = float(df["bb_pos"].iloc[-1])
disp_title = "이격도 " + ",".join(str(p) for p in DISPARITY_PERIODS)
fig = make_subplots(
rows=6,
cols=1,
shared_xaxes=True,
vertical_spacing=0.03,
row_heights=[0.42, 0.11, 0.11, 0.11, 0.13, 0.12],
subplot_titles=(
f"{COIN_NAME} ({SYMBOL}) {iv_label}",
disp_title,
f"Stochastic ({STOCH_K_PERIOD},{STOCH_D_PERIOD})",
"RSI (14)",
f"MACD ({MACD_FAST},{MACD_SLOW},{MACD_SIGNAL})",
"거래량",
),
)
disp_colors = ("#0d9488", "#7c3aed", "#ca8a04")
fig.add_trace(
go.Candlestick(
x=df.index,
open=df["Open"],
high=df["High"],
low=df["Low"],
close=df["Close"],
name=f"{iv_label} 캔들",
increasing_line_color="#ef4444",
decreasing_line_color="#3b82f6",
),
row=1,
col=1,
)
if "MA" in df.columns:
fig.add_trace(
go.Scatter(
x=df.index,
y=df["MA"],
name="BB 중심",
line=dict(color="#64748b", width=1, dash="dot"),
),
row=1,
col=1,
)
if "Upper" in df.columns:
fig.add_trace(
go.Scatter(
x=df.index,
y=df["Upper"],
name="BB 상단",
line=dict(color="#94a3b8", width=1),
),
row=1,
col=1,
)
if "Lower" in df.columns:
fig.add_trace(
go.Scatter(
x=df.index,
y=df["Lower"],
name="BB 하단",
line=dict(color="#94a3b8", width=1),
),
row=1,
col=1,
)
if truth_trades:
_add_truth_markers(fig, truth_trades, row=1)
disp_row = 2
for i, p in enumerate(DISPARITY_PERIODS):
col = disparity_column(p)
if col not in df.columns:
continue
color = disp_colors[i % len(disp_colors)]
fig.add_trace(
go.Scatter(
x=df.index,
y=df[col],
name=f"D.I. {p}",
line=dict(color=color, width=1),
),
row=disp_row,
col=1,
)
if any(disparity_column(p) in df.columns for p in DISPARITY_PERIODS):
fig.add_hline(
y=100, line_dash="solid", line_color="#64748b", row=disp_row, col=1
)
fig.add_hline(
y=DISPARITY_OVERBOUGHT,
line_dash="dot",
line_color="#ef4444",
row=disp_row,
col=1,
)
fig.add_hline(
y=DISPARITY_OVERSOLD,
line_dash="dot",
line_color="#16a34a",
row=disp_row,
col=1,
)
stoch_row = 3
if "stoch_k" in df.columns:
fig.add_trace(
go.Scatter(
x=df.index,
y=df["stoch_k"],
name="Stoch %K",
line=dict(color="#0ea5e9", width=1),
),
row=stoch_row,
col=1,
)
fig.add_trace(
go.Scatter(
x=df.index,
y=df["stoch_d"],
name="Stoch %D",
line=dict(color="#f97316", width=1),
),
row=stoch_row,
col=1,
)
fig.add_hline(y=80, line_dash="dot", line_color="#9ca3af", row=stoch_row, col=1)
fig.add_hline(y=20, line_dash="dot", line_color="#9ca3af", row=stoch_row, col=1)
rsi_row = 4
if "RSI" in df.columns:
fig.add_trace(
go.Scatter(
x=df.index,
y=df["RSI"],
name="RSI",
line=dict(color="#7c3aed"),
),
row=rsi_row,
col=1,
)
fig.add_hline(y=70, line_dash="dot", line_color="#9ca3af", row=rsi_row, col=1)
fig.add_hline(y=30, line_dash="dot", line_color="#9ca3af", row=rsi_row, col=1)
macd_row = 5
vol_row = 6
if "macd_hist" in df.columns:
colors = np.where(df["macd_hist"].astype(float) >= 0, "#ef4444", "#3b82f6")
fig.add_trace(
go.Bar(
x=df.index,
y=df["macd_hist"],
name="MACD Hist",
marker_color=colors,
),
row=macd_row,
col=1,
)
fig.add_trace(
go.Scatter(
x=df.index,
y=df["macd_line"],
name="MACD",
line=dict(color="#2563eb", width=1),
),
row=macd_row,
col=1,
)
fig.add_trace(
go.Scatter(
x=df.index,
y=df["macd_signal"],
name="Signal",
line=dict(color="#ea580c", width=1, dash="dot"),
),
row=macd_row,
col=1,
)
fig.add_trace(
go.Bar(
x=df.index,
y=df["Volume"],
name="Volume",
marker_color="#cbd5e1",
),
row=vol_row,
col=1,
)
fig.update_layout(
height=1180,
template="plotly_white",
xaxis_rangeslider_visible=False,
legend=dict(orientation="h", y=1.05, x=0),
margin=dict(l=60, r=30, t=90, b=40),
)
fig.update_yaxes(title_text="가격 (KRW)", row=1, col=1)
fig.update_yaxes(title_text="이격도", row=2, col=1)
fig.update_yaxes(title_text="Stoch", row=3, col=1, range=[0, 100])
fig.update_yaxes(title_text="RSI", row=4, col=1, range=[0, 100])
fig.update_yaxes(title_text="MACD", row=5, col=1)
chart_html = fig.to_html(full_html=False, include_plotlyjs="cdn")
note_html = f"<p class='note'>{note}</p>" if note else ""
bb_pos_txt = f"{bb_pos:.2f}" if bb_pos is not None else "-"
pnl = pnl_summary or {}
if truth_trades and not pnl:
from deepcoin.ground_truth.ground_truth import simulate_truth_portfolio
pnl = simulate_truth_portfolio(
truth_trades,
initial_cash=GT_INITIAL_CASH_KRW,
fee_rate=TRADING_FEE_RATE,
last_price=close_last,
)
trade_rows = ""
if truth_trades:
from deepcoin.ground_truth.ground_truth import simulate_truth_portfolio_steps
steps = simulate_truth_portfolio_steps(
truth_trades,
initial_cash=GT_INITIAL_CASH_KRW,
fee_rate=TRADING_FEE_RATE,
)
step_key = {
(s["dt"], s["action"], float(s["price"]), float(s["weight"])): s
for s in steps
}
sorted_trades = sorted(truth_trades, key=lambda x: x["dt"])
trade_rows += f"""
<tr class="initial-row">
<td>시작</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td><b>₩{GT_INITIAL_CASH_KRW:,.0f}</b></td>
<td>초기 현금 (보유 0)</td>
</tr>"""
for t in sorted_trades:
cls = "buy" if t["action"] == "buy" else "sell"
mark = "매수" if t["action"] == "buy" else "매도"
ret = t.get("forward_return_pct")
ret_s = f" (+{ret}%)" if ret is not None else ""
w = float(t.get("weight", 1.0))
key = (t["dt"], t["action"], float(t["price"]), w)
step = step_key.get(key)
if step:
total_s = f"{step['total_asset_krw']:,.0f}"
hold_s = f" (현금 ₩{step['cash_krw']:,.0f} + 코인 {step['holding_qty']:,.2f}개)"
else:
total_s = "-"
hold_s = ""
trade_rows += f"""
<tr>
<td>{t['dt'][:16]}</td>
<td class="{cls}">{mark}</td>
<td>{w*100:.0f}%</td>
<td>₩{t['price']:,.0f}{ret_s}</td>
<td><b>{total_s}</b>{hold_s}</td>
<td>{t.get('memo', '')}</td>
</tr>"""
trade_table = ""
if truth_trades:
if not trade_rows:
trade_rows = "<tr><td colspan='6'>타점 없음</td></tr>"
mark_note = ""
if pnl.get("mark_price"):
mark_note = (
f" 상단 최종 자산은 미청산 포함 종가 ₩{pnl['mark_price']:,.0f} 평가."
)
trade_table = f"""
<h2>정답 타점 (ground_truth)</h2>
<p class="meta">삼각형 크기 = 비중. 매수: 저점 분할 / 매도: 고점 1~2회.
총평가 = 체결 직후 현금 + 보유×체결가.{mark_note}</p>
<table>
<thead><tr><th>시각</th><th>구분</th><th>비중</th><th>가격</th><th>총 평가금액</th><th>해석</th></tr></thead>
<tbody>{trade_rows}</tbody>
</table>"""
pnl_cards = ""
if truth_trades and pnl.get("initial_cash_krw") is not None:
pnl_cards = f"""
<div class="card"><span>시작</span><b>₩{pnl['initial_cash_krw']:,.0f}</b></div>
<div class="card"><span>최종 자산</span><b>₩{pnl['final_asset_krw']:,.0f}</b></div>
<div class="card"><span>수익금</span><b>₩{pnl['pnl_krw']:+,.0f}</b></div>
<div class="card"><span>수익률</span><b>{pnl['pnl_pct']:+.2f}%</b></div>
<div class="card"><span>수수료</span><b>₩{pnl['total_fees_krw']:,.0f}</b></div>"""
if pnl.get("holding_qty", 0) > 0:
pnl_cards += f"""
<div class="card"><span>미청산</span><b>{pnl['holding_qty']}개 (₩{pnl['holding_value_krw']:,.0f})</b></div>"""
return f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<title>{SYMBOL} {title_suffix}</title>
<style>
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f8fafc; }}
h1 {{ font-size: 1.35rem; }}
.meta {{ color: #475569; font-size: 0.9rem; }}
.note {{ background: #f1f5f9; border: 1px solid #cbd5e1; padding: 10px; border-radius: 6px; color: #334155; }}
.cards {{ display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; }}
.card {{ background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 14px; }}
.card span {{ font-size: 0.75rem; color: #64748b; display: block; }}
.card b {{ font-size: 1.05rem; }}
.chart-wrap {{ background:#fff; border:1px solid #e2e8f0; border-radius:8px; padding:8px; }}
.legend-box {{ font-size:0.85rem; color:#475569; margin-bottom:10px; }}
table {{ width:100%; border-collapse:collapse; background:#fff; font-size:0.85rem; }}
th, td {{ border:1px solid #e2e8f0; padding:8px; text-align:left; }}
th {{ background:#f1f5f9; }}
td.buy {{ color:#16a34a; font-weight:600; }}
td.sell {{ color:#dc2626; font-weight:600; }}
</style>
</head>
<body>
<h1>{COIN_NAME} ({SYMBOL}) {title_suffix}</h1>
<p class="meta">추세(참고): {trend} | 기간: {df.index[0]} ~ {df.index[-1]} | 봉 수: {len(df)}</p>
{note_html}
<div class="legend-box">▲ 매수 · ▼ 매도 — 삼각형이 클수록 비중이 큽니다.</div>
<div class="cards">
<div class="card"><span>종가</span><b>₩{close_last:,.2f}</b></div>
<div class="card"><span>BB %B</span><b>{bb_pos_txt}</b></div>
<div class="card"><span>정답 타점</span><b>{len(truth_trades) if truth_trades else 0}건</b></div>
{pnl_cards}
</div>
<div class="chart-wrap">{chart_html}</div>
{trade_table}
</body>
</html>"""
def _frames_to_mtf(
frames: dict[int, pd.DataFrame],
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
"""전 간격 frames에서 1d/1h/3m 추출."""
df_3m = frames.get(ENTRY_INTERVAL)
if df_3m is None or df_3m.empty:
raise ValueError(f"{ENTRY_INTERVAL}분봉 데이터 없음")
df_1d = frames.get(TREND_INTERVAL_1D)
if df_1d is None or df_1d.empty:
df_1d = df_3m
df_1h = frames.get(TREND_INTERVAL_1H)
if df_1h is None or df_1h.empty:
df_1h = df_3m
return df_1d, df_1h, df_3m
def load_chart_frames() -> dict[int, pd.DataFrame] | None:
"""coins.db 전 간격 로드. 부족 시 None."""
monitor = Monitor(cooldown_file=None)
print(f"DB 조회: 최근 {CHART_LOOKBACK_DAYS}일 (CHART_LOOKBACK_DAYS)")
frames = load_frames_from_db(monitor, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
if ENTRY_INTERVAL not in frames:
print("coins.db 데이터 부족. python scripts/01_download.py 실행 후 재시도.")
return None
return frames
def run_ground_truth_chart(open_browser: bool = True) -> Path:
"""
정답 타점을 생성·저장하고 마커가 포함된 HTML 차트를 만듭니다.
Args:
open_browser: True면 브라우저로 HTML을 엽니다.
Returns:
HTML 파일 경로.
"""
from deepcoin.ground_truth.ground_truth import run_from_db
data = run_from_db()
frames = load_chart_frames()
if frames is None:
raise RuntimeError("차트 데이터 로드 실패")
df_1d, df_1h, df_3m = _frames_to_mtf(frames)
trend = get_trend(df_1d, df_1h)
df_chart = apply_bar_indicators(df_3m)
trades = data.get("trades") or []
summary = data.get("summary") or {}
html = build_chart_html(
df_chart,
trend,
note=data.get("note", ""),
truth_trades=trades,
title_suffix=f"정답 타점 ({CHART_LOOKBACK_DAYS}일)",
pnl_summary=summary if summary.get("pnl_krw") is not None else None,
)
REPORT_DIR.mkdir(parents=True, exist_ok=True)
TRUTH_HTML.write_text(html, encoding="utf-8")
print(f"HTML: {TRUTH_HTML}")
if open_browser:
webbrowser.open(TRUTH_HTML.resolve().as_uri())
return TRUTH_HTML
def run_chart(open_browser: bool = True) -> Path:
"""
3분봉 BB 차트 HTML을 생성합니다.
Args:
open_browser: True면 기본 브라우저로 HTML을 엽니다.
Returns:
저장된 HTML 경로.
"""
frames = load_chart_frames()
if frames is None:
raise RuntimeError("차트 데이터 로드 실패")
df_1d, df_1h, df_3m = _frames_to_mtf(frames)
trend = get_trend(df_1d, df_1h)
df_chart = apply_bar_indicators(df_3m)
print(f"\n추세(참고): {trend}")
print(f"3분: {df_chart.index[0]} ~ {df_chart.index[-1]} ({len(df_chart)}봉)")
html = build_chart_html(
df_chart,
trend,
note="자동 매수·매도 전략은 사용하지 않습니다.",
)
REPORT_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_HTML.write_text(html, encoding="utf-8")
print(f"HTML: {OUTPUT_HTML}")
if open_browser:
webbrowser.open(OUTPUT_HTML.resolve().as_uri())
return OUTPUT_HTML
def print_usage() -> None:
print(
"""
DeepCoin simulation.py
python simulation.py
WLD 3분봉 BB 차트 → docs/charts/wld_bb_chart.html
python simulation.py truth
정답 타점 생성 → ground_truth_trades.json
차트 → docs/02_ground_truth/wld_ground_truth_chart.html
"""
)
def main() -> None:
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help", "help"):
print_usage()
return
if len(sys.argv) > 1 and sys.argv[1] in ("truth", "ground-truth", "gt"):
print("=" * 60)
print("정답 타점 생성 + 차트")
print("=" * 60)
run_ground_truth_chart()
print("\n완료.")
return
if len(sys.argv) > 1:
print(f"알 수 없는 옵션: {sys.argv[1]}\n")
print_usage()
return
print("=" * 60)
print("WLD BB 차트 (매매 전략 없음)")
print("=" * 60)
run_chart()
print("\n완료.")
if __name__ == "__main__":
main()

95
deepcoin/paths.py Normal file
View File

@@ -0,0 +1,95 @@
"""
DeepCoin 프로젝트 경로 (data + docs 통합).
docs/
reference/ 가이드·기법 명세 (Git 추적)
02_ground_truth/ … 05_ops/, charts/ 단계별 산출물 (로컬 재생성, Git 제외)
"""
from __future__ import annotations
import os
from pathlib import Path
# DeepCoin/ (이 파일: DeepCoin/deepcoin/paths.py)
PROJECT_ROOT = Path(__file__).resolve().parents[1]
# --- data ---
DATA_DIR = PROJECT_ROOT / "data"
DB_DIR = DATA_DIR
GROUND_TRUTH_DIR = DATA_DIR / "ground_truth"
OPS_STATE_DIR = DATA_DIR / "ops"
COOLDOWN_FILE = OPS_STATE_DIR / "coins_buy_time.json"
# --- docs (reference + 단계별 산출물) ---
DOCS_DIR = PROJECT_ROOT / "docs"
DOCS_REFERENCE_DIR = DOCS_DIR / "reference"
DOCS_CHARTS = DOCS_DIR / "charts"
DOCS_GROUND_TRUTH = DOCS_DIR / "02_ground_truth"
DOCS_ANALYSIS = DOCS_DIR / "03_analysis"
DOCS_MATCHING = DOCS_DIR / "04_matching"
DOCS_OPS = DOCS_DIR / "05_ops"
ANALYSIS_TRADES_CSV = DOCS_ANALYSIS / "general_analysis_trades.csv"
ANALYSIS_REPORT_HTML = DOCS_ANALYSIS / "general_analysis_report.html"
ANALYSIS_CAPABILITY_HTML = DOCS_ANALYSIS / "general_analysis_capability.html"
ANALYSIS_LATEST_DIR = DOCS_ANALYSIS / "latest"
CHART_BB_HTML = DOCS_CHARTS / "wld_bb_chart.html"
CHART_TRUTH_HTML = DOCS_GROUND_TRUTH / "wld_ground_truth_chart.html"
# 하위 호환 (구 reports/ 이름)
REPORTS_DIR = DOCS_DIR
REPORTS_CHARTS = DOCS_CHARTS
REPORTS_GROUND_TRUTH = DOCS_GROUND_TRUTH
REPORTS_ANALYSIS = DOCS_ANALYSIS
REPORTS_MATCHING = DOCS_MATCHING
REPORTS_OPS = DOCS_OPS
def resolve_db_path() -> Path:
"""존재하는 coins.db 경로."""
candidates = [
DATA_DIR / "coins.db",
PROJECT_ROOT / "coins.db",
]
for p in candidates:
if p.is_file():
return p
return DATA_DIR / "coins.db"
def resolve_ground_truth_file() -> Path:
"""존재하는 ground_truth_trades.json 경로."""
name = os.getenv("GROUND_TRUTH_FILE", "ground_truth_trades.json")
p = Path(name)
if p.is_absolute():
return p
candidates = [
GROUND_TRUTH_DIR / "ground_truth_trades.json",
PROJECT_ROOT / "ground_truth_trades.json",
PROJECT_ROOT / name,
]
for c in candidates:
if c.is_file():
return c
return GROUND_TRUTH_DIR / "ground_truth_trades.json"
def ensure_dirs() -> None:
"""단계별 출력·가이드 디렉터리 생성."""
for d in (
DATA_DIR,
GROUND_TRUTH_DIR,
OPS_STATE_DIR,
DOCS_DIR,
DOCS_REFERENCE_DIR,
DOCS_CHARTS,
DOCS_GROUND_TRUTH,
DOCS_ANALYSIS,
DOCS_MATCHING,
DOCS_OPS,
ANALYSIS_LATEST_DIR,
):
d.mkdir(parents=True, exist_ok=True)

View File

@@ -1,21 +0,0 @@
{
"name": "discovered_best",
"buy_all": [
"m3:cross_up_lower",
"m60:ichi_tk_bull",
"d1:!ichi_below_cloud",
"m3:bb_zone_low",
"m3:ichi_above_cloud"
],
"buy_any": [],
"sell_all": [
"m60:cross_up_upper"
],
"sell_stop": [
"m3:cross_down_lower"
],
"train_return_pct": 0.7385912698412721,
"test_return_pct": 0.0,
"full_return_pct": 0.7385912698412721,
"trade_count": 2
}

View File

@@ -1,223 +0,0 @@
# 로고스(Logos) 매매 타점 전략 설계
BB predicate 탐색(`discovered_rules`)과 **분리**된, 차트 구조·추세·과열을 우선하는 **3분 현물** 전략입니다.
수동 타점(`logos_trades.json`)은 이 전략의 **교사 데이터(벤치마크)** 로 사용합니다.
---
## Plan (계획)
### 목적
- **저점 근처 매수**, **고점·과열에서 매도**, **추격 매수·바닥 매도**를 시스템적으로 배제한다.
- 3분봉 실행, 1시간·일봉으로 **방향 필터**만 거는 MTF 구조를 유지한다.
- 체결은 **엣지(전환) 1회** + **쿨다운·최소 봉 간격**으로 휩소를 줄인다.
### 목표 KPI (백테스트·실거래 공통)
| KPI | 목표 |
|-----|------|
| 매도가 ≥ 직전 매수가 비율 | 60% 이상 |
| 월 거래 횟수 (3분 WLD) | 8~40회 (과다 체결 방지) |
| 최대 연속 손실 매도 | 3회 이하 |
| 수동 로고스 타점 일치율 | ±3봉(9분), ±2% 가격 이내 50% 이상 (캘리브레이션) |
### 전략 원칙 (5가지)
1. **구조 우선**: 지표 한 개보다 «바닥 형성 → 눌림 → 가속 → 과열» 순서를 본다.
2. **가치 매수만**: 하단 돌파·망치·밴드 하단 구간. 상단 구간 단독 매수 금지.
3. **익절은 강도로**: 밴드 상단 돌파만으로 팔지 않고, **위치·캔들·RSI·거래량**이 맞을 때만.
4. **추세 보유**: 눌림(예: 378)은 **상위 추세가 살아 있으면** 버틴다.
5. **현물 순환**: 공매도 없음. 매도 = 보유 청산 또는 익절 후 재진입 대기.
---
## Do (실행 구조)
### 아키텍처 (4계층)
```mermaid
flowchart TB
subgraph L1 [L1 레짐 1D/1H]
R1[추세: up / range / down]
R2[일목: 구름 위·아래]
end
subgraph L2 [L2 구조 3m/15m/60m]
S1[스윙 고저 pivot]
S2[고저점 상승 HL / 하락 LH]
end
subgraph L3 [L3 트리거 3m]
B1[매수 A: 바닥·반등]
B2[매수 B: 추세 눌림]
X1[매도: 익절·과열]
X2[매도: 손절]
end
subgraph L4 [L4 체결]
E1[엣지 트리거]
E2[쿨다운·최소봉격]
end
L1 --> L2 --> L3 --> L4
```
### L1. 레짐 필터 (1D + 1H)
| 레짐 | 매수 | 매도(익절) |
|------|------|------------|
| **up** | A·B 허용 | 전량·분할 허용 |
| **range** | A만 (바닥형), B 제한 | 분할 위주 |
| **down** | A만 소량, B 금지 | 보유 시 손절·약익절만 |
판별(기존 `strategy.get_trend` 활용):
- 1H·1D MA 정배열/역배열 + 갭
- 보조: 일봉 `!ichi_below_cloud` (매수), 구름 아래 매수 금지
### L2. 구조 (스윙)
| 간격 | pivot order (3분봉 개수) | 용도 |
|------|--------------------------|------|
| 3m | 40 (~2시간) | 주요 고저 |
| 3m | 15 (~45분) | 보조 눌림 |
| 15m | 20 | 중기 저항 |
정의:
- **스윙 저점**: 좌우 `order` 봉보다 `Low`가 낮은 봉
- **스윙 고점**: 좌우 `order` 봉보다 `High`가 높은 봉
- **HL(상승)**: 직전 스윙 저점 < 현재 스윙 저점
- **과열 고점**: 종가가 120봉 최고가 대비 97% 이상
### L3-A. 매수 트리거
#### A. 바닥·투매 종료 (5/18 340 유형)
**엣지**로만 진입. 아래 **모두** 충족:
| # | 조건 |
|---|------|
| A1 | 3m `bb_pos` < 0.35 또는 `bb_zone_bottom` |
| A2 | `cross_up_lower` **또는** `hammer` (전봉 대비 신규) |
| A3 | RSI(14) < 42 **그리고** 전봉 대비 RSI 상승 |
| A4 | 120봉 최저가 대비 종가 ≤ 103% (진정한 바닥권) |
| A5 | 레짐 ≠ down 또는 일봉 구름 아래 아님 |
**금지**: `bb_pos` ≥ 0.55 단독, `bb_zone_top`, 당일 `shooting_star` + 고점권
#### B. 추세 눌림 재진입 (5/23 392, 5/25 427 유형)
**엣지** + 레짐 **up**:
| # | 조건 |
|---|------|
| B1 | 1H 추세 up, 3m `bb_zone_low` 또는 `cross_up_lower` |
| B2 | 직전 스윙 고점 대비 3~12% 조정(눌림 깊이) |
| B3 | 3m `bb_pos` < 0.50 |
| B4 | 15m `ichi_tk_bull` 또는 전환선 지지 |
| B5 | 마지막 매도 후 ≥ 20봉(1시간) |
**금지**: 450+ 구간 «급등 후 재추격»(5/26 04:00 유형) — `bb_pos` > 0.65 신규 매수 차단
### L3-B. 매도 트리거
#### C. 구조적 익절 (5/23 00:30, 5/24 464 유형)
**엣지** + 보유 중:
| # | 조건 |
|---|------|
| C1 | 3m **스윙 고점** 확정(피벗) **또는** `cross_up_upper` |
| C2 | `bb_pos`**0.65** (저점 익절 방지) |
| C3 | **NOT** (`hammer` OR `bb_zone_bottom` 동일 봉) |
| C4 | 종가 ≥ BB 중심(MA) |
분할: 1차 C만 충족 시 50% 익절, 2차 조건(D) 시 나머지 (구현 옵션).
#### D. 과열·불꽃 익절 (5/26 603 유형)
| # | 조건 |
|---|------|
| D1 | 3m `bb_pos` ≥ 0.90 **또는** 20봉 누적 상승률 ≥ 8% |
| D2 | 거래량 > 20봉 평균 × 1.5 |
| D3 | `shooting_star` 또는 윗꼬리 비율 > 0.45 |
**전량 매도** 우선.
#### E. 손절 (필수)
| # | 조건 |
|---|------|
| E1 | 3m `cross_down_lower` **엣지** |
| E2 | 또는 매수가 대비 -3% (설정값) |
**금지**: `bb_pos` < 0.40 에서 `cross_up_upper` 단독 매도 (5/26 01:48 유형)
### L4. 체결 규칙 (공통)
| 항목 | 기본값 | 설명 |
|------|--------|------|
| `SIGNAL_EDGE_ONLY` | true | False→True 봉만 |
| `TRADE_MIN_GAP_BARS` | 5 | 체결 후 15분 |
| `BUY_COOLDOWN_SEC` | 1800 | 매수 간 30분 |
| `SELL_COOLDOWN_SEC` | 900 | 매도 간 15분 |
| `SELL_MIN_BB_POS` | 0.40 | 이보다 낮으면 C 매도 금지 |
| `BUY_MAX_BB_POS_CHASE` | 0.55 | 이보다 높으면 value 트리거 없이 매수 금지 |
우선순위(보유 중): **E 손절 > D 과열 > C 익절**
---
## 수동 타점과의 매핑
| 수동 타점 | 전략 분류 | 자동화 핵심 조건 |
|-----------|-----------|------------------|
| 5/18 340 매수 | A 바닥 | A1~A4 + hammer |
| 5/23 00:30 445 매도 | C+D | pivot 고점 + bb_pos≥0.65 |
| 5/23 392 매수 | B 눌림 | up + bb_zone_low + HL |
| 5/24 464 매도 | C | cross_up_upper + 과열 |
| 5/25 427 매수 | B | 급등 전 마지막 눌림, bb_pos<0.5 |
| 5/26 603 매도 | D | bb_pos·거래량·급등률 |
| 5/28 420 매수 | A 급락 바닥 | 1일 -20% 후 첫 3m 저점 |
| 5/29 441 매도 | C | 반등 실패·중심선 이탈 |
---
## Check (검토)
### 백테스트 절차
1. `python simulation.py` — 로고스 전략 체결 HTML (`reports/wld_bb_simulation.html`)
2. `python simulation.py benchmark` — 수동 정답 참고 (`reports/wld_logos_benchmark.html`)
### 리스크
| 리스크 | 완화 |
|--------|------|
| 눌림 구간 손실 확대 | E 손절, down 레짐 매수 축소 |
| 급등 후 재매수 | `BUY_MAX_BB_POS_CHASE` |
| 바닥에서 익절 | `SELL_MIN_BB_POS` + 망치 동봉 매도 금지 |
| 거래 과다 | 엣지 + 쿨다운 + pivot 최소 간격 |
---
## Act (개선)
### 자동 전략 (코드 고정)
벤치마크 1위였던 S9 로직을 `logos_strategy.py` 상단 상수로만 유지합니다.
`logos_best_policy.json`, `LogosPolicy`, `logos-fit` 제거.
1. **Phase 3**: `USE_LOGOS_LIVE` 실거래 스위치
2. **Phase 4**: 눌림/바닥 분기 강화
---
## 코드 위치
| 파일 | 역할 |
|------|------|
| `docs/LOGOS_STRATEGY.md` | 본 설계서 |
| `logos_strategy.py` | 신호·체결 엔진 |
| `logos_trades.json` | 수동 벤치마크 |
| `logos_chart.py` | `simulation.py` 와 동일 진입 |
| `simulation.py` | 기본 실행 = 로고스 전략 HTML |
| `simulation.py benchmark` | 수동 정답 참고 HTML |

12
docs/README.md Normal file
View File

@@ -0,0 +1,12 @@
# docs
| 경로 | 용도 | Git |
|------|------|-----|
| [reference/](reference/) | 로드맵·구조·GT 가이드·기법 명세 (`trade_anaysis.html`) | 추적 |
| [02_ground_truth/](02_ground_truth/) | 정답 차트 HTML | 제외 (재생성) |
| [03_analysis/](03_analysis/) | enrich CSV·GT 스냅샷·점검 HTML | 제외 |
| [04_matching/](04_matching/) | 규칙·유사도 (예정) | 제외 |
| [05_ops/](05_ops/) | 운영 로그 (예정) | 제외 |
| [charts/](charts/) | BB 등 차트 HTML | 제외 |
실행·경로 상세: [reference/STRUCTURE.md](reference/STRUCTURE.md)

View File

@@ -0,0 +1,33 @@
# 정답 타점 (Ground Truth)
1달(기본 45일) 3분봉 구간에서 **사후적 최적 스윙** 매수·매도 라벨을 만듭니다.
실시간 매매 전략이 아니라, 이후 전략 검증·학습용 **정답 데이터**입니다.
## Plan
- **목적**: 차트 상 의미 있는 저점 매수·고점 매도를 JSON으로 고정
- **방법**: 고점(major swing)에서 1~2회 매도 · 저점(ZigZag+BB)에서 분할 매수 · 삼각형 크기=비중
## Do
```bash
python scripts/02_ground_truth.py # ground_truth_trades.json
python scripts/05_chart_truth.py # JSON + HTML 차트
```
## Check
| 환경 변수 | 기본 | 설명 |
|-----------|------|------|
| `CHART_LOOKBACK_DAYS` | 365 | 조회 일수 (`.env` 기본 1년) |
| `GT_MIN_SWING_PCT` | 4.0 | ZigZag 최소 스윙(%) |
| `GT_PIVOT_ORDER` | 20 | 국소 극값 반경(봉) |
| `GT_MIN_BARS_BETWEEN` | 30 | 체결 간격(3분봉 30봉=90분) |
| `GT_MIN_LEG_PCT` | 8.0 | 한 구간 최소 수익(%) |
| `GT_MAX_ROUND_TRIPS` | 24 | 최대 라운드트립 |
| `GT_SELECTION_MODE` | split_buy_peak_sell | `split_buy_peak_sell` 등 (`.env` 참고) |
## Act
- JSON 수동 수정 후 `scripts/05_chart_truth.py` 재실행으로 차트 갱신
- 파라미터 조정으로 타점 수·크기 튜닝

29
docs/reference/ROADMAP.md Normal file
View File

@@ -0,0 +1,29 @@
# DeepCoin 로드맵 (WLD)
## 완료
| 단계 | 내용 | 실행 |
|------|------|------|
| 01 데이터 | 1년치 3분~일봉 `coins.db` 적재 | `python scripts/01_download.py` |
| 02 Ground Truth | 매수·매도 정답 타점 JSON | `python scripts/02_ground_truth.py` |
| 03 분석 준비 | 8TF 기술적 지표·패턴 enrich | `python scripts/03_analyze_enrich.py` |
## 진행 예정
| 단계 | 내용 | 패키지 | 실행 (예정) |
|------|------|--------|-------------|
| 03b | GT 타점 3분~일봉 기술적 상태 분석 (CLI 준비, 전량 CSV 재실행 필요) | `deepcoin/analysis/` | `python scripts/03_analyze_trades.py` |
| 04 | GT에 가장 근접한 기술적 상태 선택 | `deepcoin/matching/` | `python scripts/04_match_rules.py` |
| 05 | 1분 단위 상태 확인·실거래 | `deepcoin/ops/` | `python scripts/05_run_monitor.py` |
## 디렉터리
구조: [STRUCTURE.md](STRUCTURE.md)
```text
scripts/01~05_*.py 단계별 CLI
data/ coins.db, ground_truth/, ops/
docs/reference/ 가이드·명세
docs/02~05, charts/ 단계별 산출물 (HTML·CSV)
deepcoin/ 단계별 Python 패키지
```

View File

@@ -0,0 +1,52 @@
# DeepCoin 프로젝트 구조
단계별 PM 기준으로 디렉터리·진입점을 정리합니다. **실행은 `scripts/`만 사용**합니다.
**문서·산출물은 `docs/` 한 곳**에서 관리합니다.
- `docs/reference/` — 사람이 읽는 가이드·명세 (Git 추적)
- `docs/02_*` ~ `docs/charts/` — 스크립트가 만든 HTML·CSV (로컬 재생성)
## 단계 ↔ 실행 ↔ 산출물
| 단계 | 목적 | 스크립트 | 패키지 | 산출물 |
|------|------|----------|--------|--------|
| 01 | 1년치 봉 적재 | `scripts/01_download.py` | `deepcoin/data/` | `data/coins.db` |
| 02 | Ground Truth 타점 | `scripts/02_ground_truth.py` | `deepcoin/ground_truth/` | `data/ground_truth/*.json`, `docs/02_ground_truth/` |
| 03 | 8TF 지표 enrich | `scripts/03_analyze_enrich.py` | `deepcoin/analysis/` | `docs/03_analysis/latest/` |
| 03b | GT 타점 MTF 스냅샷 | `scripts/03_analyze_trades.py` | `deepcoin/analysis/` | `docs/03_analysis/general_analysis_trades.csv` |
| 04 | GT 근접 규칙 선택 | `scripts/04_match_rules.py` | `deepcoin/matching/` | `docs/04_matching/` (예정) |
| 05 | 차트·1분 모니터 | `scripts/05_chart_*.py`, `05_run_monitor.py` | `deepcoin/ops/` | `docs/charts/`, `docs/05_ops/` |
## 디렉터리 트리
```text
DeepCoin/
├── .env, config.py
├── scripts/ # 단계별 CLI
├── deepcoin/ # 로직 패키지
├── data/ # DB, GT JSON, ops 상태
└── docs/
├── README.md
├── reference/ # 가이드 (Git)
│ ├── ROADMAP.md
│ ├── STRUCTURE.md
│ ├── GROUND_TRUTH.md
│ └── trade_anaysis.html
├── 02_ground_truth/ # 산출물
├── 03_analysis/
├── 04_matching/
├── 05_ops/
└── charts/
```
## `deepcoin/analysis/` 모듈 역할
| 파일 | 역할 |
|------|------|
| `general_analysis_enrich_runner.py` | 03 enrich |
| `general_analysis_runner.py` | 03b GT 스냅샷 |
| `general_analysis_pipeline.py` | 타점별 MTF 조립 |
| `general_analysis_*` (기타) | 지표·패턴·리포트 |
`reports/` 폴더는 `docs/`로 통합되었습니다. 코드의 `REPORTS_*` 이름은 `paths.py`에서 `docs/*`로 연결되는 별칭입니다.

View File

@@ -0,0 +1,394 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8"/>
<title>MTF 기술적 분석 기법 목록 (trade_anaysis)</title>
<style>
body { font-family: "Malgun Gothic", Arial, sans-serif; margin: 28px 36px; background: #f5f5f5; color: #1e293b; line-height: 1.55; }
h1 { font-size: 1.45rem; border-bottom: 2px solid #64748b; padding-bottom: 8px; }
h2 { font-size: 1.15rem; margin-top: 32px; color: #334155; }
h3 { font-size: 1rem; margin-top: 20px; color: #475569; }
p.meta { color: #64748b; font-size: 0.9rem; }
.box { background: #fff; border: 1px solid #cbd5e1; border-radius: 8px; padding: 16px 20px; margin: 16px 0; }
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; background: #fff; margin: 12px 0 20px; }
th, td { border: 1px solid #e2e8f0; padding: 8px 10px; text-align: left; vertical-align: top; }
th { background: #e2e8f0; font-weight: 600; white-space: nowrap; }
tr:nth-child(even) td { background: #f8fafc; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.78rem; margin-right: 4px; }
.done { background: #dcfce7; color: #166534; }
.partial { background: #fef9c3; color: #854d0e; }
.todo { background: #f1f5f9; color: #475569; }
ul.compact { margin: 6px 0; padding-left: 20px; }
ul.compact li { margin: 4px 0; }
code { background: #f1f5f9; padding: 1px 5px; border-radius: 3px; font-size: 0.85em; }
.matrix td, .matrix th { text-align: center; font-size: 0.82rem; }
.yes { color: #15803d; font-weight: 600; }
</style>
</head>
<body>
<h1>MTF 기술적 분석 기법 목록 및 구현 상태</h1>
<p class="meta">
목적: 3분~일봉 OHLCV 유입 시 <strong>모든 기법을 봉 단위로 검증</strong> (매수·매도 타점과 무관) ·
간격: <strong>3, 5, 10, 15, 30, 60, 240분 + 일봉(1440)</strong> ·
데이터: <code>data/coins.db</code> (WLD) ·
문서: <code>docs/reference/trade_anaysis.html</code>
</p>
<div class="box">
<h3>구현 상태 범례</h3>
<ul class="compact">
<li><span class="tag done">완료</span><code>general_analysis_enrich_bars(df, interval)</code> CSV 컬럼 또는 기존 Plotly 차트.</li>
<li><span class="tag partial">부분</span><strong>Plotly/HTML UI만</strong> 없음. 수치·플래그는 CSV에 존재.</li>
<li><span class="tag todo">미구현</span> — 전용 UI·리포트 페이지 없음 (아래 2건).</li>
<li><span class="tag partial">구현완료·전량재실행</span> — 코드·CLI는 준비됨. 산출 CSV가 GT 전체를 반영하려면 <code>--limit</code> 없이 재실행.</li>
</ul>
</div>
<div class="box">
<h3>실행 및 산출물</h3>
<ul class="compact">
<li><strong>03 enrich (권장)</strong>: <code>python scripts/03_analyze_enrich.py</code>
· 모듈: <code>deepcoin/analysis/general_analysis_enrich_runner.py</code></li>
<li><strong>산출</strong>: <code>docs/03_analysis/latest/m3_latest.csv</code><code>d1_latest.csv</code>
(간격당 약 <strong>247컬럼</strong>, 최근 N봉 — <code>GA_DEFAULT_TAIL_EXPORT</code> 기본 <strong>200</strong>, <code>--tail-export</code>로 변경)</li>
<li><strong>점검</strong>: <code>docs/03_analysis/general_analysis_capability.html</code></li>
<li><strong>03b GT 타점 MTF</strong>: <code>python scripts/03_analyze_trades.py</code>
<code>docs/03_analysis/general_analysis_trades.csv</code>
· 리포트: <code>docs/03_analysis/general_analysis_report.html</code></li>
<li><strong>주의</strong>: <code>--limit N</code>은 테스트용. 전체 GT(약 450타점) 반영 시 <code>--limit</code> 없이 실행.</li>
</ul>
</div>
<div class="box">
<h3>분석 시점 정의</h3>
<ul class="compact">
<li>봉 분석: 각 간격의 <strong>완성봉 시계열</strong>에 지표·패턴 컬럼 부여 (전 봉 + lookback 롤링).</li>
<li>타점 분석(선택): <code>dt</code> 직전 완성봉만 사용 (<code>merge_asof</code> backward).</li>
<li>3분봉 lookback 롤링은 성능상 최근 6000봉 구간만 패턴·VP·파동·하모닉 갱신 (<code>CONTEXT_TAIL_ROWS</code>).</li>
</ul>
</div>
<h2>1. DB 보유 간격</h2>
<table class="matrix">
<thead>
<tr>
<th>간격</th><th>3분</th><th>5분</th><th>10분</th><th>15분</th>
<th>30분</th><th>60분</th><th>240분</th><th>일봉</th>
</tr>
</thead>
<tbody>
<tr>
<th>enrich 대상</th>
<td class="yes">O</td><td class="yes">O</td><td class="yes">O</td><td class="yes">O</td>
<td class="yes">O</td><td class="yes">O</td><td class="yes">O</td><td class="yes">O</td>
</tr>
<tr>
<th>DB 적재</th>
<td class="yes">O</td><td class="yes">O</td><td class="yes">O</td><td class="yes">O</td>
<td class="yes">O</td><td class="yes">O</td><td class="yes">O</td><td class="yes">O</td>
</tr>
<tr>
<th>대략 기간</th>
<td>~12개월</td><td>~12개월</td><td>~12개월</td><td>~12개월</td>
<td>~12개월</td><td>~12개월</td><td>~12개월</td><td>~12개월</td>
</tr>
<tr>
<th>비고</th>
<td colspan="8">1분봉은 DB 6개월만 있어 본 문서 범위 제외.</td>
</tr>
</tbody>
</table>
<h2>2. 파이프라인</h2>
<div class="box">
<ol>
<li><strong>입력</strong>: <code>mtf_bb.load_frames_from_db()</code> — 8개 간격 OHLCV</li>
<li><strong>enrich</strong>: <span class="tag done">완료</span> <code>general_analysis_enrich_bars(raw, interval, full_context=True)</code></li>
<li><strong>모듈 순서</strong>: <code>candle_features</code><code>indicators</code><code>candles</code><code>chart</code><code>context</code>(patterns/wave/volume/harmonic)</li>
<li><strong>MTF 합성</strong>: <span class="tag done">완료</span> <code>general_analysis_mtf_vote_latest()</code>, <code>ga_align_*</code></li>
<li><strong>시각화</strong>: <span class="tag partial">부분</span> <code>scripts/05_chart_*.py</code> 3분 6패널 · 8TF 타일·타점 미니차트 UI <span class="tag todo">미구현</span></li>
</ol>
</div>
<h2>3. 차트 분석 (Chart Analysis)</h2>
<table>
<thead>
<tr><th>방법</th><th>설명</th><th>구현</th><th>주요 컬럼 / 모듈</th></tr>
</thead>
<tbody>
<tr>
<td>캔들차트 (Candlestick)</td>
<td>OHLC + BB·일목 오버레이</td>
<td><span class="tag done">완료</span></td>
<td><code>scripts/05_chart_*.py</code>, <code>ga_chart_type_candle</code></td>
</tr>
<tr>
<td>선차트 (Line)</td>
<td>종가·MA·MACD 등 시계열</td>
<td><span class="tag done">완료</span> <span class="tag partial">UI</span></td>
<td><code>ga_chart_line_slope</code>, <code>ga_chart_line_slope_1</code> · Plotly 전용 선차트 없음</td>
</tr>
<tr>
<td>바차트 (Bar / OHLC Bar)</td>
<td>봉 범위·거래량 스파이크</td>
<td><span class="tag done">완료</span> <span class="tag partial">UI</span></td>
<td><code>ga_chart_bar_range_pct</code>, <code>ga_chart_vol_spike</code></td>
</tr>
<tr>
<td>Heikin-Ashi</td>
<td>노이즈 완화 캔들</td>
<td><span class="tag done">완료</span></td>
<td><code>ga_ha_*</code>, <code>ga_chart_ha_trend</code></td>
</tr>
<tr>
<td>Renko</td>
<td>ATR 브릭 방향</td>
<td><span class="tag done">완료</span></td>
<td><code>ga_chart_renko_dir</code>, <code>ga_chart_renko_up</code>, <code>ga_chart_renko_brick_up_ratio</code></td>
</tr>
<tr>
<td>Point &amp; Figure</td>
<td>박스 크기 기준 X/O 열</td>
<td><span class="tag done">완료</span></td>
<td><code>ga_chart_pnf_col</code></td>
</tr>
<tr>
<td>멀티 패널 (6패널)</td>
<td>BB·이격·Stoch·RSI·MACD</td>
<td><span class="tag done">완료</span></td>
<td><code>scripts/05_chart_*.py</code> 3분</td>
</tr>
<tr>
<td>MTF 타일 (Small Multiples)</td>
<td>8TF 나란히 Plotly</td>
<td><span class="tag todo">미구현</span></td>
<td>CSV 8TF 컬럼으로 대체 · <code>docs/03_analysis/latest/*_latest.csv</code></td>
</tr>
</tbody>
</table>
<h2>4. 패턴 분석 (Pattern Analysis)</h2>
<p>lookback 윈도우(<code>LOOKBACK_BARS</code>) 마지막 봉 기준. 롤링 적용: <code>general_analysis_apply_context_features</code>.</p>
<h3>4.1 반전 패턴</h3>
<table>
<thead>
<tr><th>패턴</th><th>구현</th><th>컬럼</th><th>권장 TF</th></tr>
</thead>
<tbody>
<tr><td>헤드앤숄더 / 역H&amp;S</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_head_shoulders</code>, <code>ga_pattern_inv_head_shoulders</code></td><td>60분~일봉</td></tr>
<tr><td>쌍봉 / 쌍바닥</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_double_top</code>, <code>ga_pattern_double_bottom</code></td><td>30분~일봉</td></tr>
<tr><td>트리플 탑/바닼</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_triple_top</code>, <code>ga_pattern_triple_bottom</code></td><td>60분~일봉</td></tr>
<tr><td>V자 반등 / 스파이크</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_v_bottom</code>, <code>ga_pattern_spike_top</code></td><td>5~60분</td></tr>
<tr><td>둥근 천장/바닼</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_rounding_top</code>, <code>ga_pattern_rounding_bottom</code></td><td>일봉</td></tr>
<tr><td>플래티어 (Rectangle)</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_rectangle</code></td><td>15분~240분</td></tr>
<tr><td>갭 / 아일랜드</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_gap_up/down</code>, <code>ga_pattern_island_top/bottom</code></td><td>60분~일봉</td></tr>
<tr><td>키리스톤 / 역키리스톤</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_keystone_bull</code>, <code>ga_pattern_keystone_bear</code></td><td>30분~일봉</td></tr>
</tbody>
</table>
<h3>4.2 지속 패턴</h3>
<table>
<thead>
<tr><th>패턴</th><th>구현</th><th>컬럼</th><th>권장 TF</th></tr>
</thead>
<tbody>
<tr><td>삼각수렴 (대칭/상승/하락)</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_triangle_sym/asc/desc</code></td><td>15분~240분</td></tr>
<tr><td>깃발 / 페넌트</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_flag_bull</code>, <code>ga_pattern_flag_bear</code></td><td>5~60분</td></tr>
<tr><td>웨지</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_wedge_rising</code>, <code>ga_pattern_wedge_falling</code></td><td>15분~60분</td></tr>
<tr><td>채널</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_channel_up</code>, <code>ga_pattern_channel_down</code></td><td>전 TF</td></tr>
<tr><td>박스권 + BB 스퀴즈</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_rectangle</code>, <code>ga_bb_squeeze</code></td><td>5~60분</td></tr>
<tr><td>컵앤핸들</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_cup_handle</code></td><td>일봉</td></tr>
<tr><td>측정된 움직임</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_measured_move</code></td><td>30분~일봉</td></tr>
<tr><td>패턴 요약 라벨</td><td><span class="tag done">완료</span></td><td><code>ga_pattern_label</code></td><td>전 TF</td></tr>
</tbody>
</table>
<h3>4.3 캔들 패턴</h3>
<table>
<thead>
<tr><th>패턴</th><th>구현</th><th>컬럼</th><th>권장 TF</th></tr>
</thead>
<tbody>
<tr><td>해머 / 유성 / 도지</td><td><span class="tag done">완료</span></td><td><code>ga_hammer</code>, <code>ga_shooting_star</code>, <code>ga_doji</code> + <code>candle_features</code></td><td>3~60분</td></tr>
<tr><td>장악형</td><td><span class="tag done">완료</span></td><td><code>ga_bullish_engulfing</code>, <code>ga_bearish_engulfing</code></td><td>5~60분</td></tr>
<tr><td>샛별형</td><td><span class="tag done">완료</span></td><td><code>ga_morning_star</code>, <code>ga_evening_star</code></td><td>15분~일봉</td></tr>
<tr><td>삼병 / 삼까마귀</td><td><span class="tag done">완료</span></td><td><code>ga_three_white_soldiers</code>, <code>ga_three_black_crows</code></td><td>15분~60분</td></tr>
<tr><td>피보나치 되돌림 근접</td><td><span class="tag done">완료</span></td><td><code>ga_fib_near_level</code></td><td>30분~일봉</td></tr>
</tbody>
</table>
<h2>5. 기술적 지표 (Technical Indicators)</h2>
<p>전 봉 시계열 컬럼. 레거시: <code>RSI</code>, <code>bb_pos</code>, <code>macd_*</code>, <code>stoch_*</code> 등.</p>
<h3>5.1 추세</h3>
<table>
<thead>
<tr><th>지표</th><th>구현</th><th>컬럼</th></tr>
</thead>
<tbody>
<tr><td>SMA / EMA / 골든·데드크로스</td><td><span class="tag done">완료</span></td><td><code>ga_sma_*</code>, <code>ga_golden_cross</code>, <code>ga_death_cross</code></td></tr>
<tr><td>MACD</td><td><span class="tag done">완료</span></td><td><code>macd_line</code>, <code>macd_signal</code>, <code>macd_hist</code></td></tr>
<tr><td>이격도</td><td><span class="tag done">완료</span></td><td><code>indicators</code> DI 5/20/60</td></tr>
<tr><td>ADX (+DI/-DI)</td><td><span class="tag done">완료</span></td><td><code>ga_adx_14</code>, <code>ga_plus_di</code>, <code>ga_minus_di</code></td></tr>
<tr><td>Parabolic SAR</td><td><span class="tag done">완료</span></td><td><code>ga_psar</code>, <code>ga_psar_bull</code>, <code>ga_psar_flip_bull/bear</code></td></tr>
<tr><td>Ichimoku</td><td><span class="tag done">완료</span></td><td><code>indicators</code> + <code>ga_ichi_trend</code></td></tr>
<tr><td>Linear Regression</td><td><span class="tag done">완료</span></td><td><code>ga_linreg_slope_20</code>, <code>ga_linreg_r2_20</code></td></tr>
<tr><td>VWAP</td><td><span class="tag done">완료</span></td><td><code>ga_vwap</code>, <code>ga_close_vs_vwap_pct</code> (누적 VWAP)</td></tr>
<tr><td>Supertrend</td><td><span class="tag done">완료</span></td><td><code>ga_supertrend_bull</code></td></tr>
</tbody>
</table>
<h3>5.2 모멘텀</h3>
<table>
<thead>
<tr><th>지표</th><th>구현</th><th>컬럼</th></tr>
</thead>
<tbody>
<tr><td>RSI</td><td><span class="tag done">완료</span></td><td><code>RSI</code>, <code>ga_rsi_delta_1</code></td></tr>
<tr><td>스토캐스틱</td><td><span class="tag done">완료</span></td><td><code>stoch_k</code>, <code>stoch_d</code>, <code>ga_stoch_k_delta_1</code></td></tr>
<tr><td>CCI</td><td><span class="tag done">완료</span></td><td><code>ga_cci_20</code>, <code>ga_cci_oversold/overbought</code></td></tr>
<tr><td>Williams %R</td><td><span class="tag done">완료</span></td><td><code>ga_williams_r</code>, <code>ga_williams_oversold/overbought</code></td></tr>
<tr><td>ROC</td><td><span class="tag done">완료</span></td><td><code>ga_roc_10</code></td></tr>
<tr><td>MFI</td><td><span class="tag done">완료</span></td><td><code>ga_mfi_14</code></td></tr>
<tr><td>Awesome Oscillator</td><td><span class="tag done">완료</span></td><td><code>ga_ao</code>, <code>ga_ao_bull</code>, <code>ga_ao_bear</code></td></tr>
<tr><td>RSI / MACD / Stoch 다이버전스</td><td><span class="tag done">완료</span></td><td><code>ga_rsi_*_div</code>, <code>ga_macd_*_div</code>, <code>ga_stoch_*_div</code></td></tr>
</tbody>
</table>
<h3>5.3 변동성</h3>
<table>
<thead>
<tr><th>지표</th><th>구현</th><th>컬럼</th></tr>
</thead>
<tbody>
<tr><td>볼린저 밴드</td><td><span class="tag done">완료</span></td><td><code>bb_pos</code>, <code>ga_bb_width_pct</code>, zone (<code>candle_features</code>)</td></tr>
<tr><td>ATR</td><td><span class="tag done">완료</span></td><td><code>ga_atr_14</code>, <code>ga_atr_pct</code></td></tr>
<tr><td>Keltner Channel</td><td><span class="tag done">완료</span></td><td><code>ga_keltner_mid/upper/lower</code>, <code>ga_keltner_pos</code></td></tr>
<tr><td>Donchian Channel</td><td><span class="tag done">완료</span></td><td><code>ga_donchian_pos</code></td></tr>
<tr><td>Historical Volatility</td><td><span class="tag done">완료</span></td><td><code>ga_hv_20</code>, <code>ga_hv_percentile</code></td></tr>
<tr><td>BB Squeeze</td><td><span class="tag done">완료</span></td><td><code>ga_bb_squeeze</code></td></tr>
</tbody>
</table>
<h3>5.4 거래량</h3>
<table>
<thead>
<tr><th>지표</th><th>구현</th><th>컬럼</th></tr>
</thead>
<tbody>
<tr><td>OBV + 다이버전스</td><td><span class="tag done">완료</span></td><td><code>ga_obv</code>, <code>ga_obv_slope_10</code>, <code>ga_obv_*_div</code></td></tr>
<tr><td>Volume MA ratio</td><td><span class="tag done">완료</span></td><td><code>ga_vol_ratio</code>, <code>ga_vol_ma20</code></td></tr>
<tr><td>VWAP deviation</td><td><span class="tag done">완료</span></td><td><code>ga_close_vs_vwap_pct</code></td></tr>
<tr><td>Accumulation/Distribution</td><td><span class="tag done">완료</span></td><td><code>ga_ad_line</code>, <code>ga_ad_slope_10</code></td></tr>
<tr><td>Chaikin Money Flow</td><td><span class="tag done">완료</span></td><td><code>ga_cmf_20</code></td></tr>
<tr><td>Volume Profile</td><td><span class="tag done">완료</span></td><td><code>ga_vp_poc</code>, <code>ga_vp_vah</code>, <code>ga_vp_val</code>, <code>ga_vp_in_value_area</code></td></tr>
</tbody>
</table>
<h2>6. 파동·시장 구조</h2>
<table>
<thead>
<tr><th>이론</th><th>구현</th><th>컬럼</th><th>비고</th></tr>
</thead>
<tbody>
<tr><td>다우 이론 (HH/HL/LH/LL)</td><td><span class="tag done">완료</span></td><td><code>ga_struct_*</code></td><td>피벗 기반</td></tr>
<tr><td>시장 구조 (BOS/CHoCH)</td><td><span class="tag done">완료</span></td><td><code>ga_struct_bos_*</code>, <code>ga_struct_choch</code></td><td></td></tr>
<tr><td>엘리어트 파동</td><td><span class="tag done">완료</span></td><td><code>ga_elliott_wave_count</code>, <code>ga_elliott_phase</code></td><td>라이트(스윙 수·단계)</td></tr>
<tr><td>Wyckoff</td><td><span class="tag done">완료</span></td><td><code>ga_wyckoff_phase</code>, <code>ga_wyckoff_spring</code>, <code>ga_wyckoff_utad</code></td><td>accumulation/distribution + spring/UTAD</td></tr>
<tr><td>일목 (구름)</td><td><span class="tag done">완료</span></td><td><code>ga_ichi_trend</code></td><td></td></tr>
<tr><td>피보나치</td><td><span class="tag done">완료</span></td><td><code>ga_fib_near_level</code></td><td>0/382/500/618/100/1618</td></tr>
<tr><td>하모닉 (Gartley/Bat)</td><td><span class="tag done">완료</span></td><td><code>ga_harmonic_gartley</code>, <code>ga_harmonic_bat</code>, <code>ga_harmonic_label</code></td><td>5피벗 비율</td></tr>
<tr><td>앤더류 피치포크</td><td><span class="tag done">완료</span></td><td><code>ga_pitchfork_bias</code>, <code>ga_pitchfork_dist_pct</code></td><td>3피벗 중앙선</td></tr>
</tbody>
</table>
<h2>7. MTF 합성</h2>
<table>
<thead>
<tr><th>방법</th><th>구현</th><th>컬럼 / 함수</th></tr>
</thead>
<tbody>
<tr><td>TF 가중 투표</td><td><span class="tag done">완료</span></td><td><code>general_analysis_mtf_vote_latest()</code><code>ga_vote_timing_buy/sell</code>, <code>ga_vote_trend_score</code></td></tr>
<tr><td>정렬 점수 (RSI)</td><td><span class="tag done">완료</span></td><td><code>ga_align_timing_buy_score</code>, <code>ga_align_timing_sell_score</code></td></tr>
<tr><td>상위 TF 추세 필터</td><td><span class="tag done">완료</span></td><td><code>ga_align_trend_score</code>, TF별 <code>ga_struct_trend</code></td></tr>
<tr><td>MTF 충돌 태그</td><td><span class="tag done">완료</span></td><td><code>ga_align_mtf_conflict</code></td></tr>
<tr><td>봉 간 Δ (T vs T-1)</td><td><span class="tag done">완료</span></td><td><code>ga_rsi_delta_1</code>, <code>ga_macd_hist_delta_1</code>, <code>ga_stoch_k_delta_1</code></td></tr>
</tbody>
</table>
<h2>8. 구현 단계 (현황)</h2>
<table>
<thead>
<tr><th>단계</th><th>내용</th><th>산출물</th><th>구현</th></tr>
</thead>
<tbody>
<tr><td>P0</td><td>8TF 봉 enrich + latest CSV</td><td><code>docs/03_analysis/latest/*_latest.csv</code></td><td><span class="tag done">완료</span></td></tr>
<tr><td>P1</td><td>기법 점검 HTML</td><td><code>docs/03_analysis/general_analysis_capability.html</code></td><td><span class="tag done">완료</span></td></tr>
<tr><td>P2</td><td>전 지표·거래량·변동성</td><td><code>general_analysis_indicators.py</code></td><td><span class="tag done">완료</span></td></tr>
<tr><td>P3</td><td>전 패턴·캔들</td><td><code>general_analysis_patterns.py</code>, <code>candles.py</code></td><td><span class="tag done">완료</span></td></tr>
<tr><td>P4</td><td>파동·VP·하모닉·MTF</td><td><code>wave</code>, <code>volume</code>, <code>harmonic</code>, <code>align</code></td><td><span class="tag done">완료</span></td></tr>
<tr><td>P5</td><td>GT 타점 wide CSV (03b)</td><td><code>docs/03_analysis/general_analysis_trades.csv</code></td><td><span class="tag partial">구현완료·전량재실행</span></td></tr>
<tr><td>P6</td><td>8TF Plotly 타일 · 타점 미니차트</td><td><code>trade_detail.html</code></td><td><span class="tag todo">미구현</span></td></tr>
<tr><td></td><td>04 규칙 매칭 · 05 1분 운영</td><td><code>scripts/04_match_rules.py</code>, <code>05_run_monitor.py</code></td><td><span class="tag todo">로드맵 예정</span></td></tr>
</tbody>
</table>
<h2>9. 코드베이스 매핑</h2>
<table>
<thead>
<tr><th>모듈</th><th>역할</th></tr>
</thead>
<tbody>
<tr><td><code>general_analysis_enrich_runner.py</code></td><td>8TF enrich 로직 · CLI: <code>scripts/03_analyze_enrich.py</code></td></tr>
<tr><td><code>general_analysis_pipeline.py</code></td><td><code>enrich_bars</code>, <code>snapshot_at_bar</code></td></tr>
<tr><td><code>general_analysis_indicators.py</code></td><td>추세·모멘텀·변동성·거래량·SAR·Keltner·AO·HV·Δ</td></tr>
<tr><td><code>general_analysis_candles.py</code></td><td>Heikin-Ashi, 단일·복수 봉 패턴</td></tr>
<tr><td><code>general_analysis_chart.py</code></td><td>Renko, P&amp;F, 선·바 파생</td></tr>
<tr><td><code>general_analysis_patterns.py</code></td><td>반전·지속 패턴 + 롤링 적용</td></tr>
<tr><td><code>general_analysis_wave.py</code></td><td>구조·엘리어트·Wyckoff·피보나치·피치포크</td></tr>
<tr><td><code>general_analysis_volume.py</code></td><td>Volume Profile POC/VAH/VAL</td></tr>
<tr><td><code>general_analysis_harmonic.py</code></td><td>Gartley, Bat</td></tr>
<tr><td><code>general_analysis_context.py</code></td><td>lookback 롤링 일괄 (patterns/wave/vp/harmonic)</td></tr>
<tr><td><code>general_analysis_align.py</code></td><td><code>ga_align_*</code>, <code>ga_vote_*</code></td></tr>
<tr><td><code>general_analysis_runner.py</code></td><td>GT 타점 wide CSV · CLI: <code>scripts/03_analyze_trades.py</code></td></tr>
<tr><td><code>indicators.py</code> / <code>candle_features.py</code></td><td>BB, 일목, RSI, MACD, Stoch, 이격도, zone</td></tr>
<tr><td><code>scripts/05_chart_*.py</code></td><td>3분 6패널 · ground truth 차트</td></tr>
</tbody>
</table>
<h2>10. 구현 집계</h2>
<table>
<thead>
<tr><th>구분</th><th>완료</th><th>부분 (UI만)</th><th>미구현</th></tr>
</thead>
<tbody>
<tr><td>차트 분석 (§3)</td><td>7</td><td>2 (선·바 Plotly)</td><td>1 (MTF 타일)</td></tr>
<tr><td>반전 패턴 (§4.1)</td><td>8</td><td>0</td><td>0</td></tr>
<tr><td>지속 패턴 (§4.2)</td><td>8</td><td>0</td><td>0</td></tr>
<tr><td>캔들 패턴 (§4.3)</td><td>5</td><td>0</td><td>0</td></tr>
<tr><td>추세 지표 (§5.1)</td><td>9</td><td>0</td><td>0</td></tr>
<tr><td>모멘텀 (§5.2)</td><td>8</td><td>0</td><td>0</td></tr>
<tr><td>변동성 (§5.3)</td><td>6</td><td>0</td><td>0</td></tr>
<tr><td>거래량 (§5.4)</td><td>6</td><td>0</td><td>0</td></tr>
<tr><td>파동·구조 (§6)</td><td>8</td><td>0</td><td>0</td></tr>
<tr><td>MTF 합성 (§7)</td><td>5</td><td>0</td><td>0</td></tr>
<tr><td><strong>합계</strong></td><td><strong>70</strong></td><td><strong>2</strong></td><td><strong>1</strong></td></tr>
</tbody>
</table>
<p class="meta">「부분」= CSV 수치는 있으나 전용 Plotly UI 없음. P5 「구현완료·전량재실행」= 코드·CLI 준비됨, GT 450건 전체 wide CSV는 <code>03_analyze_trades.py</code><code>--limit</code> 없이 실행해 갱신.</p>
<p class="meta" style="margin-top: 40px;">
문서 버전: 2026-05-30 (프로젝트 구조·CLI 동기화) · DeepCoin / WLD ·
<code>docs/reference/trade_anaysis.html</code> ·
DB: <code>data/coins.db</code> · GT: <code>data/ground_truth/ground_truth_trades.json</code> ·
enrich: <code>python scripts/03_analyze_enrich.py</code> ·
타점: <code>python scripts/03_analyze_trades.py</code> ·
약 247컬럼/TF · tail 기본 200봉 ·
UI 미구현: MTF 타일 Plotly, <code>trade_detail.html</code> ·
다음: 04 매칭, 05 운영 — <code>docs/reference/ROADMAP.md</code>
</p>
</body>
</html>

View File

@@ -1,58 +0,0 @@
"""
볼린저 밴드·일목균형표 계산 (모든 봉 간격 공용).
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from config import BB_PERIOD, BB_STD
def add_bollinger(
df: pd.DataFrame,
period: int = BB_PERIOD,
std_mult: float = BB_STD,
) -> pd.DataFrame:
"""MA, Upper, Lower, BB_Width, bb_pos 컬럼 추가."""
out = df.copy()
if "MA" not in out.columns:
out["MA"] = out["Close"].rolling(period).mean()
if "Upper" not in out.columns or "Lower" not in out.columns:
std = out["Close"].rolling(period).std()
out["STD"] = std
out["Upper"] = out["MA"] + std_mult * std
out["Lower"] = out["MA"] - std_mult * std
ma = out["MA"].replace(0, np.nan)
band = (out["Upper"] - out["Lower"]).replace(0, np.nan)
out["bb_pos"] = ((out["Close"] - out["Lower"]) / band).clip(0, 1)
out["BB_Width"] = band / ma * 100
return out
def add_ichimoku(
df: pd.DataFrame,
tenkan: int = 9,
kijun: int = 26,
senkou_b_period: int = 52,
) -> pd.DataFrame:
"""
일목균형표 라인·구름 위치 컬럼 추가 (해당 봉 시점, 미래 데이터 미사용).
Returns:
ichi_tenkan, ichi_kijun, ichi_span_a, ichi_span_b,
ichi_cloud_top, ichi_cloud_bottom
"""
out = df.copy()
h = out["High"].astype(float)
l = out["Low"].astype(float)
c = out["Close"].astype(float)
out["ichi_tenkan"] = (h.rolling(tenkan).max() + l.rolling(tenkan).min()) / 2
out["ichi_kijun"] = (h.rolling(kijun).max() + l.rolling(kijun).min()) / 2
out["ichi_span_a"] = (out["ichi_tenkan"] + out["ichi_kijun"]) / 2
out["ichi_span_b"] = (h.rolling(senkou_b_period).max() + l.rolling(senkou_b_period).min()) / 2
out["ichi_cloud_top"] = np.maximum(out["ichi_span_a"], out["ichi_span_b"])
out["ichi_cloud_bottom"] = np.minimum(out["ichi_span_a"], out["ichi_span_b"])
return out

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env python3
"""
로고스 전략 백테스트·HTML (simulation.py 와 동일).
python logos_chart.py
"""
from __future__ import annotations
from simulation import run_full_pipeline
def main() -> None:
run_full_pipeline()
if __name__ == "__main__":
main()

View File

@@ -1,349 +0,0 @@
"""
로고스(Logos) 매매 타점 전략 — 수동 타점(logos_trades.json) 흐름에 맞춘 단일 로직.
- 바닥 1회 매수 → 장기 보유 → 고점 익절 → 눌림 재매수 (8타점 수준, 과다 체결 방지)
"""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.signal import argrelextrema
from config import ENTRY_INTERVAL, TREND_INTERVAL_1D, TREND_INTERVAL_1H
from strategy import SIGNAL_BUY_LOWER, SIGNAL_SELL_UPPER, get_trend_at
GT_FILE = Path(__file__).parent / "logos_trades.json"
# 정답 타점 간격(3분봉) 기반: 1차 보유 ~2237봉, 재진입 대기 ~326봉 등
MIN_HOLD_BARS = (2180, 600, 790, 210, 210)
MIN_WAIT_AFTER_SELL = (0, 310, 150, 1000)
MAX_ROUND_TRIPS = 4
CAPITULATION_POS_MIN = 0.08
CAPITULATION_POS_MAX = 0.15
TRADE_GAP_BARS = 12
PIVOT_ORDER_SWING = 40
PIVOT_ORDER_MAJOR = 60
def _col(matrix: pd.DataFrame, name: str) -> pd.Series:
pfx = f"m{ENTRY_INTERVAL}_"
key = f"{pfx}{name}" if not name.startswith("m") else name
if key in matrix.columns:
return matrix[key].fillna(0)
return pd.Series(0, index=matrix.index)
def _bb_pos(matrix: pd.DataFrame) -> np.ndarray:
return _col(matrix, "bb_pos").astype(float).to_numpy()
def _bool_col(matrix: pd.DataFrame, name: str) -> np.ndarray:
return _col(matrix, name).astype(bool).to_numpy()
def _pivot_low_mask(low: np.ndarray, order: int = 40) -> np.ndarray:
idx = argrelextrema(low, np.less_equal, order=order)[0]
mask = np.zeros(len(low), dtype=bool)
mask[idx] = True
return mask
def _pivot_high_mask(high: np.ndarray, order: int = 40) -> np.ndarray:
idx = argrelextrema(high, np.greater_equal, order=order)[0]
mask = np.zeros(len(high), dtype=bool)
mask[idx] = True
return mask
def _trend_mask(matrix: pd.DataFrame, df_1d: pd.DataFrame, df_1h: pd.DataFrame) -> np.ndarray:
return np.array(
[get_trend_at(df_1d, df_1h, ts) for ts in matrix.index],
dtype=object,
)
def _prepare_arrays(
matrix: pd.DataFrame,
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
) -> dict[str, np.ndarray]:
"""3분봉 피처 배열 (정답 타점 분석 기준)."""
close = matrix["Close"].astype(float).to_numpy()
low = matrix["Low"].astype(float).to_numpy()
high = matrix["High"].astype(float).to_numpy()
pos = _bb_pos(matrix)
roll_lo = matrix["Close"].astype(float).rolling(120, min_periods=20).min().to_numpy()
roll_hi = matrix["Close"].astype(float).rolling(80, min_periods=20).max().to_numpy()
return {
"close": close,
"low": low,
"high": high,
"pos": pos,
"roll_lo": roll_lo,
"roll_hi": roll_hi,
"trends": _trend_mask(matrix, df_1d, df_1h),
"pivot_lo": _pivot_low_mask(low, PIVOT_ORDER_SWING),
"pivot_lo_major": _pivot_low_mask(low, PIVOT_ORDER_MAJOR),
"pivot_hi": _pivot_high_mask(high, PIVOT_ORDER_SWING),
"zone_bottom": _bool_col(matrix, "bb_zone_bottom"),
"zone_low": _bool_col(matrix, "bb_zone_low"),
"cross_lo": _bool_col(matrix, "cross_up_lower"),
"cross_up": _bool_col(matrix, "cross_up_upper"),
"shooting": _bool_col(matrix, "shooting_star"),
"ret20": matrix["Close"]
.astype(float)
.pct_change()
.rolling(20, min_periods=5)
.sum()
.to_numpy()
* 100.0,
"vol_spike": (
matrix["Volume"].astype(float)
> matrix["Volume"].astype(float).rolling(20, min_periods=5).mean() * 1.5
)
.fillna(False)
.to_numpy(),
}
def _near_roll_low(arr: dict[str, np.ndarray], i: int) -> bool:
rl = arr["roll_lo"][i]
return rl > 0 and arr["close"][i] <= rl * 1.015
def _at_roll_high(arr: dict[str, np.ndarray], i: int) -> bool:
rh = arr["roll_hi"][i]
return rh > 0 and arr["close"][i] >= rh * 0.985
def _buy_structure(arr: dict[str, np.ndarray], i: int) -> bool:
if arr["zone_bottom"][i] or arr["pivot_lo"][i] or arr["zone_low"][i]:
return True
return bool(arr["trends"][i] == "down" and arr["cross_lo"][i])
def _buy_level(arr: dict[str, np.ndarray], i: int, round_trip: int, last_sell_i: int) -> bool:
if arr["pos"][i] >= 0.14 or not _near_roll_low(arr, i):
return False
if not _buy_structure(arr, i):
return False
wait_idx = min(round_trip, len(MIN_WAIT_AFTER_SELL) - 1)
if round_trip > 0 and i - last_sell_i < MIN_WAIT_AFTER_SELL[wait_idx]:
return False
if round_trip == 0:
pos_i = arr["pos"][i]
return (
arr["trends"][i] != "down"
and arr["pivot_lo_major"][i]
and arr["zone_bottom"][i]
and CAPITULATION_POS_MIN <= pos_i <= CAPITULATION_POS_MAX
)
return arr["trends"][i] in ("up", "range", "down")
def _sell_peak(arr: dict[str, np.ndarray], i: int, buy_round: int = 1) -> bool:
"""매도 신호. buy_round=0 은 1차 파동 고점(과열) 전용."""
at_top = arr["pos"][i] >= 0.88 or _at_roll_high(arr, i)
if not at_top:
return False
blow = (arr["pos"][i] >= 0.95 and arr["close"][i] >= 480) or (
arr["ret20"][i] >= 5.0 and arr["vol_spike"][i] and arr["pos"][i] >= 0.85
)
peak = arr["pivot_hi"][i] or arr["cross_up"][i] or (
arr["shooting"][i] and arr["pos"][i] >= 0.85
)
if buy_round == 0:
return bool(
arr["pos"][i] >= 0.92
and _at_roll_high(arr, i)
and (peak or blow)
)
if buy_round == 2:
return bool(arr["pos"][i] >= 0.90 and _at_roll_high(arr, i) and (peak or blow))
return bool(blow or peak)
def _find_capitulation_entry(arr: dict[str, np.ndarray], n: int) -> int | None:
"""투매 종료 후 첫 바닥 매수(시간상 최초 후보)."""
for i in range(n):
if _buy_level(arr, i, 0, -10_000):
return i
return None
def generate_logos_events(
matrix: pd.DataFrame,
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
) -> list[tuple[pd.Timestamp, str, str]]:
"""
로고스 체결 이벤트 — 포지션 1개·사이클별 최소 보유/대기 (과다 체결 방지).
Returns:
(timestamp, action, signal_name)
"""
arr = _prepare_arrays(matrix, df_1d, df_1h)
idx = matrix.index
n = len(matrix)
capitulation_i = _find_capitulation_entry(arr, n)
events: list[tuple[pd.Timestamp, str, str]] = []
qty = 0.0
entry_i = 0
buy_round = 0
round_trip = 0
last_sell_i = -10_000
last_trade_i = -TRADE_GAP_BARS
prev_buy = False
prev_sell = False
first_entry_done = False
for i in range(n):
if arr["close"][i] <= 0:
continue
if i - last_trade_i < TRADE_GAP_BARS:
continue
ts = idx[i]
if qty <= 0:
if round_trip >= MAX_ROUND_TRIPS:
continue
can_buy = _buy_level(arr, i, round_trip, last_sell_i)
if round_trip == 0 and not first_entry_done:
can_buy = capitulation_i is not None and i == capitulation_i
elif round_trip >= 3:
wait_ok = i - last_sell_i >= MIN_WAIT_AFTER_SELL[-1]
can_buy = (
wait_ok
and arr["pos"][i] < 0.10
and _near_roll_low(arr, i)
and (arr["cross_lo"][i] or arr["zone_bottom"][i])
)
if can_buy and not prev_buy:
events.append((ts, "buy", SIGNAL_BUY_LOWER))
qty = 1.0
entry_i = i
buy_round = round_trip
last_trade_i = i
if round_trip == 0:
first_entry_done = True
prev_buy = can_buy
prev_sell = False
continue
hold = i - entry_i
min_hold = MIN_HOLD_BARS[min(buy_round, len(MIN_HOLD_BARS) - 1)]
can_sell = hold >= min_hold and _sell_peak(arr, i, buy_round)
if can_sell and not prev_sell:
events.append((ts, "sell", SIGNAL_SELL_UPPER))
qty = 0.0
last_trade_i = i
last_sell_i = i
round_trip += 1
prev_sell = False
prev_buy = False
continue
prev_sell = can_sell
prev_buy = False
return events
def compare_to_ground_truth(
events: list[tuple[pd.Timestamp, str, str]],
matrix: pd.DataFrame,
bar_tol: int = 20,
price_tol_pct: float = 6.0,
) -> list[dict]:
"""logos_trades.json 정답과 자동 체결 비교."""
if not GT_FILE.exists():
return []
spec = json.loads(GT_FILE.read_text(encoding="utf-8"))
close = matrix["Close"].astype(float)
cand = []
for ts, action, _ in events:
px = float(close.loc[ts]) if ts in close.index else float(
close.iloc[matrix.index.get_indexer([ts], method="nearest")[0]]
)
cand.append((ts, action, px))
used: set[int] = set()
rows: list[dict] = []
for row in spec.get("trades") or []:
gdt = pd.Timestamp(row["dt"])
gact = row["action"]
gpx = float(row["price"])
best_j = -1
best_score = 0.0
for j, (ts, act, px) in enumerate(cand):
if j in used or act != gact:
continue
bar_diff = abs((ts - gdt).total_seconds()) / 180.0
if bar_diff > bar_tol:
continue
price_pct = abs(px - gpx) / max(gpx, 1e-9) * 100.0
if price_pct > price_tol_pct:
continue
score = (1.0 - bar_diff / bar_tol + 1.0 - price_pct / price_tol_pct) / 2.0
if score > best_score:
best_score = score
best_j = j
if best_j >= 0:
used.add(best_j)
ts, _, px = cand[best_j]
rows.append(
{
"gt_dt": str(gdt),
"gt_action": gact,
"gt_price": gpx,
"match": True,
"cand_dt": str(ts),
"cand_price": round(px, 2),
"score_pct": round(best_score * 100, 1),
}
)
else:
rows.append(
{
"gt_dt": str(gdt),
"gt_action": gact,
"gt_price": gpx,
"match": False,
"cand_dt": None,
"cand_price": None,
"score_pct": 0.0,
}
)
return rows
def backtest_logos(
matrix: pd.DataFrame,
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
entry_ohlc: pd.DataFrame,
) -> tuple[float, int]:
"""로고스 전략 수익률·거래 수."""
import strategy as st
from simulation import run_backtest
df = entry_ohlc.loc[matrix.index].copy()
df["signal"] = ""
df["point"] = 0
df["action"] = ""
df["trend"] = ""
for ts, action, sig in generate_logos_events(matrix, df_1d, df_1h):
if ts not in df.index:
continue
df.at[ts, "signal"] = sig
df.at[ts, "point"] = 1
df.at[ts, "action"] = action
df.at[ts, "trend"] = st.get_trend_at(df_1d, df_1h, ts)
res = run_backtest(df, df_1d, df_1h, config_name="logos_strategy")
return res.total_return_pct, res.trade_count

View File

@@ -1,57 +0,0 @@
{
"name": "logos_discretionary",
"author": "Logos (직관·차트 해석)",
"symbol": "WLD",
"interval_min": 3,
"note": "BB/탐색 규칙 미사용. 3분봉 가격·추세·거래량 구조를 보고 선별한 대표 타점입니다.",
"trades": [
{
"dt": "2026-05-18 08:39:00",
"action": "buy",
"price": 340,
"memo": "구간 최저·투매 종료. 이후 고점 갱신 전환"
},
{
"dt": "2026-05-23 00:30:00",
"action": "sell",
"price": 445,
"memo": "1차 파동 단기 고점(445). 378 눌림 견디고 새벽 과열 구간 익절"
},
{
"dt": "2026-05-23 16:48:00",
"action": "buy",
"price": 392,
"memo": "상승 추세 눌림·저점 상승 확인 후 재진입"
},
{
"dt": "2026-05-24 22:45:00",
"action": "sell",
"price": 464,
"memo": "단기 과열·윗꼬리 구간, 1차 분할 익절"
},
{
"dt": "2026-05-25 06:42:00",
"action": "buy",
"price": 427,
"memo": "26일 급등 전 마지막 눌림. 450대 재매수는 추격이라 보류"
},
{
"dt": "2026-05-26 21:30:00",
"action": "sell",
"price": 603,
"memo": "거래량·각도 과열 끝자락. 전량 익절(01:48 저가 매도 회피)"
},
{
"dt": "2026-05-28 23:39:00",
"action": "buy",
"price": 420,
"memo": "급락 직후 첫 바닥(당일 3분 저점). 소량·단기"
},
{
"dt": "2026-05-29 10:09:00",
"action": "sell",
"price": 441,
"memo": "반등 첫 저항·전일 하락 중충 돌파 실패 구간"
}
]
}

View File

@@ -1,39 +0,0 @@
"""
WLD(월드코인) 실시간 모니터 — 전 봉 BB·일목 조합 (discovered_rules).
전략: strategy.py / rule_discovery.py
"""
from datetime import datetime
import time
from config import COIN_NAME, COOLDOWN_FILE, MONITOR_LOOP_SLEEP_SEC, SYMBOL
from monitor import Monitor
class MonitorCoin(Monitor):
"""WLD 모니터링 및 매매 실행."""
def __init__(self, cooldown_file: str = COOLDOWN_FILE) -> None:
super().__init__(cooldown_file)
def monitor_wld(self) -> None:
"""일봉·1시간 추세 + 3분 신호로 현물 매수/매도."""
balances = self.load_balances_dict()
print(
"[{}] {} ({})".format(
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
COIN_NAME,
SYMBOL,
)
)
self.process_wld_mtf(SYMBOL, balances=balances)
def run_schedule(self) -> None:
while True:
self.monitor_wld()
time.sleep(MONITOR_LOOP_SLEEP_SEC)
if __name__ == "__main__":
MonitorCoin().run_schedule()

222
mtf_bb.py
View File

@@ -1,222 +0,0 @@
"""
봉 간격별 볼린저밴드 상태 분석 및 최적 매수/매도 봉 추천.
기본 규칙(모든 봉 동일):
- 매수: 하단 밴드 상향 돌파
- 매도: 상단 밴드 상향 돌파
- 손절(선택): 하단 재이탈
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass
from pathlib import Path
import pandas as pd
from config import DOWNLOAD_INTERVALS, SYMBOL
from strategy import (
ACTIVE_CONFIG,
StrategyConfig,
annotate_interval_signals,
get_latest_bb_state,
MtfBbPolicy,
)
POLICY_FILE = Path(__file__).parent / "mtf_bb_policy.json"
# 매수/매도 트리거 실행 후보 (긴 봉은 확인용만)
EXECUTION_INTERVAL_CANDIDATES: tuple[int, ...] = (3, 10, 15, 30, 60)
DEFAULT_CONFIRM_INTERVALS: tuple[int, ...] = (60, 1440)
@dataclass
class IntervalBacktestSummary:
"""봉 간격별 백테스트 요약."""
interval: int
label: str
return_pct: float
trade_count: int
buy_count: int
sell_count: int
final_asset: float
def interval_label(interval: int) -> str:
if interval >= 1440:
return "일봉"
return f"{interval}"
def load_frames_from_db(monitor, symbol: str) -> dict[int, pd.DataFrame]:
"""coins.db에서 DOWNLOAD_INTERVALS 전부 로드·지표 계산."""
frames: dict[int, pd.DataFrame] = {}
for iv in DOWNLOAD_INTERVALS:
df = monitor.get_coin_some_data(symbol, iv)
if df is None or df.empty:
print(f" [{interval_label(iv)}] DB/API 데이터 없음 — 스킵")
continue
df = monitor.calculate_technical_indicators(df)
frames[iv] = df
print(f" [{interval_label(iv)}] {len(df)}{df.index[0]} ~ {df.index[-1]}")
return frames
def print_latest_states(frames: dict[int, pd.DataFrame], cfg: StrategyConfig) -> None:
"""각 봉의 최신 BB 상태 출력."""
print("\n--- 봉별 최신 BB 상태 ---")
for iv in sorted(frames.keys()):
st = get_latest_bb_state(frames[iv], cfg)
df = frames[iv]
close = df["Close"].iloc[-1]
print(
f" {interval_label(iv):>6} ({iv:>4}분) | {st:20} | 종가 {close:,.2f} | "
f"L={df['Lower'].iloc[-1]:,.2f} U={df['Upper'].iloc[-1]:,.2f}"
)
def backtest_interval(
frames: dict[int, pd.DataFrame],
interval: int,
cfg: StrategyConfig,
) -> IntervalBacktestSummary | None:
"""한 간격만으로 BB 매매 백테스트."""
from simulation import run_backtest
if interval not in frames:
return None
df = annotate_interval_signals(SYMBOL, frames[interval].copy(), config=cfg)
# 단일 봉 비교이므로 추세 필터 없이 동일 df를 HTF로 전달
res = run_backtest(df, frames[interval], frames[interval], config_name=f"{interval}")
buys = sum(1 for t in res.trades if t.action == "매수")
sells = sum(1 for t in res.trades if t.action == "매도")
return IntervalBacktestSummary(
interval=interval,
label=interval_label(interval),
return_pct=res.total_return_pct,
trade_count=res.trade_count,
buy_count=buys,
sell_count=sells,
final_asset=res.final_asset,
)
def recommend_policy(
summaries: list[IntervalBacktestSummary],
frames: dict[int, pd.DataFrame],
) -> MtfBbPolicy:
"""
백테스트 결과로 매수/매도 실행 봉과 확인용 상위 봉을 추천합니다.
- 매수/매도 실행: 수익률 1위 간격 (동일하면 더 긴 봉 우선)
- 확인 봉: 실행 봉보다 긴 간격 중 가장 가까운 2개
"""
if not summaries:
return MtfBbPolicy()
exec_pool = [s for s in summaries if s.interval in EXECUTION_INTERVAL_CANDIDATES]
if not exec_pool:
exec_pool = list(summaries)
ranked = sorted(
exec_pool,
key=lambda s: (s.return_pct, s.trade_count, -s.interval),
reverse=True,
)
best = ranked[0]
buy_iv = sell_iv = best.interval
if best.trade_count == 0:
buy_iv = sell_iv = min(
(iv for iv in frames if iv in EXECUTION_INTERVAL_CANDIDATES),
default=min(frames.keys()),
)
longer = sorted([iv for iv in frames if iv > buy_iv])
confirm = tuple(iv for iv in DEFAULT_CONFIRM_INTERVALS if iv in frames and iv > buy_iv)
if not confirm:
confirm = tuple(longer[-2:]) if len(longer) >= 2 else tuple(longer)
if not confirm and len(longer) == 1:
confirm = (longer[0],)
return MtfBbPolicy(
buy_interval=buy_iv,
sell_interval=sell_iv,
buy_confirm_intervals=confirm,
sell_confirm_intervals=confirm[:1] if confirm else (),
name=f"auto_{interval_label(buy_iv)}_buy_{interval_label(sell_iv)}_sell",
)
def run_interval_comparison(monitor) -> tuple[MtfBbPolicy, list[IntervalBacktestSummary]]:
"""모든 봉 간격 BB 백테스트 후 정책 추천."""
cfg = StrategyConfig(
name="봉별_BB_기본",
use_mtf=False,
use_regime_switch=False,
use_rsi_filter=False,
use_volume_filter=False,
use_squeeze_filter=False,
use_stop_loss=True,
)
print(f"\n{'='*72}")
print("봉 간격별 BB 매매 비교 (하단↑매수 / 상단↑매도 / 수수료 반영)")
print(f"{'='*72}")
frames = load_frames_from_db(monitor, SYMBOL)
if not frames:
raise RuntimeError("로드된 봉 데이터가 없습니다. downloader.py 먼저 실행하세요.")
print_latest_states(frames, cfg)
summaries: list[IntervalBacktestSummary] = []
for iv in sorted(frames.keys()):
s = backtest_interval(frames, iv, cfg)
if s:
summaries.append(s)
summaries.sort(key=lambda x: x.return_pct, reverse=True)
print(f"\n{'순위':<4} {'':>8} {'수익률':>9} {'거래':>6} {'매수':>5} {'매도':>5}")
print("-" * 45)
for i, s in enumerate(summaries, 1):
print(
f"{i:<4} {s.label:>8} {s.return_pct:>+8.2f}% "
f"{s.trade_count:>6} {s.buy_count:>5} {s.sell_count:>5}"
)
policy = recommend_policy(summaries, frames)
print(f"\n추천 정책:")
print(f" 매수 실행 봉: {interval_label(policy.buy_interval)}")
print(f" 매도 실행 봉: {interval_label(policy.sell_interval)}")
print(f" 매수 확인 봉: {[interval_label(i) for i in policy.buy_confirm_intervals]}")
print(f" 매도 확인 봉: {[interval_label(i) for i in policy.sell_confirm_intervals]}")
print(f"{'='*72}\n")
return policy, summaries
def save_policy(policy: MtfBbPolicy, path: Path = POLICY_FILE) -> None:
"""추천 정책을 JSON으로 저장."""
path.write_text(json.dumps(asdict(policy), ensure_ascii=False, indent=2), encoding="utf-8")
def load_policy(path: Path = POLICY_FILE) -> MtfBbPolicy | None:
"""저장된 정책 로드."""
if not path.exists():
return None
data = json.loads(path.read_text(encoding="utf-8"))
return MtfBbPolicy(
buy_interval=int(data["buy_interval"]),
sell_interval=int(data["sell_interval"]),
buy_confirm_intervals=tuple(data.get("buy_confirm_intervals", [])),
sell_confirm_intervals=tuple(data.get("sell_confirm_intervals", [])),
name=data.get("name", "loaded"),
)
def apply_policy(policy: MtfBbPolicy) -> None:
"""strategy.ACTIVE_MTF_POLICY 에 반영."""
import strategy as st
st.ACTIVE_MTF_POLICY = policy
print(f"ACTIVE_MTF_POLICY 적용: {policy.name}")

View File

@@ -1,11 +0,0 @@
{
"buy_interval": 60,
"sell_interval": 60,
"buy_confirm_intervals": [
1440
],
"sell_confirm_intervals": [
1440
],
"name": "auto_60분_buy_60분_sell"
}

View File

@@ -3,5 +3,6 @@ numpy
PyJWT PyJWT
requests requests
python-dateutil python-dateutil
python-dotenv>=1.0.0
python-telegram-bot python-telegram-bot
plotly plotly

View File

@@ -1,859 +0,0 @@
"""
모든 봉·캔들 특징 행렬에서 매수/매도 규칙을 탐색합니다 (인과적 백테스트).
python simulation.py discover
"""
from __future__ import annotations
import json
import random
from dataclasses import asdict, dataclass, field
from pathlib import Path
import numpy as np
import pandas as pd
from candle_features import (
FEATURE_BOOL_COLS,
build_master_feature_matrix,
interval_prefix,
)
from config import (
BUY_COOLDOWN_SEC,
BUY_MAX_BB_POS_CHASE,
DISCOVER_MAX_TRADES,
DISCOVER_TRADE_PENALTY_PCT,
DOWNLOAD_INTERVALS,
ENTRY_INTERVAL,
SELL_COOLDOWN_SEC,
SELL_MIN_BB_POS,
SIGNAL_EDGE_ONLY,
SIM_INITIAL_CASH_KRW,
SIM_MIN_ORDER_KRW,
SYMBOL,
TRADE_MIN_GAP_BARS,
TRADING_FEE_RATE,
)
from strategy import (
SIGNAL_BUY_LOWER,
SIGNAL_SELL_STOP,
SIGNAL_SELL_UPPER,
)
RULES_FILE = Path(__file__).parent / "discovered_rules.json"
# 탐색에 쓸 특징 (불리언 컬럼)
SEARCH_FEATURES: tuple[str, ...] = FEATURE_BOOL_COLS
# 상위 봉 과열·하락 차단용 부정 조건 후보
NEG_BLOCK_FEATURES: tuple[str, ...] = (
"cross_up_upper",
"above_upper",
"cross_down_lower",
"shooting_star",
"ichi_above_cloud",
"bb_zone_top",
"bb_pos_high",
)
# 탐색 규칙 적용 시 항상 매수 차단 (상단 돌파·과열)
BUY_SAFETY_BLOCK: tuple[str, ...] = (
"m3:above_upper",
"m3:cross_up_upper",
"m10:above_upper",
"m10:cross_up_upper",
)
# 연속 봉에서 오래 참 → 엣지 없으면 과다 체결
LEVEL_STATE_FEATURES: tuple[str, ...] = (
"below_lower",
"above_upper",
"inside_band",
"bb_zone_bottom",
"bb_zone_top",
"bb_pos_low",
"bb_pos_high",
"ichi_price_above_tenkan",
"ichi_price_below_tenkan",
)
@dataclass
class DiscoveredRules:
"""탐색된 매수/매도 규칙 (모든 봉 특징 조합)."""
name: str = "discovered"
buy_all: list[str] = field(default_factory=list)
buy_any: list[list[str]] = field(default_factory=list)
sell_all: list[str] = field(default_factory=list)
sell_stop: list[str] = field(default_factory=list)
train_return_pct: float = 0.0
test_return_pct: float = 0.0
full_return_pct: float = 0.0
trade_count: int = 0
def predicate_column(key: str) -> tuple[str, bool]:
"""
'm60:cross_up_lower' / 'd1:!above_upper' -> (컬럼명, negated).
"""
if ":" not in key:
raise ValueError(f"잘못된 predicate: {key}")
prefix, rest = key.split(":", 1)
neg = rest.startswith("!")
feat = rest[1:] if neg else rest
return f"{prefix}_{feat}", neg
def _predicate_feature(key: str) -> str:
"""predicate에서 특징명만 추출 (! 제외)."""
rest = key.split(":", 1)[1]
return rest[1:] if rest.startswith("!") else rest
def is_level_state_predicate(key: str) -> bool:
"""한번 참이면 여러 봉 연속 참인 상태형 조건."""
return _predicate_feature(key) in LEVEL_STATE_FEATURES
def is_weak_sell_predicate(key: str) -> bool:
"""
!cross_* / !below_* 등 — 대부분의 봉에서 참이라 매도가 과다해짐.
"""
if ":" not in key:
return False
rest = key.split(":", 1)[1]
if not rest.startswith("!"):
return False
feat = rest[1:]
if feat.startswith("cross_"):
return True
return feat in ("below_lower", "above_upper", "inside_band")
def is_blocked_buy_predicate(key: str) -> bool:
"""진입(3분) 봉의 상태형 매수 조건은 탐색에서 제외."""
pfx = interval_prefix(ENTRY_INTERVAL)
return key.startswith(f"{pfx}:") and is_level_state_predicate(key)
# 고점 추격 매수(상단 구간·과열) — 탐색·체결에서 제외
CHASE_BUY_FEATURES: tuple[str, ...] = (
"bb_zone_top",
"bb_zone_high",
"bb_pos_high",
"above_upper",
"cross_up_upper",
)
# 저점·반등 매수 트리거
VALUE_BUY_FEATURES: tuple[str, ...] = (
"cross_up_lower",
"bb_zone_bottom",
"bb_zone_low",
"hammer",
"bb_pos_low",
"ichi_tk_cross_up",
"cross_down_lower",
)
def is_chase_buy_predicate(key: str) -> bool:
"""밴드 상단·고점 추격 매수 조건."""
if ":" not in key:
return False
rest = key.split(":", 1)[1]
if rest.startswith("!"):
return False
return _predicate_feature(key) in CHASE_BUY_FEATURES
def is_value_buy_predicate(key: str) -> bool:
"""하단 돌파·반등형 매수 조건."""
if ":" not in key:
return False
rest = key.split(":", 1)[1]
if rest.startswith("!"):
return False
return _predicate_feature(key) in VALUE_BUY_FEATURES
def _entry_bb_pos_col() -> str:
return f"{interval_prefix(ENTRY_INTERVAL)}_bb_pos"
def discover_score(return_pct: float, trade_count: int) -> float:
"""탐색 목적함수: 수익률 과다 거래 패널티."""
excess = max(0, trade_count - DISCOVER_MAX_TRADES)
return return_pct - excess * DISCOVER_TRADE_PENALTY_PCT
def _rising_edge(mask: np.ndarray, i: int) -> bool:
"""i번째 봉에서 조건이 새로 참이 됐는지."""
if not bool(mask[i]):
return False
if i == 0:
return True
return not bool(mask[i - 1])
def _trigger_at(mask: np.ndarray, i: int, edge_only: bool = SIGNAL_EDGE_ONLY) -> bool:
if edge_only:
return _rising_edge(mask, i)
return bool(mask[i])
def _mask_for_keys(matrix: pd.DataFrame, keys: list[str]) -> np.ndarray:
"""AND 조건 마스크."""
n = len(matrix)
if not keys:
return np.ones(n, dtype=bool)
out = np.ones(n, dtype=bool)
for key in keys:
col, neg = predicate_column(key)
if col not in matrix.columns:
return np.zeros(n, dtype=bool)
vals = matrix[col].fillna(0).astype(bool).to_numpy()
if neg:
vals = ~vals
out &= vals
return out
def _unsafe_buy_mask(matrix: pd.DataFrame) -> np.ndarray:
"""
고점 매수 차단.
- 상단/상향돌파
- 3분 밴드 하단(low)인데 유성형 → 급등 끝물음 (5/27 사례)
"""
n = len(matrix)
unsafe = np.zeros(n, dtype=bool)
for key in BUY_SAFETY_BLOCK:
col, neg = predicate_column(key)
if neg or col not in matrix.columns:
continue
unsafe |= matrix[col].fillna(0).astype(bool).to_numpy()
if "Close" in matrix.columns:
roll_hi = matrix["Close"].astype(float).rolling(20, min_periods=5).max()
near_peak = matrix["Close"].astype(float) >= roll_hi * 0.97
if "m3_bb_pos_low" in matrix.columns and "m3_shooting_star" in matrix.columns:
# 급등 끝 고점: 밴드하단+유성형 (5/27 02:33) — 차트상 매도 구간
toxic = (
matrix["m3_bb_pos_low"].fillna(0).astype(bool)
& matrix["m3_shooting_star"].fillna(0).astype(bool)
& near_peak.fillna(False)
)
unsafe |= toxic.to_numpy()
if "m30_hammer" in matrix.columns:
# 30분 망치만으로 고점 매수 (5/27 00:00)
unsafe |= (
matrix["m30_hammer"].fillna(0).astype(bool) & near_peak.fillna(False)
).to_numpy()
if _entry_bb_pos_col() in matrix.columns:
pos = matrix[_entry_bb_pos_col()].fillna(0.5).astype(float).to_numpy()
unsafe |= pos >= BUY_MAX_BB_POS_CHASE
for key in CHASE_BUY_FEATURES:
col = f"{interval_prefix(ENTRY_INTERVAL)}_{key}"
if col in matrix.columns:
unsafe |= matrix[col].fillna(0).astype(bool).to_numpy()
return unsafe
def _value_buy_gate_mask(matrix: pd.DataFrame, group: list[str]) -> np.ndarray:
"""
매수 그룹별: 저점 트리거(value) 또는 3분 bb_pos < BUY_MAX_BB_POS_CHASE 일 때만 허용.
"""
n = len(matrix)
pos_col = _entry_bb_pos_col()
if pos_col in matrix.columns:
pos_ok = (
matrix[pos_col].fillna(0.5).astype(float).to_numpy()
< BUY_MAX_BB_POS_CHASE
)
else:
pos_ok = np.ones(n, dtype=bool)
value_keys = [k for k in group if is_value_buy_predicate(k)]
if not value_keys:
return pos_ok
value_hit = _mask_for_keys(matrix, value_keys)
return pos_ok | value_hit
def _unsafe_sell_mask(matrix: pd.DataFrame) -> np.ndarray:
"""
저점·반등 구간 매도 차단.
- 3분 bb_pos < SELL_MIN_BB_POS
- 망치·밴드 하단 구간에서 상단돌파 익절 방지 (5/26 01:48 유형)
"""
n = len(matrix)
blocked = np.zeros(n, dtype=bool)
pos_col = _entry_bb_pos_col()
if pos_col in matrix.columns:
pos = matrix[pos_col].fillna(0.5).astype(float).to_numpy()
blocked |= pos < SELL_MIN_BB_POS
pfx = interval_prefix(ENTRY_INTERVAL)
for feat in ("hammer", "bb_zone_bottom", "bb_zone_low", "bb_pos_low"):
col = f"{pfx}_{feat}"
if col in matrix.columns:
blocked |= matrix[col].fillna(0).astype(bool).to_numpy()
return blocked
def buy_mask(matrix: pd.DataFrame, rules: DiscoveredRules) -> np.ndarray:
"""
매수 마스크 = (buy_all) 또는 (buy_any 각 그룹의 AND) 중 하나 + 안전필터.
buy_any는 추가 분기(OR)이지, buy_all과의 AND가 아닙니다.
"""
n = len(matrix)
groups: list[list[str]] = []
if rules.buy_all:
groups.append(list(rules.buy_all))
for g in rules.buy_any:
if g:
groups.append(list(g))
if not groups:
return np.zeros(n, dtype=bool)
any_ok = np.zeros(n, dtype=bool)
for group in groups:
raw = _mask_for_keys(matrix, group)
any_ok |= raw & _value_buy_gate_mask(matrix, group)
return any_ok & ~_unsafe_buy_mask(matrix)
def sell_mask(matrix: pd.DataFrame, rules: DiscoveredRules, stop: bool = False) -> np.ndarray:
keys = rules.sell_stop if stop else rules.sell_all
raw = _mask_for_keys(matrix, keys)
if stop:
return raw
return raw & ~_unsafe_sell_mask(matrix)
def sanitize_rules(rules: DiscoveredRules) -> DiscoveredRules:
"""탐색 결과에서 추격 매수·무의미 조건 제거."""
rules.buy_all = [p for p in rules.buy_all if not is_chase_buy_predicate(p)]
rules.buy_any = [
[p for p in g if not is_chase_buy_predicate(p)]
for g in rules.buy_any
]
rules.buy_any = [g for g in rules.buy_any if g]
rules.sell_all = [p for p in rules.sell_all if not is_weak_sell_predicate(p)]
return rules
def _discovery_seed() -> DiscoveredRules:
"""탐색 시드: 하단 돌파 기준선 (combination_seed의 상단 추격 매수 미사용)."""
return _baseline_rules()
def generate_predicate_pool(intervals: list[int]) -> list[str]:
"""탐색 후보 predicate 목록."""
pool: list[str] = []
for iv in intervals:
pfx = interval_prefix(iv)
for feat in SEARCH_FEATURES:
pool.append(f"{pfx}:{feat}")
if iv != ENTRY_INTERVAL:
for feat in NEG_BLOCK_FEATURES:
pool.append(f"{pfx}:!{feat}")
return pool
def list_rule_signal_edges(
matrix: pd.DataFrame,
rules: DiscoveredRules,
) -> list[tuple[pd.Timestamp, str]]:
"""
전 기간 규칙 엣지 신호(체결 여부와 무관).
Returns:
(timestamp, action) — buy_signal | sell_signal | sell_stop_signal
"""
idx = matrix.index
b_mask = buy_mask(matrix, rules)
s_mask = sell_mask(matrix, rules, stop=False)
stop_mask = (
sell_mask(matrix, rules, stop=True)
if rules.sell_stop
else np.zeros(len(matrix), dtype=bool)
)
out: list[tuple[pd.Timestamp, str]] = []
for i in range(len(matrix)):
if _rising_edge(b_mask, i):
out.append((idx[i], "buy_signal"))
if _rising_edge(s_mask, i):
out.append((idx[i], "sell_signal"))
if rules.sell_stop and _rising_edge(stop_mask, i):
out.append((idx[i], "sell_stop_signal"))
return out
def generate_trade_events(
matrix: pd.DataFrame,
rules: DiscoveredRules,
) -> list[tuple[pd.Timestamp, str, str]]:
"""
규칙에 따른 체결 이벤트 목록.
Returns:
(timestamp, action, signal_name)
"""
close = matrix["Close"].astype(float).to_numpy()
idx = matrix.index
b_mask = buy_mask(matrix, rules)
s_mask = sell_mask(matrix, rules, stop=False)
stop_mask = (
sell_mask(matrix, rules, stop=True)
if rules.sell_stop
else np.zeros(len(matrix), dtype=bool)
)
events: list[tuple[pd.Timestamp, str, str]] = []
qty = 0.0
last_buy_i: int | None = None
last_sell_i: int | None = None
last_trade_i: int | None = None
for i in range(len(matrix)):
price = close[i]
if price <= 0 or np.isnan(price):
continue
ts = idx[i]
if last_trade_i is not None and i - last_trade_i < TRADE_MIN_GAP_BARS:
continue
if qty > 0:
is_stop = _trigger_at(stop_mask, i) if rules.sell_stop else False
is_sell = _trigger_at(s_mask, i)
if is_stop or is_sell:
if last_sell_i is not None:
if (ts - idx[last_sell_i]).total_seconds() < SELL_COOLDOWN_SEC:
continue
sig = SIGNAL_SELL_STOP if is_stop else SIGNAL_SELL_UPPER
events.append((ts, "sell", sig))
qty = 0.0
last_sell_i = i
last_trade_i = i
continue
if _trigger_at(b_mask, i) and qty <= 0:
if last_buy_i is not None:
if (ts - idx[last_buy_i]).total_seconds() < BUY_COOLDOWN_SEC:
continue
events.append((ts, "buy", SIGNAL_BUY_LOWER))
qty = 1.0
last_buy_i = i
last_trade_i = i
return events
def backtest_rules(
matrix: pd.DataFrame,
rules: DiscoveredRules,
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
entry_ohlc: pd.DataFrame,
) -> tuple[float, int]:
"""
HTML 시뮬과 동일한 run_backtest 로직으로 수익률 계산.
"""
from simulation import run_backtest
import strategy as st
df = entry_ohlc.loc[matrix.index].copy()
df["signal"] = ""
df["point"] = 0
df["action"] = ""
df["trend"] = ""
for ts, action, sig in generate_trade_events(matrix, rules):
if ts not in df.index:
continue
trend_at = st.get_trend_at(df_1d, df_1h, ts)
df.at[ts, "signal"] = sig
df.at[ts, "point"] = 1
df.at[ts, "action"] = action
df.at[ts, "trend"] = trend_at
res = run_backtest(df, df_1d, df_1h, config_name=rules.name)
return res.total_return_pct, res.trade_count
def _evaluate_train(
train: pd.DataFrame,
rules: DiscoveredRules,
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
entry_ohlc: pd.DataFrame,
) -> tuple[float, int, float]:
"""학습 구간 수익·거래수·목적함수 점수."""
ret, tc = backtest_rules(train, rules, df_1d, df_1h, entry_ohlc)
return ret, tc, discover_score(ret, tc)
def _baseline_rules() -> DiscoveredRules:
"""다봉 BB 하단 돌파 + 상단 돌파 기준선."""
p3 = interval_prefix(ENTRY_INTERVAL)
return DiscoveredRules(
name="baseline_bb_mtf",
buy_all=[
f"{p3}:cross_up_lower",
f"{interval_prefix(60)}:ichi_tk_bull",
f"{interval_prefix(1440)}:!ichi_below_cloud",
],
sell_all=[
f"{p3}:cross_up_upper",
f"{interval_prefix(60)}:cross_up_upper",
],
sell_stop=[f"{p3}:cross_down_lower"],
)
def _seed_from_combination_report() -> DiscoveredRules | None:
"""combination_report.json 제안 규칙."""
path = Path(__file__).parent / "combination_report.json"
if not path.exists():
return None
data = json.loads(path.read_text(encoding="utf-8"))
sug = data.get("suggested_rules") or {}
buy_all = list(sug.get("buy_all") or [])
if not buy_all:
return None
return DiscoveredRules(
name="combination_seed",
buy_all=buy_all,
buy_any=[list(g) for g in (sug.get("buy_any") or []) if g],
sell_all=list(sug.get("sell_all") or [f"{interval_prefix(ENTRY_INTERVAL)}:cross_up_upper"]),
sell_stop=list(sug.get("sell_stop") or []),
)
def greedy_search(
matrix: pd.DataFrame,
train_end: int,
pool: list[str],
seed: DiscoveredRules,
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
entry_ohlc: pd.DataFrame,
max_buy: int = 8,
max_sell: int = 6,
max_stop: int = 2,
) -> DiscoveredRules:
"""학습 구간 수익률을 올리도록 매수/매도 조건을 탐욕적으로 확장."""
train = matrix.iloc[:train_end]
best = DiscoveredRules(
name=seed.name,
buy_all=list(seed.buy_all),
buy_any=[list(g) for g in seed.buy_any],
sell_all=list(seed.sell_all),
sell_stop=list(seed.sell_stop),
)
best_ret, best_tc, best_score = _evaluate_train(
train, best, df_1d, df_1h, entry_ohlc
)
buy_pool = [
p
for p in pool
if not is_blocked_buy_predicate(p) and not is_chase_buy_predicate(p)
]
sell_pool = [p for p in pool if not is_weak_sell_predicate(p)]
improved = True
while improved:
improved = False
# 매수 AND 추가/제거
for pred in buy_pool:
if pred in best.buy_all:
trial_all = [p for p in best.buy_all if p != pred]
else:
if len(best.buy_all) >= max_buy:
continue
trial_all = best.buy_all + [pred]
trial = DiscoveredRules(
name="trial",
buy_all=trial_all,
buy_any=best.buy_any,
sell_all=best.sell_all,
sell_stop=best.sell_stop,
)
ret, tc, score = _evaluate_train(
train, trial, df_1d, df_1h, entry_ohlc
)
if score > best_score:
best_ret, best_tc, best_score = ret, tc, score
best.buy_all = trial_all
improved = True
# 매도 AND
for pred in sell_pool:
if pred in best.sell_all:
trial_s = [p for p in best.sell_all if p != pred]
else:
if len(best.sell_all) >= max_sell:
continue
trial_s = best.sell_all + [pred]
trial = DiscoveredRules(
name="trial",
buy_all=best.buy_all,
buy_any=best.buy_any,
sell_all=trial_s,
sell_stop=best.sell_stop,
)
ret, tc, score = _evaluate_train(
train, trial, df_1d, df_1h, entry_ohlc
)
if score > best_score:
best_ret, best_tc, best_score = ret, tc, score
best.sell_all = trial_s
improved = True
# 손절
stop_pool = [
p
for p in pool
if "cross_down_lower" in p
and not is_level_state_predicate(p)
]
for pred in stop_pool:
if pred in best.sell_stop:
trial_st = [p for p in best.sell_stop if p != pred]
else:
if len(best.sell_stop) >= max_stop:
continue
trial_st = best.sell_stop + [pred]
trial = DiscoveredRules(
name="trial",
buy_all=best.buy_all,
buy_any=best.buy_any,
sell_all=best.sell_all,
sell_stop=trial_st,
)
ret, tc, score = _evaluate_train(
train, trial, df_1d, df_1h, entry_ohlc
)
if score > best_score:
best_ret, best_tc, best_score = ret, tc, score
best.sell_stop = trial_st
improved = True
return sanitize_rules(best)
def try_buy_any_branches(
matrix: pd.DataFrame,
train_end: int,
base: DiscoveredRules,
pool: list[str],
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
entry_ohlc: pd.DataFrame,
max_branches: int = 8,
) -> DiscoveredRules:
"""매수 OR 분기: 다른 봉의 cross_up_lower / hammer 등."""
train = matrix.iloc[:train_end]
triggers = [
p
for p in pool
if p.endswith(":cross_up_lower")
or p.endswith(":hammer")
or p.endswith(":bb_zone_bottom")
or p.endswith(":ichi_tk_cross_up")
]
best = DiscoveredRules(
name=base.name,
buy_all=list(base.buy_all),
buy_any=[list(g) for g in base.buy_any],
sell_all=list(base.sell_all),
sell_stop=list(base.sell_stop),
)
best_ret, best_tc, best_score = _evaluate_train(
train, best, df_1d, df_1h, entry_ohlc
)
for pred in triggers[:max_branches]:
if pred in best.buy_all:
continue
trial = DiscoveredRules(
name="trial_or",
buy_all=[],
buy_any=[list(best.buy_all), [pred]],
sell_all=best.sell_all,
sell_stop=best.sell_stop,
)
if not trial.buy_any[0]:
trial.buy_any = [[pred]]
ret, tc, score = _evaluate_train(
train, trial, df_1d, df_1h, entry_ohlc
)
if score > best_score:
best_ret, best_score = ret, score
best = sanitize_rules(trial)
best.name = "discovered_or"
return sanitize_rules(best)
def random_search_refine(
matrix: pd.DataFrame,
train_end: int,
pool: list[str],
seed: DiscoveredRules,
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
entry_ohlc: pd.DataFrame,
iterations: int = 1200,
) -> DiscoveredRules:
"""무작위 변형으로 국소 최적 보완."""
train = matrix.iloc[:train_end]
best = seed
best_ret, best_tc, best_score = _evaluate_train(
train, best, df_1d, df_1h, entry_ohlc
)
rng = random.Random(42)
buy_pool = [
p
for p in pool
if not is_blocked_buy_predicate(p) and not is_chase_buy_predicate(p)
]
sell_pool = [p for p in pool if not is_weak_sell_predicate(p)]
for _ in range(iterations):
trial = DiscoveredRules(
name="rand",
buy_all=[p for p in best.buy_all],
buy_any=[list(g) for g in best.buy_any],
sell_all=[p for p in best.sell_all],
sell_stop=[p for p in best.sell_stop],
)
action = rng.choice(["add_buy", "drop_buy", "add_sell", "drop_sell", "swap_buy"])
if action == "add_buy" and len(trial.buy_all) < 6 and buy_pool:
p = rng.choice(buy_pool)
if p not in trial.buy_all:
trial.buy_all.append(p)
elif action == "drop_buy" and trial.buy_all:
trial.buy_all.pop(rng.randrange(len(trial.buy_all)))
elif action == "add_sell" and len(trial.sell_all) < 5 and sell_pool:
p = rng.choice(sell_pool)
if p not in trial.sell_all:
trial.sell_all.append(p)
elif action == "drop_sell" and trial.sell_all:
trial.sell_all.pop(rng.randrange(len(trial.sell_all)))
elif action == "swap_buy" and buy_pool:
if trial.buy_all:
trial.buy_all[rng.randrange(len(trial.buy_all))] = rng.choice(buy_pool)
trial = sanitize_rules(trial)
ret, tc, score = _evaluate_train(
train, trial, df_1d, df_1h, entry_ohlc
)
if score > best_score:
best_ret, best_score = ret, score
best = trial
best.name = "discovered_refined"
return sanitize_rules(best)
def discover_rules(frames: dict[int, pd.DataFrame]) -> DiscoveredRules:
"""전체 탐색 파이프라인."""
print("특징 행렬 생성 (모든 봉·캔들 위치/높이)...")
from config import ENTRY_INTERVAL, TREND_INTERVAL_1D, TREND_INTERVAL_1H
entry_raw = frames[ENTRY_INTERVAL]
df_1d = frames.get(TREND_INTERVAL_1D)
if df_1d is None or df_1d.empty:
df_1d = entry_raw
df_1h = frames.get(TREND_INTERVAL_1H)
if df_1h is None or df_1h.empty:
df_1h = entry_raw
matrix = build_master_feature_matrix(frames)
matrix = matrix.iloc[21:].copy()
entry_ohlc = entry_raw.iloc[21:].loc[matrix.index]
n = len(matrix)
train_end = int(n * 0.7)
intervals = sorted(frames.keys())
pool = generate_predicate_pool(intervals)
print(f" 샘플 {n}봉 | 학습 {train_end} | predicate 후보 {len(pool)}")
baseline = _discovery_seed()
br, bt = backtest_rules(
matrix.iloc[:train_end], baseline, df_1d, df_1h, entry_ohlc
)
print(f" 시드 규칙: {baseline.name} (하단돌파 매수·상단돌파 매도)")
bf, _ = backtest_rules(matrix, baseline, df_1d, df_1h, entry_ohlc)
print(f" 기준선: 학습 {br:+.2f}% | 전체 {bf:+.2f}%")
print("1단계: 탐욕적 AND 확장...")
g1 = greedy_search(matrix, train_end, pool, baseline, df_1d, df_1h, entry_ohlc)
r1, _ = backtest_rules(matrix.iloc[:train_end], g1, df_1d, df_1h, entry_ohlc)
print(f" 학습 {r1:+.2f}% | buy={g1.buy_all} sell={g1.sell_all}")
print("2단계: 매수 OR 분기(다른 봉 트리거)...")
g2 = try_buy_any_branches(matrix, train_end, g1, pool, df_1d, df_1h, entry_ohlc)
r2, _ = backtest_rules(matrix.iloc[:train_end], g2, df_1d, df_1h, entry_ohlc)
print(f" 학습 {r2:+.2f}%")
print("3단계: 무작위 정밀 탐색...")
best = g2 if r2 >= r1 else g1
g3 = random_search_refine(matrix, train_end, pool, best, df_1d, df_1h, entry_ohlc, iterations=1200)
g3 = sanitize_rules(g3)
train_ret, t_cnt = backtest_rules(
matrix.iloc[:train_end], g3, df_1d, df_1h, entry_ohlc
)
test_ret, _ = backtest_rules(
matrix.iloc[train_end:], g3, df_1d, df_1h, entry_ohlc
)
full_ret, full_cnt = backtest_rules(matrix, g3, df_1d, df_1h, entry_ohlc)
g3.train_return_pct = train_ret
g3.test_return_pct = test_ret
g3.full_return_pct = full_ret
g3.trade_count = full_cnt
g3.name = "discovered_best"
print(f"\n최종 규칙 ({g3.name})")
print(f" 매수 AND: {g3.buy_all}")
if g3.buy_any:
print(f" 매수 OR: {g3.buy_any}")
print(f" 매도 AND: {g3.sell_all}")
if g3.sell_stop:
print(f" 손절: {g3.sell_stop}")
print(
f" 학습 {train_ret:+.2f}% | 검증 {test_ret:+.2f}% | 전체 {full_ret:+.2f}% ({full_cnt}건)"
)
return g3
def save_rules(rules: DiscoveredRules, path: Path = RULES_FILE) -> None:
path.write_text(json.dumps(asdict(rules), ensure_ascii=False, indent=2), encoding="utf-8")
def rules_have_buy(rules: DiscoveredRules) -> bool:
"""매수 규칙이 하나라도 있는지."""
if rules.buy_all:
return True
return any(bool(g) for g in rules.buy_any)
def load_rules(path: Path = RULES_FILE) -> DiscoveredRules | None:
if not path.exists():
return None
data = json.loads(path.read_text(encoding="utf-8"))
rules = DiscoveredRules(**{k: data[k] for k in asdict(DiscoveredRules()).keys() if k in data})
if not rules_have_buy(rules):
return None
return rules
def load_frames(monitor) -> dict[int, pd.DataFrame]:
from mtf_bb import load_frames_from_db
return load_frames_from_db(monitor, SYMBOL)

11
scripts/01_download.py Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""01단계: WLD 봉 데이터 다운로드 → data/coins.db"""
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()

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""02단계: Ground Truth 매수·매도 타점 생성."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.ground_truth.ground_truth import run_from_db
if __name__ == "__main__":
run_from_db()

View File

@@ -0,0 +1,11 @@
#!/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

@@ -0,0 +1,11 @@
#!/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()

11
scripts/04_match_rules.py Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""04단계: GT 근접 규칙 선택 (스텁)."""
import runpy
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.matching.match_rules import run_match_stub
if __name__ == "__main__":
run_match_stub()

13
scripts/05_chart_bb.py Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""05: 3분봉 BB 차트 HTML."""
import runpy
import sys
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.ops import simulation
if __name__ == "__main__":
sys.argv = [sys.argv[0]]
simulation.main()

13
scripts/05_chart_truth.py Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""05: Ground Truth 차트 + JSON."""
import runpy
import sys
from pathlib import Path
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from deepcoin.ops import simulation
if __name__ == "__main__":
sys.argv = [sys.argv[0], "truth"]
simulation.main()

11
scripts/05_run_monitor.py Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""05단계: WLD 실시간 모니터 루프."""
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__":
MonitorCoin(cooldown_file=None).run_schedule()

17
scripts/README.md Normal file
View File

@@ -0,0 +1,17 @@
# scripts — 단계별 CLI
프로젝트 루트에서 실행하세요. 각 스크립트는 `_bootstrap.py``.env``config`를 로드합니다.
```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/04_match_rules.py # 스텁
python scripts/05_chart_truth.py
python scripts/05_chart_bb.py
python scripts/05_run_monitor.py
python scripts/verify_env.py
```
상세 구조: [docs/reference/STRUCTURE.md](../docs/reference/STRUCTURE.md)

11
scripts/_bootstrap.py Normal file
View File

@@ -0,0 +1,11 @@
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
load_project_env()

219
scripts/verify_env.py Normal file
View File

@@ -0,0 +1,219 @@
#!/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",
"DOWNLOAD_MONTHS_1M",
"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())

File diff suppressed because it is too large Load Diff

View File

@@ -1,677 +0,0 @@
"""
WLD 볼린저밴드 MTF 전략.
기본 타이밍 (모든 봉 동일):
- 매수: 하단 밴드 상향 돌파 (prev_close <= Lower, close > Lower)
- 매도: 상단 밴드 상향 돌파 (prev_close < Upper, close >= Upper)
여러 봉(3·10·15·30·60·240·1440분)의 BB 상태를 비교해
실행 봉·확인 봉을 정한 뒤 매매합니다 (MtfBbPolicy).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal
import numpy as np
import pandas as pd
from config import (
BB_MIN_WIDTH_PCT,
DEFAULT_BUY_KRW,
ENTRY_INTERVAL,
RANGE_BUY_KRW,
RSI_BUY_MAX,
RSI_PERIOD,
TREND_RANGE_MA_GAP_PCT,
VOLUME_BUY_RATIO,
)
Trend = Literal["up", "down", "range"]
Action = Literal["buy", "sell"]
# 기본 신호
SIGNAL_BUY_LOWER = "bb_base_buy_lower"
SIGNAL_SELL_UPPER = "bb_base_sell_upper"
SIGNAL_SELL_STOP = "bb_base_sell_stop"
BUY_SIGNALS = {SIGNAL_BUY_LOWER}
SELL_SIGNALS = {SIGNAL_SELL_UPPER, SIGNAL_SELL_STOP}
# 봉별 BB 상태 (분석·확인용)
STATE_SQUEEZE = "squeeze"
STATE_CROSS_UP_LOWER = "cross_up_lower"
STATE_CROSS_UP_UPPER = "cross_up_upper"
STATE_CROSS_DOWN_LOWER = "cross_down_lower"
STATE_BELOW_LOWER = "below_lower"
STATE_ABOVE_UPPER = "above_upper"
STATE_INSIDE = "inside"
BUY_TRIGGER_STATE = STATE_CROSS_UP_LOWER
SELL_TRIGGER_STATE = STATE_CROSS_UP_UPPER
STOP_TRIGGER_STATE = STATE_CROSS_DOWN_LOWER
# 확인 봉에서 매수 허용/차단 상태
BUY_CONFIRM_OK = {
STATE_INSIDE,
STATE_BELOW_LOWER,
STATE_CROSS_UP_LOWER,
STATE_SQUEEZE,
}
BUY_CONFIRM_BLOCK = {
STATE_CROSS_UP_UPPER,
STATE_ABOVE_UPPER,
STATE_CROSS_DOWN_LOWER,
}
SELL_CONFIRM_OK = {
STATE_INSIDE,
STATE_ABOVE_UPPER,
STATE_CROSS_UP_UPPER,
STATE_CROSS_UP_LOWER,
STATE_BELOW_LOWER,
}
@dataclass
class StrategyConfig:
"""전략 조합 설정 (시뮬 비교·실거래 공용)."""
name: str = "default"
use_mtf: bool = True
use_regime_switch: bool = True
use_rsi_filter: bool = True
rsi_buy_max: float = RSI_BUY_MAX
use_volume_filter: bool = False
volume_ratio: float = VOLUME_BUY_RATIO
use_squeeze_filter: bool = True
use_stop_loss: bool = True
allow_buy_in_up: bool = True
allow_buy_in_range: bool = True
allow_buy_in_down: bool = False
use_discovered_rules: bool = False
# HTML 시뮬: discovered_rules.json (python simulation.py discover)
ACTIVE_CONFIG = StrategyConfig(
name="discovered_best",
use_discovered_rules=True,
use_mtf=False,
use_regime_switch=False,
use_rsi_filter=False,
use_volume_filter=False,
use_squeeze_filter=False,
use_stop_loss=False,
)
@dataclass
class MtfBbPolicy:
"""
봉 간격별 BB 비교 후 적용할 매매 정책.
buy_interval / sell_interval: 실제 주문 트리거 봉
buy_confirm_intervals: 매수 시 함께 봐야 할 상위(긴) 봉
"""
buy_interval: int = 3
sell_interval: int = 3
buy_confirm_intervals: tuple[int, ...] = (60, 1440)
sell_confirm_intervals: tuple[int, ...] = (60,)
name: str = "default_mtf"
ACTIVE_MTF_POLICY = MtfBbPolicy(
name="3분실행_60·일봉확인",
buy_interval=3,
sell_interval=3,
buy_confirm_intervals=(60, 1440),
sell_confirm_intervals=(60,),
)
@dataclass
class TradeSignal:
"""실시간 1회 매매 신호."""
action: Action
signal: str
close: float
trend: Trend
def prepare_entry_df(data: pd.DataFrame) -> pd.DataFrame:
"""3분봉 보조 지표 추가."""
df = data.copy()
delta = df["Close"].diff()
gain = delta.where(delta > 0, 0.0).rolling(RSI_PERIOD).mean()
loss = (-delta.where(delta < 0, 0.0)).rolling(RSI_PERIOD).mean()
rs = gain / loss.replace(0, np.nan)
df["RSI"] = 100 - (100 / (1 + rs))
df["VolMA5"] = df["Volume"].rolling(5).mean()
ma = df["MA"].replace(0, np.nan)
df["BB_Width"] = (df["Upper"] - df["Lower"]) / ma * 100
return df
def get_trend(df_1d: pd.DataFrame, df_1h: pd.DataFrame) -> Trend:
"""일봉·1시간봉 기준 추세."""
if len(df_1d) < 20 or len(df_1h) < 40:
return "range"
d_close = float(df_1d["Close"].iloc[-1])
d_ma20 = float(df_1d["MA20"].iloc[-1])
h_close = float(df_1h["Close"].iloc[-1])
h_ma20 = float(df_1h["MA20"].iloc[-1])
h_ma40 = float(df_1h["MA40"].iloc[-1])
if h_ma40 == 0:
return "range"
ma_gap_pct = abs(h_ma20 - h_ma40) / h_ma40 * 100
if ma_gap_pct < TREND_RANGE_MA_GAP_PCT:
return "range"
if d_close > d_ma20 and h_ma20 > h_ma40 and h_close > h_ma20:
return "up"
if d_close < d_ma20 and h_ma20 < h_ma40 and h_close < h_ma20:
return "down"
return "range"
def get_trend_at(
df_1d: pd.DataFrame, df_1h: pd.DataFrame, ts: pd.Timestamp
) -> Trend:
"""특정 시점까지의 데이터로 추세 판별 (백테스트용)."""
d = df_1d[df_1d.index <= ts]
h = df_1h[df_1h.index <= ts]
if d.empty or h.empty:
return "range"
return get_trend(d, h)
def _bb_squeeze(bb_width: float) -> bool:
return bb_width < BB_MIN_WIDTH_PCT
def classify_bb_state_at(
df: pd.DataFrame,
index_pos: int,
cfg: StrategyConfig | None = None,
) -> str:
"""
한 봉의 볼린저밴드 상태를 분류합니다.
Returns:
cross_up_lower, cross_up_upper, cross_down_lower, below_lower,
above_upper, inside, squeeze
"""
cfg = cfg or ACTIVE_CONFIG
if index_pos < 0:
index_pos = len(df) + index_pos
if index_pos < 1 or index_pos >= len(df):
return STATE_INSIDE
close = float(df["Close"].iloc[index_pos])
prev_close = float(df["Close"].iloc[index_pos - 1])
lower = float(df["Lower"].iloc[index_pos])
prev_lower = float(df["Lower"].iloc[index_pos - 1])
upper = float(df["Upper"].iloc[index_pos])
prev_upper = float(df["Upper"].iloc[index_pos - 1])
bb_width = float(df["BB_Width"].iloc[index_pos]) if "BB_Width" in df.columns else 999.0
if cfg.use_squeeze_filter and _bb_squeeze(bb_width):
return STATE_SQUEEZE
if prev_close <= prev_lower and close > lower:
return STATE_CROSS_UP_LOWER
if prev_close < prev_upper and close >= upper:
return STATE_CROSS_UP_UPPER
if prev_close >= prev_lower and close < lower:
return STATE_CROSS_DOWN_LOWER
if close < lower:
return STATE_BELOW_LOWER
if close > upper:
return STATE_ABOVE_UPPER
return STATE_INSIDE
def get_latest_bb_state(df: pd.DataFrame, cfg: StrategyConfig | None = None) -> str:
"""마지막 봉의 BB 상태."""
df = prepare_entry_df(df)
if len(df) < 2:
return STATE_INSIDE
return classify_bb_state_at(df, -1, cfg)
def _allow_buy(trend: Trend, cfg: StrategyConfig) -> bool:
"""레짐·MTF에 따른 매수 허용 여부."""
if cfg.use_regime_switch:
if trend == "down":
return cfg.allow_buy_in_down
if trend == "range":
return cfg.allow_buy_in_range
if trend == "up":
return cfg.allow_buy_in_up
if cfg.use_mtf and trend == "down":
return False
return True
def evaluate_interval_bb(
symbol: str,
df_entry: pd.DataFrame,
config: StrategyConfig | None = None,
) -> TradeSignal | None:
"""
단일 봉 간격에서 BB 기본 규칙만으로 신호 1건.
매도 우선 → 매수.
"""
cfg = config or ACTIVE_CONFIG
df = prepare_entry_df(df_entry)
if len(df) < 22:
return None
state = classify_bb_state_at(df, -1, cfg)
close = float(df["Close"].iloc[-1])
trend: Trend = "range"
if cfg.use_stop_loss and state == STATE_CROSS_DOWN_LOWER:
return TradeSignal("sell", SIGNAL_SELL_STOP, close, trend)
if state == STATE_CROSS_UP_UPPER:
return TradeSignal("sell", SIGNAL_SELL_UPPER, close, trend)
if state == STATE_CROSS_UP_LOWER:
if cfg.use_rsi_filter:
rsi = float(df["RSI"].iloc[-1])
if not np.isnan(rsi) and rsi > cfg.rsi_buy_max:
return None
if cfg.use_volume_filter:
vol = float(df["Volume"].iloc[-1])
vol_ma = float(df["VolMA5"].iloc[-1])
if vol_ma > 0 and vol < vol_ma * cfg.volume_ratio:
return None
return TradeSignal("buy", SIGNAL_BUY_LOWER, close, trend)
return None
def _confirm_buy(states: dict[int, str], policy: MtfBbPolicy) -> bool:
"""상위 봉 상태가 매수에 적합한지."""
for iv in policy.buy_confirm_intervals:
st = states.get(iv)
if st is None:
return False
if st in BUY_CONFIRM_BLOCK:
return False
if st not in BUY_CONFIRM_OK and st != STATE_SQUEEZE:
return False
return True
def _confirm_sell(states: dict[int, str], policy: MtfBbPolicy) -> bool:
"""상위 봉 상태가 매도에 적합한지."""
for iv in policy.sell_confirm_intervals:
st = states.get(iv)
if st is None:
continue
if st in {STATE_CROSS_DOWN_LOWER, STATE_BELOW_LOWER}:
return False
return True
def evaluate_mtf_bb(
symbol: str,
frames: dict[int, pd.DataFrame],
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
policy: MtfBbPolicy | None = None,
config: StrategyConfig | None = None,
) -> TradeSignal | None:
"""
여러 봉의 BB 상태를 비교해 최종 매수/매도 1건을 결정합니다.
1) 각 봉 최신 BB 상태 분류
2) policy.sell_interval 에서 손절/상단돌파 → 매도
3) policy.buy_interval 에서 하단돌파 + 확인봉 OK → 매수
4) 일봉·1시간 추세로 하락장 매수 차단 (config)
"""
cfg = config or ACTIVE_CONFIG
policy = policy or ACTIVE_MTF_POLICY
if policy.buy_interval not in frames or policy.sell_interval not in frames:
return None
states = {iv: get_latest_bb_state(frames[iv], cfg) for iv in frames}
trend = get_trend(df_1d, df_1h)
sell_df = prepare_entry_df(frames[policy.sell_interval])
sell_close = float(sell_df["Close"].iloc[-1])
sell_state = states[policy.sell_interval]
if cfg.use_stop_loss and sell_state == STATE_CROSS_DOWN_LOWER:
if not policy.sell_confirm_intervals or _confirm_sell(states, policy):
return TradeSignal("sell", SIGNAL_SELL_STOP, sell_close, trend)
if sell_state == STATE_CROSS_UP_UPPER:
if not policy.sell_confirm_intervals or _confirm_sell(states, policy):
return TradeSignal("sell", SIGNAL_SELL_UPPER, sell_close, trend)
buy_state = states[policy.buy_interval]
if buy_state != STATE_CROSS_UP_LOWER:
return None
if not _allow_buy(trend, cfg):
return None
if not _confirm_buy(states, policy):
return None
buy_close = float(frames[policy.buy_interval]["Close"].iloc[-1])
sig = evaluate_interval_bb(symbol, frames[policy.buy_interval], cfg)
if sig and sig.action == "buy":
return TradeSignal("buy", sig.signal, buy_close, trend)
return None
def evaluate(
symbol: str,
df_entry: pd.DataFrame,
df_1h: pd.DataFrame,
df_1d: pd.DataFrame,
config: StrategyConfig | None = None,
trend_override: Trend | None = None,
frames: dict[int, pd.DataFrame] | None = None,
policy: MtfBbPolicy | None = None,
) -> TradeSignal | None:
"""
매매 신호 1건.
frames 가 있으면 evaluate_mtf_bb, 없으면 df_entry 단일 봉 BB.
"""
if frames:
return evaluate_mtf_bb(symbol, frames, df_1d, df_1h, policy, config)
cfg = config or ACTIVE_CONFIG
trend = trend_override if trend_override else get_trend(df_1d, df_1h)
sig = evaluate_interval_bb(symbol, df_entry, cfg)
if sig is None:
return None
if sig.action == "buy" and not _allow_buy(trend, cfg):
return None
return TradeSignal(sig.action, sig.signal, sig.close, trend)
def annotate_interval_signals(
symbol: str,
data: pd.DataFrame,
simulation: bool | None = None,
config: StrategyConfig | None = None,
) -> pd.DataFrame:
"""단일 봉 간격 전체에 BB 신호 기록 (봉별 백테스트용)."""
cfg = config or ACTIVE_CONFIG
df = prepare_entry_df(data)
df["signal"] = ""
df["point"] = 0
df["action"] = ""
df["bb_state"] = ""
for i in range(21, len(df)):
st = classify_bb_state_at(df, i, cfg)
df.at[df.index[i], "bb_state"] = st
sig = evaluate_interval_bb(symbol, df.iloc[: i + 1], cfg)
if sig:
df.at[df.index[i], "signal"] = sig.signal
df.at[df.index[i], "point"] = 1
df.at[df.index[i], "action"] = sig.action
if not simulation:
sig = evaluate_interval_bb(symbol, df, cfg)
if sig:
df.at[df.index[-1], "signal"] = sig.signal
df.at[df.index[-1], "point"] = 1
df.at[df.index[-1], "action"] = sig.action
df.at[df.index[-1], "bb_state"] = get_latest_bb_state(df, cfg)
return df
def _slice_frames_at(frames: dict[int, pd.DataFrame], ts: pd.Timestamp) -> dict[int, pd.DataFrame]:
"""시점 ts 이하 봉만 남긴 frames."""
out: dict[int, pd.DataFrame] = {}
for iv, df in frames.items():
part = df[df.index <= ts]
if len(part) >= 22:
out[iv] = part
return out
def annotate_mtf_signals(
symbol: str,
frames: dict[int, pd.DataFrame],
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
policy: MtfBbPolicy | None = None,
config: StrategyConfig | None = None,
) -> pd.DataFrame:
"""실행 봉(buy_interval) 타임라인에 MTF BB 신호 기록."""
policy = policy or ACTIVE_MTF_POLICY
cfg = config or ACTIVE_CONFIG
if policy.buy_interval not in frames:
raise ValueError(f"buy_interval {policy.buy_interval} 데이터 없음")
df = prepare_entry_df(frames[policy.buy_interval].copy())
df["signal"] = ""
df["point"] = 0
df["action"] = ""
df["trend"] = ""
df["bb_state"] = ""
for i in range(21, len(df)):
ts = df.index[i]
sliced = _slice_frames_at(frames, ts)
if policy.buy_interval not in sliced:
continue
df.at[ts, "bb_state"] = get_latest_bb_state(sliced[policy.buy_interval], cfg)
trend_at = get_trend_at(df_1d, df_1h, ts)
sig = evaluate_mtf_bb(symbol, sliced, df_1d, df_1h, policy, cfg)
if sig:
df.at[ts, "signal"] = sig.signal
df.at[ts, "point"] = 1
df.at[ts, "action"] = sig.action
df.at[ts, "trend"] = sig.trend
return df
def evaluate_discovered_live(
symbol: str,
frames: dict[int, pd.DataFrame],
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
balances: dict,
) -> TradeSignal | None:
"""
최신 3분 봉 시점에서 discovered_rules + 전 봉 BB·일목 조합으로 신호 1건.
"""
from candle_features import build_master_feature_matrix
from rule_discovery import (
_trigger_at,
buy_mask,
load_rules,
rules_have_buy,
sell_mask,
)
rules = load_rules()
if rules is None or not rules_have_buy(rules):
return None
matrix = build_master_feature_matrix(frames)
if len(matrix) < 23:
return None
tail = matrix.iloc[-2:]
i = 1
ts = tail.index[-1]
close = float(tail["Close"].iloc[-1])
trend = get_trend_at(df_1d, df_1h, ts)
b_m = buy_mask(tail, rules)
s_m = sell_mask(tail, rules, stop=False)
st_m = (
sell_mask(tail, rules, stop=True)
if rules.sell_stop
else np.zeros(len(tail), dtype=bool)
)
position = float(balances.get(symbol, {}).get("balance", 0) or 0)
if position >= 1.0:
if rules.sell_stop and _trigger_at(st_m, i):
return TradeSignal("sell", SIGNAL_SELL_STOP, close, trend)
if _trigger_at(s_m, i):
return TradeSignal("sell", SIGNAL_SELL_UPPER, close, trend)
return None
if _trigger_at(b_m, i):
return TradeSignal("buy", SIGNAL_BUY_LOWER, close, trend)
return None
def annotate_discovered_signals(
symbol: str,
frames: dict[int, pd.DataFrame],
df_1d: pd.DataFrame,
df_1h: pd.DataFrame,
rules=None,
data: pd.DataFrame | None = None,
) -> pd.DataFrame:
"""탐색된 다봉·캔들 규칙으로 3분 타임라인(전체 봉)에 신호 기록."""
from candle_features import build_master_feature_matrix
from rule_discovery import generate_trade_events, load_rules, rules_have_buy
rule_set = rules or load_rules()
if rule_set is None or not rules_have_buy(rule_set):
raise FileNotFoundError(
"discovered_rules.json 없거나 매수 규칙이 비어 있습니다. "
"python simulation.py 실행"
)
entry = frames.get(ENTRY_INTERVAL)
if entry is None or entry.empty:
raise ValueError("3분봉 데이터가 없습니다.")
matrix = build_master_feature_matrix(frames).iloc[21:].copy()
df = prepare_entry_df(data if data is not None else entry)
df["signal"] = ""
df["point"] = 0
df["action"] = ""
df["trend"] = ""
for ts, action, sig in generate_trade_events(matrix, rule_set):
if ts not in df.index:
continue
trend_at = get_trend_at(df_1d, df_1h, ts)
df.at[ts, "signal"] = sig
df.at[ts, "point"] = 1
df.at[ts, "action"] = action
df.at[ts, "trend"] = trend_at
return df
def annotate_signals(
symbol: str,
data: pd.DataFrame,
simulation: bool | None = None,
df_1h: pd.DataFrame | None = None,
df_1d: pd.DataFrame | None = None,
config: StrategyConfig | None = None,
frames: dict[int, pd.DataFrame] | None = None,
) -> pd.DataFrame:
"""3분봉 구간 전체에 signal/point/action/trend 기록."""
cfg = config or ACTIVE_CONFIG
if cfg.use_discovered_rules and frames:
h1d = df_1d if df_1d is not None and not df_1d.empty else data
h1h = df_1h if df_1h is not None and not df_1h.empty else data
return annotate_discovered_signals(symbol, frames, h1d, h1h, data=data)
df = prepare_entry_df(data)
df["signal"] = ""
df["point"] = 0
df["action"] = ""
df["trend"] = ""
htf_1h = df_1h if df_1h is not None else df
htf_1d = df_1d if df_1d is not None else df
for i in range(21, len(df)):
ts = df.index[i]
trend_at = get_trend_at(htf_1d, htf_1h, ts) if simulation else get_trend(htf_1d, htf_1h)
sig = evaluate(
symbol,
df.iloc[: i + 1],
htf_1h,
htf_1d,
config=cfg,
trend_override=trend_at if simulation else None,
)
if sig:
df.at[df.index[i], "signal"] = sig.signal
df.at[df.index[i], "point"] = 1
df.at[df.index[i], "action"] = sig.action
df.at[df.index[i], "trend"] = sig.trend
if not simulation:
live = evaluate(symbol, df, htf_1h, htf_1d, config=cfg)
if live:
df.at[df.index[-1], "signal"] = live.signal
df.at[df.index[-1], "point"] = 1
df.at[df.index[-1], "action"] = live.action
df.at[df.index[-1], "trend"] = live.trend
return df
def comparison_presets() -> list[StrategyConfig]:
"""기법 조합 비교용 프리셋."""
return [
StrategyConfig(name="01_기본_BB만", use_mtf=False, use_regime_switch=False,
use_rsi_filter=False, use_squeeze_filter=False, use_stop_loss=False),
StrategyConfig(name="02_기본+손절", use_mtf=False, use_regime_switch=False,
use_rsi_filter=False, use_squeeze_filter=False, use_stop_loss=True),
StrategyConfig(name="03_기본+MTF", use_mtf=True, use_regime_switch=False,
use_rsi_filter=False, use_squeeze_filter=False, use_stop_loss=True),
StrategyConfig(name="04_기본+MTF+스퀴즈", use_mtf=True, use_regime_switch=False,
use_rsi_filter=False, use_squeeze_filter=True, use_stop_loss=True),
StrategyConfig(name="05_기본+MTF+RSI", use_mtf=True, use_regime_switch=False,
use_rsi_filter=True, use_squeeze_filter=False, use_stop_loss=True),
StrategyConfig(name="06_기본+MTF+거래량", use_mtf=True, use_regime_switch=False,
use_rsi_filter=False, use_volume_filter=True, volume_ratio=1.1,
use_squeeze_filter=False, use_stop_loss=True),
StrategyConfig(name="07_레짐스위치", use_mtf=True, use_regime_switch=True,
use_rsi_filter=False, use_squeeze_filter=False, use_stop_loss=True),
StrategyConfig(name="08_레짐+RSI+스퀴즈", use_mtf=True, use_regime_switch=True,
use_rsi_filter=True, use_squeeze_filter=True, use_stop_loss=True),
StrategyConfig(name="09_풀필터", use_mtf=True, use_regime_switch=True,
use_rsi_filter=True, use_volume_filter=True, volume_ratio=1.1,
use_squeeze_filter=True, use_stop_loss=True),
]
def get_signal_action(signal: str) -> Action | None:
if signal in BUY_SIGNALS:
return "buy"
if signal in SELL_SIGNALS:
return "sell"
return None
def get_buy_amount(symbol: str, signal: str, close: float, trend: Trend = "up") -> int:
if trend == "range":
return RANGE_BUY_KRW
return DEFAULT_BUY_KRW
def get_sell_ratio(symbol: str, signal: str) -> float:
return 1.0
def allowed_inverse_sell_signals() -> set[str]:
return set()
def should_double_buy(symbol: str, signal: str, data: pd.DataFrame) -> bool:
return False