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:
36
.env.example
36
.env.example
@@ -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
8
.gitignore
vendored
@@ -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
104
README.md
@@ -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 스냅샷 |
|
||||||
|
|
||||||
## 면책
|
## 면책
|
||||||
|
|
||||||
실거래 손실 책임은 사용자에게 있습니다.
|
실거래는 사용자 책임입니다. 본 저장소는 주문 실행을 포함하지 않습니다.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
300
config.py
@@ -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")
|
||||||
|
|||||||
5442
data/ground_truth/ground_truth_trades.json
Normal file
5442
data/ground_truth/ground_truth_trades.json
Normal file
File diff suppressed because it is too large
Load Diff
0
data/ops/.gitkeep
Normal file
0
data/ops/.gitkeep
Normal file
14
deepcoin/__init__.py
Normal file
14
deepcoin/__init__.py
Normal 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()
|
||||||
6
deepcoin/analysis/README.md
Normal file
6
deepcoin/analysis/README.md
Normal 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`만 사용합니다.
|
||||||
0
deepcoin/analysis/__init__.py
Normal file
0
deepcoin/analysis/__init__.py
Normal file
153
deepcoin/analysis/general_analysis_align.py
Normal file
153
deepcoin/analysis/general_analysis_align.py
Normal 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"]
|
||||||
114
deepcoin/analysis/general_analysis_candles.py
Normal file
114
deepcoin/analysis/general_analysis_candles.py
Normal 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",
|
||||||
|
]
|
||||||
159
deepcoin/analysis/general_analysis_chart.py
Normal file
159
deepcoin/analysis/general_analysis_chart.py
Normal 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",
|
||||||
|
]
|
||||||
26
deepcoin/analysis/general_analysis_config.py
Normal file
26
deepcoin/analysis/general_analysis_config.py
Normal 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)
|
||||||
98
deepcoin/analysis/general_analysis_context.py
Normal file
98
deepcoin/analysis/general_analysis_context.py
Normal 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
|
||||||
92
deepcoin/analysis/general_analysis_core.py
Normal file
92
deepcoin/analysis/general_analysis_core.py
Normal 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
|
||||||
|
}
|
||||||
148
deepcoin/analysis/general_analysis_enrich_runner.py
Normal file
148
deepcoin/analysis/general_analysis_enrich_runner.py
Normal 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()
|
||||||
72
deepcoin/analysis/general_analysis_harmonic.py
Normal file
72
deepcoin/analysis/general_analysis_harmonic.py
Normal 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"]
|
||||||
382
deepcoin/analysis/general_analysis_indicators.py
Normal file
382
deepcoin/analysis/general_analysis_indicators.py
Normal 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",
|
||||||
|
]
|
||||||
302
deepcoin/analysis/general_analysis_patterns.py
Normal file
302
deepcoin/analysis/general_analysis_patterns.py
Normal 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
|
||||||
154
deepcoin/analysis/general_analysis_pipeline.py
Normal file
154
deepcoin/analysis/general_analysis_pipeline.py
Normal 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"]
|
||||||
73
deepcoin/analysis/general_analysis_report.py
Normal file
73
deepcoin/analysis/general_analysis_report.py
Normal 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
|
||||||
71
deepcoin/analysis/general_analysis_runner.py
Normal file
71
deepcoin/analysis/general_analysis_runner.py
Normal 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()
|
||||||
100
deepcoin/analysis/general_analysis_snapshot.py
Normal file
100
deepcoin/analysis/general_analysis_snapshot.py
Normal 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
|
||||||
92
deepcoin/analysis/general_analysis_volume.py
Normal file
92
deepcoin/analysis/general_analysis_volume.py
Normal 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()}
|
||||||
205
deepcoin/analysis/general_analysis_wave.py
Normal file
205
deepcoin/analysis/general_analysis_wave.py
Normal 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
5
deepcoin/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""외부 API 연동 (빗썸 등)."""
|
||||||
|
|
||||||
|
from deepcoin.api.bithumb import HTS
|
||||||
|
|
||||||
|
__all__ = ["HTS"]
|
||||||
@@ -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()
|
||||||
0
deepcoin/common/__init__.py
Normal file
0
deepcoin/common/__init__.py
Normal 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"]
|
||||||
315
deepcoin/common/indicators.py
Normal file
315
deepcoin/common/indicators.py
Normal 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:
|
||||||
|
"""
|
||||||
|
스토캐스틱 %K·%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"
|
||||||
0
deepcoin/data/__init__.py
Normal file
0
deepcoin/data/__init__.py
Normal 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
47
deepcoin/data/mtf_bb.py
Normal 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
53
deepcoin/env_loader.py
Normal 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,
|
||||||
|
}
|
||||||
0
deepcoin/ground_truth/__init__.py
Normal file
0
deepcoin/ground_truth/__init__.py
Normal file
1165
deepcoin/ground_truth/ground_truth.py
Normal file
1165
deepcoin/ground_truth/ground_truth.py
Normal file
File diff suppressed because it is too large
Load Diff
15
deepcoin/matching/README.md
Normal file
15
deepcoin/matching/README.md
Normal 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
|
||||||
|
```
|
||||||
3
deepcoin/matching/__init__.py
Normal file
3
deepcoin/matching/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
04단계: Ground Truth에 근접한 기술적 상태·규칙 선택 (예정).
|
||||||
|
"""
|
||||||
31
deepcoin/matching/match_rules.py
Normal file
31
deepcoin/matching/match_rules.py
Normal 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
0
deepcoin/ops/__init__.py
Normal 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)
|
||||||
34
deepcoin/ops/monitor_coin.py
Normal file
34
deepcoin/ops/monitor_coin.py
Normal 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
595
deepcoin/ops/simulation.py
Normal 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
95
deepcoin/paths.py
Normal 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)
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
12
docs/README.md
Normal 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)
|
||||||
33
docs/reference/GROUND_TRUTH.md
Normal file
33
docs/reference/GROUND_TRUTH.md
Normal 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
29
docs/reference/ROADMAP.md
Normal 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 패키지
|
||||||
|
```
|
||||||
52
docs/reference/STRUCTURE.md
Normal file
52
docs/reference/STRUCTURE.md
Normal 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/*`로 연결되는 별칭입니다.
|
||||||
394
docs/reference/trade_anaysis.html
Normal file
394
docs/reference/trade_anaysis.html
Normal 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 & 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&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&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>
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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": "반등 첫 저항·전일 하락 중충 돌파 실패 구간"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
222
mtf_bb.py
@@ -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}")
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"buy_interval": 60,
|
|
||||||
"sell_interval": 60,
|
|
||||||
"buy_confirm_intervals": [
|
|
||||||
1440
|
|
||||||
],
|
|
||||||
"sell_confirm_intervals": [
|
|
||||||
1440
|
|
||||||
],
|
|
||||||
"name": "auto_60분_buy_60분_sell"
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
11
scripts/01_download.py
Normal 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()
|
||||||
11
scripts/02_ground_truth.py
Normal file
11
scripts/02_ground_truth.py
Normal 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()
|
||||||
11
scripts/03_analyze_enrich.py
Normal file
11
scripts/03_analyze_enrich.py
Normal 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()
|
||||||
11
scripts/03_analyze_trades.py
Normal file
11
scripts/03_analyze_trades.py
Normal 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
11
scripts/04_match_rules.py
Normal 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
13
scripts/05_chart_bb.py
Normal 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
13
scripts/05_chart_truth.py
Normal 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
11
scripts/05_run_monitor.py
Normal 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
17
scripts/README.md
Normal 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
11
scripts/_bootstrap.py
Normal 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
219
scripts/verify_env.py
Normal 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())
|
||||||
1095
simulation.py
1095
simulation.py
File diff suppressed because it is too large
Load Diff
677
strategy.py
677
strategy.py
@@ -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
|
|
||||||
Reference in New Issue
Block a user