로고스 전략 FSM을 simulation 기본 실행에 통합한다.
수동 타점(logos_trades.json) 흐름에 맞춘 순차 매매 로직을 추가하고, python simulation.py 실행 시 로고스 백테스트·HTML을 생성한다. 규칙 탐색·BB 안전장치 개선과 함께 reports HTML은 gitignore로 제외한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
12
.env.example
12
.env.example
@@ -6,9 +6,15 @@ BITHUMB_SECRET_KEY=
|
|||||||
COIN_TELEGRAM_BOT_TOKEN=
|
COIN_TELEGRAM_BOT_TOKEN=
|
||||||
COIN_TELEGRAM_CHAT_ID=
|
COIN_TELEGRAM_CHAT_ID=
|
||||||
|
|
||||||
# 쿨다운(초) — 3분 전략
|
# 쿨다운(초) — 3분 전략 (빈번 체결 완화)
|
||||||
BUY_COOLDOWN_SEC=300
|
BUY_COOLDOWN_SEC=1800
|
||||||
SELL_COOLDOWN_SEC=180
|
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)
|
# 매수 금액(KRW)
|
||||||
DEFAULT_BUY_KRW=30000
|
DEFAULT_BUY_KRW=30000
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -86,6 +86,9 @@ celerybeat-schedule
|
|||||||
# dotenv
|
# dotenv
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# 백테스트·시뮬레이션 HTML (로컬 재생성)
|
||||||
|
reports/
|
||||||
|
|
||||||
# virtualenv
|
# virtualenv
|
||||||
.venv
|
.venv
|
||||||
venv/
|
venv/
|
||||||
|
|||||||
331
combination_report.json
Normal file
331
combination_report.json
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
22
config.py
22
config.py
@@ -27,11 +27,27 @@ KR_COINS: dict[str, str] = {
|
|||||||
TREND_INTERVAL_1H = 60
|
TREND_INTERVAL_1H = 60
|
||||||
TREND_INTERVAL_1D = 1440
|
TREND_INTERVAL_1D = 1440
|
||||||
|
|
||||||
# --- 쿨다운(초) ---
|
# --- 쿨다운(초) — 3분봉: 기본 30분/15분 (빈번 체결 완화) ---
|
||||||
BUY_COOLDOWN_SEC = int(os.getenv("BUY_COOLDOWN_SEC", "300"))
|
BUY_COOLDOWN_SEC = int(os.getenv("BUY_COOLDOWN_SEC", "1800"))
|
||||||
SELL_COOLDOWN_SEC = int(os.getenv("SELL_COOLDOWN_SEC", "180"))
|
SELL_COOLDOWN_SEC = int(os.getenv("SELL_COOLDOWN_SEC", "900"))
|
||||||
BUY_MINUTE_LIMIT = BUY_COOLDOWN_SEC
|
BUY_MINUTE_LIMIT = BUY_COOLDOWN_SEC
|
||||||
|
|
||||||
|
# 매수·매도 신호는 조건이 False→True로 바뀐 봉에서만 (연속 참 방지)
|
||||||
|
SIGNAL_EDGE_ONLY = os.getenv("SIGNAL_EDGE_ONLY", "true").lower() in ("1", "true", "yes")
|
||||||
|
|
||||||
|
# 체결(매수·매도 공통) 후 최소 대기 봉 수 (3분봉 5봉 = 15분)
|
||||||
|
TRADE_MIN_GAP_BARS = int(os.getenv("TRADE_MIN_GAP_BARS", "5"))
|
||||||
|
|
||||||
|
# 규칙 탐색 시 거래 횟수 패널티 (학습 구간)
|
||||||
|
DISCOVER_MAX_TRADES = int(os.getenv("DISCOVER_MAX_TRADES", "120"))
|
||||||
|
DISCOVER_TRADE_PENALTY_PCT = float(os.getenv("DISCOVER_TRADE_PENALTY_PCT", "0.03"))
|
||||||
|
|
||||||
|
# 3분 BB 위치: 이 값 미만에서 상단돌파 매도 차단 (저점 익절 방지)
|
||||||
|
SELL_MIN_BB_POS = float(os.getenv("SELL_MIN_BB_POS", "0.4"))
|
||||||
|
|
||||||
|
# 3분 BB 위치: 이 값 이상이면 단독 상단구간 매수 차단 (고점 추격 방지)
|
||||||
|
BUY_MAX_BB_POS_CHASE = float(os.getenv("BUY_MAX_BB_POS_CHASE", "0.55"))
|
||||||
|
|
||||||
# --- 볼린저 (3분봉, 20, 2σ) ---
|
# --- 볼린저 (3분봉, 20, 2σ) ---
|
||||||
BB_PERIOD = 20
|
BB_PERIOD = 20
|
||||||
BB_STD = 2
|
BB_STD = 2
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "discovered_best",
|
"name": "discovered_best",
|
||||||
"buy_all": [
|
"buy_all": [
|
||||||
"m240:above_upper",
|
"m3:cross_up_lower",
|
||||||
"m3:bb_zone_bottom"
|
"m60:ichi_tk_bull",
|
||||||
|
"d1:!ichi_below_cloud",
|
||||||
|
"m3:bb_zone_low",
|
||||||
|
"m3:ichi_above_cloud"
|
||||||
],
|
],
|
||||||
"buy_any": [],
|
"buy_any": [],
|
||||||
"sell_all": [
|
"sell_all": [
|
||||||
"m3:cross_up_upper",
|
"m60:cross_up_upper"
|
||||||
"m3:ichi_above_cloud",
|
|
||||||
"m3:ichi_cloud_bull",
|
|
||||||
"m3:ichi_tk_cross_up"
|
|
||||||
],
|
],
|
||||||
"sell_stop": [],
|
"sell_stop": [
|
||||||
"train_return_pct": 2.9835000000000003,
|
"m3:cross_down_lower"
|
||||||
"test_return_pct": 1.45272321428571,
|
],
|
||||||
"full_return_pct": 4.43622321428571,
|
"train_return_pct": 0.7385912698412721,
|
||||||
"trade_count": 3
|
"test_return_pct": 0.0,
|
||||||
|
"full_return_pct": 0.7385912698412721,
|
||||||
|
"trade_count": 2
|
||||||
}
|
}
|
||||||
223
docs/LOGOS_STRATEGY.md
Normal file
223
docs/LOGOS_STRATEGY.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# 로고스(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 |
|
||||||
18
logos_chart.py
Normal file
18
logos_chart.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/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()
|
||||||
349
logos_strategy.py
Normal file
349
logos_strategy.py
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
"""
|
||||||
|
로고스(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
|
||||||
57
logos_trades.json
Normal file
57
logos_trades.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"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": "반등 첫 저항·전일 하락 중충 돌파 실패 구간"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -21,12 +21,18 @@ from candle_features import (
|
|||||||
)
|
)
|
||||||
from config import (
|
from config import (
|
||||||
BUY_COOLDOWN_SEC,
|
BUY_COOLDOWN_SEC,
|
||||||
|
BUY_MAX_BB_POS_CHASE,
|
||||||
|
DISCOVER_MAX_TRADES,
|
||||||
|
DISCOVER_TRADE_PENALTY_PCT,
|
||||||
DOWNLOAD_INTERVALS,
|
DOWNLOAD_INTERVALS,
|
||||||
ENTRY_INTERVAL,
|
ENTRY_INTERVAL,
|
||||||
SELL_COOLDOWN_SEC,
|
SELL_COOLDOWN_SEC,
|
||||||
|
SELL_MIN_BB_POS,
|
||||||
|
SIGNAL_EDGE_ONLY,
|
||||||
SIM_INITIAL_CASH_KRW,
|
SIM_INITIAL_CASH_KRW,
|
||||||
SIM_MIN_ORDER_KRW,
|
SIM_MIN_ORDER_KRW,
|
||||||
SYMBOL,
|
SYMBOL,
|
||||||
|
TRADE_MIN_GAP_BARS,
|
||||||
TRADING_FEE_RATE,
|
TRADING_FEE_RATE,
|
||||||
)
|
)
|
||||||
from strategy import (
|
from strategy import (
|
||||||
@@ -59,6 +65,19 @@ BUY_SAFETY_BLOCK: tuple[str, ...] = (
|
|||||||
"m10:cross_up_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
|
@dataclass
|
||||||
class DiscoveredRules:
|
class DiscoveredRules:
|
||||||
@@ -87,6 +106,104 @@ def predicate_column(key: str) -> tuple[str, bool]:
|
|||||||
return f"{prefix}_{feat}", neg
|
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:
|
def _mask_for_keys(matrix: pd.DataFrame, keys: list[str]) -> np.ndarray:
|
||||||
"""AND 조건 마스크."""
|
"""AND 조건 마스크."""
|
||||||
n = len(matrix)
|
n = len(matrix)
|
||||||
@@ -134,9 +251,59 @@ def _unsafe_buy_mask(matrix: pd.DataFrame) -> np.ndarray:
|
|||||||
unsafe |= (
|
unsafe |= (
|
||||||
matrix["m30_hammer"].fillna(0).astype(bool) & near_peak.fillna(False)
|
matrix["m30_hammer"].fillna(0).astype(bool) & near_peak.fillna(False)
|
||||||
).to_numpy()
|
).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
|
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:
|
def buy_mask(matrix: pd.DataFrame, rules: DiscoveredRules) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
매수 마스크 = (buy_all) 또는 (buy_any 각 그룹의 AND) 중 하나 + 안전필터.
|
매수 마스크 = (buy_all) 또는 (buy_any 각 그룹의 AND) 중 하나 + 안전필터.
|
||||||
@@ -154,13 +321,34 @@ def buy_mask(matrix: pd.DataFrame, rules: DiscoveredRules) -> np.ndarray:
|
|||||||
return np.zeros(n, dtype=bool)
|
return np.zeros(n, dtype=bool)
|
||||||
any_ok = np.zeros(n, dtype=bool)
|
any_ok = np.zeros(n, dtype=bool)
|
||||||
for group in groups:
|
for group in groups:
|
||||||
any_ok |= _mask_for_keys(matrix, group)
|
raw = _mask_for_keys(matrix, group)
|
||||||
|
any_ok |= raw & _value_buy_gate_mask(matrix, group)
|
||||||
return any_ok & ~_unsafe_buy_mask(matrix)
|
return any_ok & ~_unsafe_buy_mask(matrix)
|
||||||
|
|
||||||
|
|
||||||
def sell_mask(matrix: pd.DataFrame, rules: DiscoveredRules, stop: bool = False) -> np.ndarray:
|
def sell_mask(matrix: pd.DataFrame, rules: DiscoveredRules, stop: bool = False) -> np.ndarray:
|
||||||
keys = rules.sell_stop if stop else rules.sell_all
|
keys = rules.sell_stop if stop else rules.sell_all
|
||||||
return _mask_for_keys(matrix, keys)
|
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]:
|
def generate_predicate_pool(intervals: list[int]) -> list[str]:
|
||||||
@@ -176,6 +364,35 @@ def generate_predicate_pool(intervals: list[int]) -> list[str]:
|
|||||||
return pool
|
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(
|
def generate_trade_events(
|
||||||
matrix: pd.DataFrame,
|
matrix: pd.DataFrame,
|
||||||
rules: DiscoveredRules,
|
rules: DiscoveredRules,
|
||||||
@@ -200,6 +417,7 @@ def generate_trade_events(
|
|||||||
qty = 0.0
|
qty = 0.0
|
||||||
last_buy_i: int | None = None
|
last_buy_i: int | None = None
|
||||||
last_sell_i: int | None = None
|
last_sell_i: int | None = None
|
||||||
|
last_trade_i: int | None = None
|
||||||
|
|
||||||
for i in range(len(matrix)):
|
for i in range(len(matrix)):
|
||||||
price = close[i]
|
price = close[i]
|
||||||
@@ -207,9 +425,12 @@ def generate_trade_events(
|
|||||||
continue
|
continue
|
||||||
ts = idx[i]
|
ts = idx[i]
|
||||||
|
|
||||||
|
if last_trade_i is not None and i - last_trade_i < TRADE_MIN_GAP_BARS:
|
||||||
|
continue
|
||||||
|
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
is_stop = bool(stop_mask[i])
|
is_stop = _trigger_at(stop_mask, i) if rules.sell_stop else False
|
||||||
is_sell = bool(s_mask[i])
|
is_sell = _trigger_at(s_mask, i)
|
||||||
if is_stop or is_sell:
|
if is_stop or is_sell:
|
||||||
if last_sell_i is not None:
|
if last_sell_i is not None:
|
||||||
if (ts - idx[last_sell_i]).total_seconds() < SELL_COOLDOWN_SEC:
|
if (ts - idx[last_sell_i]).total_seconds() < SELL_COOLDOWN_SEC:
|
||||||
@@ -218,15 +439,17 @@ def generate_trade_events(
|
|||||||
events.append((ts, "sell", sig))
|
events.append((ts, "sell", sig))
|
||||||
qty = 0.0
|
qty = 0.0
|
||||||
last_sell_i = i
|
last_sell_i = i
|
||||||
|
last_trade_i = i
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if b_mask[i] and qty <= 0:
|
if _trigger_at(b_mask, i) and qty <= 0:
|
||||||
if last_buy_i is not None:
|
if last_buy_i is not None:
|
||||||
if (ts - idx[last_buy_i]).total_seconds() < BUY_COOLDOWN_SEC:
|
if (ts - idx[last_buy_i]).total_seconds() < BUY_COOLDOWN_SEC:
|
||||||
continue
|
continue
|
||||||
events.append((ts, "buy", SIGNAL_BUY_LOWER))
|
events.append((ts, "buy", SIGNAL_BUY_LOWER))
|
||||||
qty = 1.0
|
qty = 1.0
|
||||||
last_buy_i = i
|
last_buy_i = i
|
||||||
|
last_trade_i = i
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
@@ -263,6 +486,18 @@ def backtest_rules(
|
|||||||
return res.total_return_pct, res.trade_count
|
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:
|
def _baseline_rules() -> DiscoveredRules:
|
||||||
"""다봉 BB 하단 돌파 + 상단 돌파 기준선."""
|
"""다봉 BB 하단 돌파 + 상단 돌파 기준선."""
|
||||||
p3 = interval_prefix(ENTRY_INTERVAL)
|
p3 = interval_prefix(ENTRY_INTERVAL)
|
||||||
@@ -321,13 +556,22 @@ def greedy_search(
|
|||||||
sell_all=list(seed.sell_all),
|
sell_all=list(seed.sell_all),
|
||||||
sell_stop=list(seed.sell_stop),
|
sell_stop=list(seed.sell_stop),
|
||||||
)
|
)
|
||||||
best_ret, _ = backtest_rules(train, best, df_1d, df_1h, entry_ohlc)
|
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
|
improved = True
|
||||||
while improved:
|
while improved:
|
||||||
improved = False
|
improved = False
|
||||||
# 매수 AND 추가/제거
|
# 매수 AND 추가/제거
|
||||||
for pred in pool:
|
for pred in buy_pool:
|
||||||
if pred in best.buy_all:
|
if pred in best.buy_all:
|
||||||
trial_all = [p for p in best.buy_all if p != pred]
|
trial_all = [p for p in best.buy_all if p != pred]
|
||||||
else:
|
else:
|
||||||
@@ -341,14 +585,16 @@ def greedy_search(
|
|||||||
sell_all=best.sell_all,
|
sell_all=best.sell_all,
|
||||||
sell_stop=best.sell_stop,
|
sell_stop=best.sell_stop,
|
||||||
)
|
)
|
||||||
ret, _ = backtest_rules(train, trial, df_1d, df_1h, entry_ohlc)
|
ret, tc, score = _evaluate_train(
|
||||||
if ret > best_ret:
|
train, trial, df_1d, df_1h, entry_ohlc
|
||||||
best_ret = ret
|
)
|
||||||
|
if score > best_score:
|
||||||
|
best_ret, best_tc, best_score = ret, tc, score
|
||||||
best.buy_all = trial_all
|
best.buy_all = trial_all
|
||||||
improved = True
|
improved = True
|
||||||
|
|
||||||
# 매도 AND
|
# 매도 AND
|
||||||
for pred in pool:
|
for pred in sell_pool:
|
||||||
if pred in best.sell_all:
|
if pred in best.sell_all:
|
||||||
trial_s = [p for p in best.sell_all if p != pred]
|
trial_s = [p for p in best.sell_all if p != pred]
|
||||||
else:
|
else:
|
||||||
@@ -362,14 +608,21 @@ def greedy_search(
|
|||||||
sell_all=trial_s,
|
sell_all=trial_s,
|
||||||
sell_stop=best.sell_stop,
|
sell_stop=best.sell_stop,
|
||||||
)
|
)
|
||||||
ret, _ = backtest_rules(train, trial, df_1d, df_1h, entry_ohlc)
|
ret, tc, score = _evaluate_train(
|
||||||
if ret > best_ret:
|
train, trial, df_1d, df_1h, entry_ohlc
|
||||||
best_ret = ret
|
)
|
||||||
|
if score > best_score:
|
||||||
|
best_ret, best_tc, best_score = ret, tc, score
|
||||||
best.sell_all = trial_s
|
best.sell_all = trial_s
|
||||||
improved = True
|
improved = True
|
||||||
|
|
||||||
# 손절
|
# 손절
|
||||||
stop_pool = [p for p in pool if "cross_down_lower" in p or "below_lower" in p]
|
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:
|
for pred in stop_pool:
|
||||||
if pred in best.sell_stop:
|
if pred in best.sell_stop:
|
||||||
trial_st = [p for p in best.sell_stop if p != pred]
|
trial_st = [p for p in best.sell_stop if p != pred]
|
||||||
@@ -384,13 +637,15 @@ def greedy_search(
|
|||||||
sell_all=best.sell_all,
|
sell_all=best.sell_all,
|
||||||
sell_stop=trial_st,
|
sell_stop=trial_st,
|
||||||
)
|
)
|
||||||
ret, _ = backtest_rules(train, trial, df_1d, df_1h, entry_ohlc)
|
ret, tc, score = _evaluate_train(
|
||||||
if ret > best_ret:
|
train, trial, df_1d, df_1h, entry_ohlc
|
||||||
best_ret = ret
|
)
|
||||||
|
if score > best_score:
|
||||||
|
best_ret, best_tc, best_score = ret, tc, score
|
||||||
best.sell_stop = trial_st
|
best.sell_stop = trial_st
|
||||||
improved = True
|
improved = True
|
||||||
|
|
||||||
return best
|
return sanitize_rules(best)
|
||||||
|
|
||||||
|
|
||||||
def try_buy_any_branches(
|
def try_buy_any_branches(
|
||||||
@@ -420,7 +675,9 @@ def try_buy_any_branches(
|
|||||||
sell_all=list(base.sell_all),
|
sell_all=list(base.sell_all),
|
||||||
sell_stop=list(base.sell_stop),
|
sell_stop=list(base.sell_stop),
|
||||||
)
|
)
|
||||||
best_ret, _ = backtest_rules(train, best, df_1d, df_1h, entry_ohlc)
|
best_ret, best_tc, best_score = _evaluate_train(
|
||||||
|
train, best, df_1d, df_1h, entry_ohlc
|
||||||
|
)
|
||||||
|
|
||||||
for pred in triggers[:max_branches]:
|
for pred in triggers[:max_branches]:
|
||||||
if pred in best.buy_all:
|
if pred in best.buy_all:
|
||||||
@@ -434,13 +691,15 @@ def try_buy_any_branches(
|
|||||||
)
|
)
|
||||||
if not trial.buy_any[0]:
|
if not trial.buy_any[0]:
|
||||||
trial.buy_any = [[pred]]
|
trial.buy_any = [[pred]]
|
||||||
ret, _ = backtest_rules(train, trial, df_1d, df_1h, entry_ohlc)
|
ret, tc, score = _evaluate_train(
|
||||||
if ret > best_ret:
|
train, trial, df_1d, df_1h, entry_ohlc
|
||||||
best_ret = ret
|
)
|
||||||
best = trial
|
if score > best_score:
|
||||||
|
best_ret, best_score = ret, score
|
||||||
|
best = sanitize_rules(trial)
|
||||||
best.name = "discovered_or"
|
best.name = "discovered_or"
|
||||||
|
|
||||||
return best
|
return sanitize_rules(best)
|
||||||
|
|
||||||
|
|
||||||
def random_search_refine(
|
def random_search_refine(
|
||||||
@@ -456,8 +715,16 @@ def random_search_refine(
|
|||||||
"""무작위 변형으로 국소 최적 보완."""
|
"""무작위 변형으로 국소 최적 보완."""
|
||||||
train = matrix.iloc[:train_end]
|
train = matrix.iloc[:train_end]
|
||||||
best = seed
|
best = seed
|
||||||
best_ret, _ = backtest_rules(train, best, df_1d, df_1h, entry_ohlc)
|
best_ret, best_tc, best_score = _evaluate_train(
|
||||||
|
train, best, df_1d, df_1h, entry_ohlc
|
||||||
|
)
|
||||||
rng = random.Random(42)
|
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):
|
for _ in range(iterations):
|
||||||
trial = DiscoveredRules(
|
trial = DiscoveredRules(
|
||||||
@@ -468,27 +735,30 @@ def random_search_refine(
|
|||||||
sell_stop=[p for p in best.sell_stop],
|
sell_stop=[p for p in best.sell_stop],
|
||||||
)
|
)
|
||||||
action = rng.choice(["add_buy", "drop_buy", "add_sell", "drop_sell", "swap_buy"])
|
action = rng.choice(["add_buy", "drop_buy", "add_sell", "drop_sell", "swap_buy"])
|
||||||
if action == "add_buy" and len(trial.buy_all) < 6:
|
if action == "add_buy" and len(trial.buy_all) < 6 and buy_pool:
|
||||||
p = rng.choice(pool)
|
p = rng.choice(buy_pool)
|
||||||
if p not in trial.buy_all:
|
if p not in trial.buy_all:
|
||||||
trial.buy_all.append(p)
|
trial.buy_all.append(p)
|
||||||
elif action == "drop_buy" and trial.buy_all:
|
elif action == "drop_buy" and trial.buy_all:
|
||||||
trial.buy_all.pop(rng.randrange(len(trial.buy_all)))
|
trial.buy_all.pop(rng.randrange(len(trial.buy_all)))
|
||||||
elif action == "add_sell" and len(trial.sell_all) < 5:
|
elif action == "add_sell" and len(trial.sell_all) < 5 and sell_pool:
|
||||||
p = rng.choice(pool)
|
p = rng.choice(sell_pool)
|
||||||
if p not in trial.sell_all:
|
if p not in trial.sell_all:
|
||||||
trial.sell_all.append(p)
|
trial.sell_all.append(p)
|
||||||
elif action == "drop_sell" and trial.sell_all:
|
elif action == "drop_sell" and trial.sell_all:
|
||||||
trial.sell_all.pop(rng.randrange(len(trial.sell_all)))
|
trial.sell_all.pop(rng.randrange(len(trial.sell_all)))
|
||||||
elif action == "swap_buy" and pool:
|
elif action == "swap_buy" and buy_pool:
|
||||||
if trial.buy_all:
|
if trial.buy_all:
|
||||||
trial.buy_all[rng.randrange(len(trial.buy_all))] = rng.choice(pool)
|
trial.buy_all[rng.randrange(len(trial.buy_all))] = rng.choice(buy_pool)
|
||||||
ret, _ = backtest_rules(train, trial, df_1d, df_1h, entry_ohlc)
|
trial = sanitize_rules(trial)
|
||||||
if ret > best_ret:
|
ret, tc, score = _evaluate_train(
|
||||||
best_ret = ret
|
train, trial, df_1d, df_1h, entry_ohlc
|
||||||
|
)
|
||||||
|
if score > best_score:
|
||||||
|
best_ret, best_score = ret, score
|
||||||
best = trial
|
best = trial
|
||||||
best.name = "discovered_refined"
|
best.name = "discovered_refined"
|
||||||
return best
|
return sanitize_rules(best)
|
||||||
|
|
||||||
|
|
||||||
def discover_rules(frames: dict[int, pd.DataFrame]) -> DiscoveredRules:
|
def discover_rules(frames: dict[int, pd.DataFrame]) -> DiscoveredRules:
|
||||||
@@ -513,11 +783,13 @@ def discover_rules(frames: dict[int, pd.DataFrame]) -> DiscoveredRules:
|
|||||||
pool = generate_predicate_pool(intervals)
|
pool = generate_predicate_pool(intervals)
|
||||||
print(f" 샘플 {n}봉 | 학습 {train_end} | predicate 후보 {len(pool)}개")
|
print(f" 샘플 {n}봉 | 학습 {train_end} | predicate 후보 {len(pool)}개")
|
||||||
|
|
||||||
baseline = _seed_from_combination_report() or _baseline_rules()
|
baseline = _discovery_seed()
|
||||||
br, bt = backtest_rules(matrix.iloc[:train_end], baseline, df_1d, df_1h, entry_ohlc)
|
br, bt = backtest_rules(
|
||||||
print(f" 시드 규칙: {baseline.name}")
|
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)
|
bf, _ = backtest_rules(matrix, baseline, df_1d, df_1h, entry_ohlc)
|
||||||
print(f" 기준선(3분 BB만): 학습 {br:+.2f}% | 전체 {bf:+.2f}%")
|
print(f" 기준선: 학습 {br:+.2f}% | 전체 {bf:+.2f}%")
|
||||||
|
|
||||||
print("1단계: 탐욕적 AND 확장...")
|
print("1단계: 탐욕적 AND 확장...")
|
||||||
g1 = greedy_search(matrix, train_end, pool, baseline, df_1d, df_1h, entry_ohlc)
|
g1 = greedy_search(matrix, train_end, pool, baseline, df_1d, df_1h, entry_ohlc)
|
||||||
@@ -532,8 +804,13 @@ def discover_rules(frames: dict[int, pd.DataFrame]) -> DiscoveredRules:
|
|||||||
print("3단계: 무작위 정밀 탐색...")
|
print("3단계: 무작위 정밀 탐색...")
|
||||||
best = g2 if r2 >= r1 else g1
|
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 = random_search_refine(matrix, train_end, pool, best, df_1d, df_1h, entry_ohlc, iterations=1200)
|
||||||
train_ret, t_cnt = backtest_rules(matrix.iloc[:train_end], g3, df_1d, df_1h, entry_ohlc)
|
g3 = sanitize_rules(g3)
|
||||||
test_ret, _ = backtest_rules(matrix.iloc[train_end:], g3, df_1d, df_1h, entry_ohlc)
|
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)
|
full_ret, full_cnt = backtest_rules(matrix, g3, df_1d, df_1h, entry_ohlc)
|
||||||
|
|
||||||
g3.train_return_pct = train_ret
|
g3.train_return_pct = train_ret
|
||||||
@@ -549,7 +826,9 @@ def discover_rules(frames: dict[int, pd.DataFrame]) -> DiscoveredRules:
|
|||||||
print(f" 매도 AND: {g3.sell_all}")
|
print(f" 매도 AND: {g3.sell_all}")
|
||||||
if g3.sell_stop:
|
if g3.sell_stop:
|
||||||
print(f" 손절: {g3.sell_stop}")
|
print(f" 손절: {g3.sell_stop}")
|
||||||
print(f" 학습 {train_ret:+.2f}% | 검증 {test_ret:+.2f}% | 전체 {full_ret:+.2f}% ({full_cnt}건)")
|
print(
|
||||||
|
f" 학습 {train_ret:+.2f}% | 검증 {test_ret:+.2f}% | 전체 {full_ret:+.2f}% ({full_cnt}건)"
|
||||||
|
)
|
||||||
return g3
|
return g3
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
302
simulation.py
302
simulation.py
@@ -25,6 +25,7 @@ from plotly.subplots import make_subplots
|
|||||||
from config import (
|
from config import (
|
||||||
BUY_COOLDOWN_SEC,
|
BUY_COOLDOWN_SEC,
|
||||||
COIN_NAME,
|
COIN_NAME,
|
||||||
|
DEFAULT_BUY_KRW,
|
||||||
ENTRY_INTERVAL,
|
ENTRY_INTERVAL,
|
||||||
SELL_COOLDOWN_SEC,
|
SELL_COOLDOWN_SEC,
|
||||||
SIM_INITIAL_CASH_KRW,
|
SIM_INITIAL_CASH_KRW,
|
||||||
@@ -39,6 +40,8 @@ import strategy
|
|||||||
|
|
||||||
REPORT_DIR = Path(__file__).resolve().parent / "reports"
|
REPORT_DIR = Path(__file__).resolve().parent / "reports"
|
||||||
OUTPUT_HTML = REPORT_DIR / "wld_bb_simulation.html"
|
OUTPUT_HTML = REPORT_DIR / "wld_bb_simulation.html"
|
||||||
|
LOGOS_BENCHMARK_HTML = REPORT_DIR / "wld_logos_benchmark.html"
|
||||||
|
LOGOS_TRADES_FILE = Path(__file__).resolve().parent / "logos_trades.json"
|
||||||
|
|
||||||
|
|
||||||
def interval_chart_label(interval_min: int) -> str:
|
def interval_chart_label(interval_min: int) -> str:
|
||||||
@@ -48,9 +51,28 @@ def interval_chart_label(interval_min: int) -> str:
|
|||||||
return f"{interval_min}분봉"
|
return f"{interval_min}분봉"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_trade_dt(dt: pd.Timestamp) -> str:
|
||||||
|
"""마커 호버용 날짜·시간 문자열."""
|
||||||
|
return pd.Timestamp(dt).strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def _trade_hover_lines(t: "SimTrade", label: str) -> str:
|
||||||
|
"""매수·매도 마커 호버(팝업) 본문."""
|
||||||
|
lines = [
|
||||||
|
label,
|
||||||
|
_format_trade_dt(t.dt),
|
||||||
|
t.signal or "",
|
||||||
|
f"₩{t.price:,.2f}",
|
||||||
|
f"₩{t.krw:,.0f}",
|
||||||
|
]
|
||||||
|
if t.pnl is not None:
|
||||||
|
lines.append(f"손익 ₩{t.pnl:+,.0f}")
|
||||||
|
return "<br>".join(line for line in lines if line)
|
||||||
|
|
||||||
|
|
||||||
def _add_trade_markers(fig, trades: list["SimTrade"], row: int = 1) -> None:
|
def _add_trade_markers(fig, trades: list["SimTrade"], row: int = 1) -> None:
|
||||||
"""
|
"""
|
||||||
매수·매도 마커·라벨 (Scatter trace만 사용, 범례와 함께 토글).
|
매수·매도 체결 마커·라벨 (Scatter trace만 사용, 범례와 함께 토글).
|
||||||
simulate_mtf.py 와 동일 스타일.
|
simulate_mtf.py 와 동일 스타일.
|
||||||
"""
|
"""
|
||||||
for action, color, symbol, label, text_pos in [
|
for action, color, symbol, label, text_pos in [
|
||||||
@@ -80,10 +102,7 @@ def _add_trade_markers(fig, trades: list["SimTrade"], row: int = 1) -> None:
|
|||||||
color=color,
|
color=color,
|
||||||
line=dict(width=2, color="#111"),
|
line=dict(width=2, color="#111"),
|
||||||
),
|
),
|
||||||
hovertext=[
|
hovertext=[_trade_hover_lines(t, label) for t in pts],
|
||||||
f"{label} 체결<br>{t.signal}<br>₩{t.price:,.2f}<br>₩{t.krw:,.0f}"
|
|
||||||
for t in pts
|
|
||||||
],
|
|
||||||
hovertemplate="%{hovertext}<extra></extra>",
|
hovertemplate="%{hovertext}<extra></extra>",
|
||||||
),
|
),
|
||||||
row=row,
|
row=row,
|
||||||
@@ -97,6 +116,8 @@ def build_simulation_html(
|
|||||||
trend: str,
|
trend: str,
|
||||||
interval_min: int = ENTRY_INTERVAL,
|
interval_min: int = ENTRY_INTERVAL,
|
||||||
note: str = "",
|
note: str = "",
|
||||||
|
title_suffix: str = "BB 시뮬레이션",
|
||||||
|
show_memo_column: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""simulate_mtf.py 와 동일 레이아웃의 HTML 리포트."""
|
"""simulate_mtf.py 와 동일 레이아웃의 HTML 리포트."""
|
||||||
df = strategy.prepare_entry_df(df.copy())
|
df = strategy.prepare_entry_df(df.copy())
|
||||||
@@ -243,6 +264,15 @@ def build_simulation_html(
|
|||||||
for t in result.trades:
|
for t in result.trades:
|
||||||
cls = "buy" if t.action == "매수" else "sell"
|
cls = "buy" if t.action == "매수" else "sell"
|
||||||
pnl = f"{t.pnl:+,.0f}" if t.pnl is not None else "-"
|
pnl = f"{t.pnl:+,.0f}" if t.pnl is not None else "-"
|
||||||
|
if show_memo_column:
|
||||||
|
trade_rows += f"""
|
||||||
|
<tr>
|
||||||
|
<td>{t.dt}</td>
|
||||||
|
<td class="{cls}">{t.action}</td>
|
||||||
|
<td>₩{t.price:,.0f}</td>
|
||||||
|
<td>{t.signal}</td>
|
||||||
|
</tr>"""
|
||||||
|
else:
|
||||||
trade_rows += f"""
|
trade_rows += f"""
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t.dt}</td>
|
<td>{t.dt}</td>
|
||||||
@@ -255,19 +285,31 @@ def build_simulation_html(
|
|||||||
<td>{pnl}</td>
|
<td>{pnl}</td>
|
||||||
</tr>"""
|
</tr>"""
|
||||||
if not trade_rows:
|
if not trade_rows:
|
||||||
trade_rows = '<tr><td colspan="8">신호 없음</td></tr>'
|
colspan = 4 if show_memo_column else 8
|
||||||
|
trade_rows = f'<tr><td colspan="{colspan}">체결 없음</td></tr>'
|
||||||
|
|
||||||
note_html = f"<p class='warn'>{summary['note']}</p>" if summary.get("note") else ""
|
note_html = f"<p class='warn'>{summary['note']}</p>" if summary.get("note") else ""
|
||||||
sells = summary["sell_signal_count"]
|
sells = summary["sell_signal_count"]
|
||||||
win_rate = (
|
win_rate = (
|
||||||
summary["win_count"] / sells * 100 if sells else 0.0
|
summary["win_count"] / sells * 100 if sells else 0.0
|
||||||
)
|
)
|
||||||
|
if show_memo_column:
|
||||||
|
table_title = "로고스 타점 (직관 해석)"
|
||||||
|
table_head = (
|
||||||
|
"<th>시각</th><th>구분</th><th>가격</th><th>해석</th>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
table_title = "체결 내역"
|
||||||
|
table_head = (
|
||||||
|
"<th>시각</th><th>구분</th><th>상태</th><th>가격</th>"
|
||||||
|
"<th>신호</th><th>금액</th><th>수수료</th><th>손익</th>"
|
||||||
|
)
|
||||||
|
|
||||||
return f"""<!DOCTYPE html>
|
return f"""<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>{SYMBOL} BB 시뮬레이션</title>
|
<title>{SYMBOL} {title_suffix}</title>
|
||||||
<style>
|
<style>
|
||||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f8fafc; }}
|
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 24px; background: #f8fafc; }}
|
||||||
h1 {{ font-size: 1.35rem; }}
|
h1 {{ font-size: 1.35rem; }}
|
||||||
@@ -290,7 +332,7 @@ def build_simulation_html(
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{COIN_NAME} ({SYMBOL}) BB 시뮬레이션</h1>
|
<h1>{COIN_NAME} ({SYMBOL}) {title_suffix}</h1>
|
||||||
<p class="meta">전략: {summary['config_name']} | 추세: {summary['trend']} | 기간: {summary['period_start']} ~ {summary['period_end']}</p>
|
<p class="meta">전략: {summary['config_name']} | 추세: {summary['trend']} | 기간: {summary['period_start']} ~ {summary['period_end']}</p>
|
||||||
{note_html}
|
{note_html}
|
||||||
<div class="legend-box">
|
<div class="legend-box">
|
||||||
@@ -304,9 +346,9 @@ def build_simulation_html(
|
|||||||
<div class="card"><span>승률(매도 기준)</span><b>{win_rate:.1f}%</b></div>
|
<div class="card"><span>승률(매도 기준)</span><b>{win_rate:.1f}%</b></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrap">{chart_html}</div>
|
<div class="chart-wrap">{chart_html}</div>
|
||||||
<h2>신호·체결 내역</h2>
|
<h2>{table_title}</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>시각</th><th>구분</th><th>상태</th><th>가격</th><th>신호</th><th>금액</th><th>수수료</th><th>손익</th></tr></thead>
|
<thead><tr>{table_head}</tr></thead>
|
||||||
<tbody>{trade_rows}</tbody>
|
<tbody>{trade_rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
@@ -364,6 +406,7 @@ def run_backtest(
|
|||||||
last_sell_ts: pd.Timestamp | None = None
|
last_sell_ts: pd.Timestamp | None = None
|
||||||
|
|
||||||
signals = df_3m[df_3m["point"] == 1].sort_index()
|
signals = df_3m[df_3m["point"] == 1].sort_index()
|
||||||
|
signals = signals[signals["action"].isin(["buy", "sell"])]
|
||||||
|
|
||||||
for ts, row in signals.iterrows():
|
for ts, row in signals.iterrows():
|
||||||
price = float(row["Close"])
|
price = float(row["Close"])
|
||||||
@@ -610,6 +653,194 @@ class Simulation:
|
|||||||
webbrowser.open(OUTPUT_HTML.resolve().as_uri())
|
webbrowser.open(OUTPUT_HTML.resolve().as_uri())
|
||||||
return OUTPUT_HTML
|
return OUTPUT_HTML
|
||||||
|
|
||||||
|
def render_logos_html(
|
||||||
|
self,
|
||||||
|
df_plot: pd.DataFrame,
|
||||||
|
result: SimResult,
|
||||||
|
trend: str,
|
||||||
|
note: str = "",
|
||||||
|
open_browser: bool = True,
|
||||||
|
) -> Path:
|
||||||
|
"""로고스 직관 타점 HTML (규칙 엔진 미사용)."""
|
||||||
|
html = build_simulation_html(
|
||||||
|
df_plot,
|
||||||
|
result,
|
||||||
|
trend,
|
||||||
|
note=note,
|
||||||
|
title_suffix="로고스 직관 타점 (3분봉)",
|
||||||
|
show_memo_column=True,
|
||||||
|
)
|
||||||
|
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
LOGOS_BENCHMARK_HTML.write_text(html, encoding="utf-8")
|
||||||
|
print(f"HTML: {LOGOS_BENCHMARK_HTML}")
|
||||||
|
if open_browser:
|
||||||
|
webbrowser.open(LOGOS_BENCHMARK_HTML.resolve().as_uri())
|
||||||
|
return LOGOS_BENCHMARK_HTML
|
||||||
|
|
||||||
|
def run_logos_strategy_chart(self, frames: dict[int, pd.DataFrame]) -> SimResult:
|
||||||
|
"""
|
||||||
|
로고스 전략(logos_strategy.py) 백테스트·HTML.
|
||||||
|
|
||||||
|
설계: docs/LOGOS_STRATEGY.md
|
||||||
|
"""
|
||||||
|
from candle_features import build_master_feature_matrix
|
||||||
|
from logos_strategy import generate_logos_events
|
||||||
|
|
||||||
|
df_1d, df_1h, df_3m = self._frames_to_mtf(frames)
|
||||||
|
trend = strategy.get_trend(df_1d, df_1h)
|
||||||
|
matrix = build_master_feature_matrix(frames).iloc[21:].copy()
|
||||||
|
df_sig = strategy.prepare_entry_df(df_3m)
|
||||||
|
df_sig["signal"] = ""
|
||||||
|
df_sig["point"] = 0
|
||||||
|
df_sig["action"] = ""
|
||||||
|
df_sig["trend"] = ""
|
||||||
|
|
||||||
|
logos_events = generate_logos_events(matrix, df_1d, df_1h)
|
||||||
|
for ts, action, sig in logos_events:
|
||||||
|
if ts not in df_sig.index:
|
||||||
|
continue
|
||||||
|
df_sig.at[ts, "signal"] = sig
|
||||||
|
df_sig.at[ts, "point"] = 1
|
||||||
|
df_sig.at[ts, "action"] = action
|
||||||
|
df_sig.at[ts, "trend"] = strategy.get_trend_at(df_1d, df_1h, ts)
|
||||||
|
|
||||||
|
n_sig = int((df_sig["point"] == 1).sum())
|
||||||
|
print(f"\n[로고스 전략] 체결 {n_sig}건")
|
||||||
|
from logos_strategy import compare_to_ground_truth
|
||||||
|
|
||||||
|
cmp_rows = compare_to_ground_truth(logos_events, matrix)
|
||||||
|
if cmp_rows:
|
||||||
|
matched = sum(1 for r in cmp_rows if r["match"])
|
||||||
|
print(f"[정답 비교] {matched}/{len(cmp_rows)} 타점 일치 (±20봉·±6%)")
|
||||||
|
for r in cmp_rows:
|
||||||
|
mark = "O" if r["match"] else "X"
|
||||||
|
cand = r.get("cand_dt") or "-"
|
||||||
|
print(
|
||||||
|
f" [{mark}] {r['gt_action']} {r['gt_dt'][:16]} "
|
||||||
|
f"-> {str(cand)[:16]} ({r['score_pct']}%)"
|
||||||
|
)
|
||||||
|
result = run_backtest(df_sig, df_1d, df_1h, config_name="logos_strategy")
|
||||||
|
print_backtest_report(result)
|
||||||
|
|
||||||
|
html = build_simulation_html(
|
||||||
|
df_sig,
|
||||||
|
result,
|
||||||
|
trend,
|
||||||
|
note="로고스 전략: docs/LOGOS_STRATEGY.md (A바닥/B눌림/C익절/D과열)",
|
||||||
|
title_suffix="로고스 전략 (3분봉)",
|
||||||
|
)
|
||||||
|
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUTPUT_HTML.write_text(html, encoding="utf-8")
|
||||||
|
print(f"HTML: {OUTPUT_HTML}")
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
webbrowser.open(OUTPUT_HTML.resolve().as_uri())
|
||||||
|
return result
|
||||||
|
|
||||||
|
def run_logos_chart(self, frames: dict[int, pd.DataFrame]) -> SimResult:
|
||||||
|
"""
|
||||||
|
차트 해석 기반 수동 타점을 전 기간 3분봉에 표시합니다.
|
||||||
|
|
||||||
|
데이터: logos_trades.json (BB/탐색 규칙과 무관)
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
if not LOGOS_TRADES_FILE.exists():
|
||||||
|
raise FileNotFoundError(f"{LOGOS_TRADES_FILE} 없음")
|
||||||
|
|
||||||
|
spec = json.loads(LOGOS_TRADES_FILE.read_text(encoding="utf-8"))
|
||||||
|
df_1d, df_1h, df_3m = self._frames_to_mtf(frames)
|
||||||
|
df_3m = strategy.prepare_entry_df(df_3m)
|
||||||
|
trend = strategy.get_trend(df_1d, df_1h)
|
||||||
|
|
||||||
|
buy_krw = float(DEFAULT_BUY_KRW)
|
||||||
|
fee_rate = TRADING_FEE_RATE
|
||||||
|
cash = float(SIM_INITIAL_CASH_KRW)
|
||||||
|
qty = 0.0
|
||||||
|
cost = 0.0
|
||||||
|
sim_trades: list[SimTrade] = []
|
||||||
|
win = 0
|
||||||
|
|
||||||
|
for row in spec.get("trades") or []:
|
||||||
|
ts = pd.Timestamp(row["dt"])
|
||||||
|
act = row["action"]
|
||||||
|
px = float(row["price"])
|
||||||
|
memo = str(row.get("memo", ""))
|
||||||
|
if act == "buy":
|
||||||
|
fee = buy_krw * fee_rate
|
||||||
|
cash -= buy_krw + fee
|
||||||
|
qty += buy_krw / px
|
||||||
|
cost += buy_krw
|
||||||
|
sim_trades.append(
|
||||||
|
SimTrade(
|
||||||
|
dt=ts,
|
||||||
|
action="매수",
|
||||||
|
signal=memo,
|
||||||
|
price=px,
|
||||||
|
krw=buy_krw,
|
||||||
|
fee=fee,
|
||||||
|
quantity=buy_krw / px,
|
||||||
|
pnl=None,
|
||||||
|
cash_after=cash,
|
||||||
|
total_asset=cash + qty * px,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif act == "sell" and qty > 0:
|
||||||
|
sell_krw = qty * px
|
||||||
|
fee = sell_krw * fee_rate
|
||||||
|
net = sell_krw - fee
|
||||||
|
pnl = net - cost
|
||||||
|
if pnl > 0:
|
||||||
|
win += 1
|
||||||
|
cash += net
|
||||||
|
sim_trades.append(
|
||||||
|
SimTrade(
|
||||||
|
dt=ts,
|
||||||
|
action="매도",
|
||||||
|
signal=memo,
|
||||||
|
price=px,
|
||||||
|
krw=sell_krw,
|
||||||
|
fee=fee,
|
||||||
|
quantity=qty,
|
||||||
|
pnl=pnl,
|
||||||
|
cash_after=cash,
|
||||||
|
total_asset=cash,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
qty = 0.0
|
||||||
|
cost = 0.0
|
||||||
|
|
||||||
|
last_px = float(df_3m["Close"].iloc[-1])
|
||||||
|
result = SimResult(
|
||||||
|
config_name=spec.get("name", "logos"),
|
||||||
|
trades=sim_trades,
|
||||||
|
initial_cash=SIM_INITIAL_CASH_KRW,
|
||||||
|
final_cash=cash,
|
||||||
|
final_coin_qty=qty,
|
||||||
|
final_price=last_px,
|
||||||
|
realized_pnl=sum(t.pnl or 0 for t in sim_trades if t.pnl),
|
||||||
|
total_fees=sum(t.fee for t in sim_trades),
|
||||||
|
final_asset=cash + qty * last_px,
|
||||||
|
total_return_pct=(cash + qty * last_px - SIM_INITIAL_CASH_KRW)
|
||||||
|
/ SIM_INITIAL_CASH_KRW
|
||||||
|
* 100,
|
||||||
|
trade_count=len(sim_trades),
|
||||||
|
win_count=win,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n[로고스 직관 타점] {spec.get('author', 'Logos')}")
|
||||||
|
print(f" 기간: {df_3m.index[0]} ~ {df_3m.index[-1]}")
|
||||||
|
print(f" 타점 {len(sim_trades)}개 (규칙 엔진·BB 조건 미사용)")
|
||||||
|
for t in sim_trades:
|
||||||
|
print(f" {t.dt} {t.action} ₩{t.price:,.0f} — {t.signal}")
|
||||||
|
print_backtest_report(result)
|
||||||
|
|
||||||
|
note = spec.get("note", "")
|
||||||
|
path = self.render_logos_html(df_3m, result, trend, note=note, open_browser=True)
|
||||||
|
print(f"\n차트 파일: {path.resolve()}")
|
||||||
|
print("브라우저에서 열리지 않으면 위 경로를 더블클릭하세요.")
|
||||||
|
return result
|
||||||
|
|
||||||
def load_all_frames(self) -> dict[int, pd.DataFrame]:
|
def load_all_frames(self) -> dict[int, pd.DataFrame]:
|
||||||
"""discovered 규칙용 전 간격 로드."""
|
"""discovered 규칙용 전 간격 로드."""
|
||||||
from mtf_bb import load_frames_from_db
|
from mtf_bb import load_frames_from_db
|
||||||
@@ -690,7 +921,7 @@ class Simulation:
|
|||||||
n_sig = int((df_sig["point"] == 1).sum())
|
n_sig = int((df_sig["point"] == 1).sum())
|
||||||
buy_sig = int((df_sig["action"] == "buy").sum())
|
buy_sig = int((df_sig["action"] == "buy").sum())
|
||||||
sell_sig = int((df_sig["action"] == "sell").sum())
|
sell_sig = int((df_sig["action"] == "sell").sum())
|
||||||
print(f"\n규칙 신호: {n_sig} (매수 {buy_sig} / 매도 {sell_sig})")
|
print(f"\n체결 신호: {n_sig} (매수 {buy_sig} / 매도 {sell_sig})")
|
||||||
|
|
||||||
result = run_backtest(df_sig, df_1d, df_1h, config_name=rule_set.name)
|
result = run_backtest(df_sig, df_1d, df_1h, config_name=rule_set.name)
|
||||||
print_backtest_report(result)
|
print_backtest_report(result)
|
||||||
@@ -770,7 +1001,15 @@ def run_analyze(frames: dict[int, pd.DataFrame] | None = None) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def run_discover(frames: dict[int, pd.DataFrame] | None = None):
|
def run_discover(frames: dict[int, pd.DataFrame] | None = None):
|
||||||
"""모든 봉·BB·일목 특징으로 최적 규칙 탐색 후 JSON 저장."""
|
"""
|
||||||
|
모든 봉·BB·일목 특징으로 최적 규칙 탐색 후 JSON 저장.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frames: 전 간격 OHLCV. None이면 coins.db에서 로드.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DiscoveredRules 또는 데이터/탐색 실패 시 None.
|
||||||
|
"""
|
||||||
from rule_discovery import discover_rules, save_rules
|
from rule_discovery import discover_rules, save_rules
|
||||||
|
|
||||||
if frames is None:
|
if frames is None:
|
||||||
@@ -784,30 +1023,25 @@ def run_discover(frames: dict[int, pd.DataFrame] | None = None):
|
|||||||
return rules
|
return rules
|
||||||
|
|
||||||
|
|
||||||
def run_full_pipeline() -> None:
|
def run_logos_benchmark() -> None:
|
||||||
"""
|
"""수동 벤치마크 타점 차트 (logos_trades.json, 참고용)."""
|
||||||
일반 사용자용 일괄 실행: analyze → discover → HTML.
|
print("=== 로고스 수동 벤치마크 차트 ===")
|
||||||
|
frames = _load_all_frames_or_exit()
|
||||||
|
if frames is None:
|
||||||
|
return
|
||||||
|
Simulation().run_logos_chart(frames)
|
||||||
|
print("\n완료.")
|
||||||
|
|
||||||
DB 로드는 한 번만 수행합니다.
|
|
||||||
"""
|
def run_full_pipeline() -> None:
|
||||||
|
"""로고스 전략 백테스트·HTML (단일 진입점)."""
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("전체 파이프라인: analyze → discover → HTML")
|
print("로고스 전략: 백테스트 → HTML")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
frames = _load_all_frames_or_exit()
|
frames = _load_all_frames_or_exit()
|
||||||
if frames is None:
|
if frames is None:
|
||||||
return
|
return
|
||||||
|
Simulation().run_logos_strategy_chart(frames)
|
||||||
print("\n[1/3] 조합 분석 (analyze)")
|
|
||||||
run_analyze(frames)
|
|
||||||
|
|
||||||
print("\n[2/3] 규칙 탐색 (discover)")
|
|
||||||
run_discover(frames)
|
|
||||||
|
|
||||||
print("\n[3/3] 백테스트·HTML 차트 (탐색 규칙 매수·매도)")
|
|
||||||
if rules is None:
|
|
||||||
print("규칙 탐색 실패 — HTML 생략")
|
|
||||||
return
|
|
||||||
Simulation().run_discovered_chart(frames, rules=rules)
|
|
||||||
print("\n완료.")
|
print("\n완료.")
|
||||||
|
|
||||||
|
|
||||||
@@ -817,7 +1051,10 @@ def print_usage() -> None:
|
|||||||
DeepCoin simulation.py
|
DeepCoin simulation.py
|
||||||
|
|
||||||
python simulation.py
|
python simulation.py
|
||||||
analyze + discover + HTML (차트 = discovered_rules 매수·매도)
|
로고스 전략 백테스트·HTML → reports/wld_bb_simulation.html
|
||||||
|
|
||||||
|
python simulation.py benchmark
|
||||||
|
수동 벤치마크(logos_trades.json) → reports/wld_logos_benchmark.html
|
||||||
|
|
||||||
(고급) analyze | discover | compare | mtf
|
(고급) analyze | discover | compare | mtf
|
||||||
"""
|
"""
|
||||||
@@ -840,6 +1077,9 @@ def main() -> None:
|
|||||||
if len(sys.argv) > 1 and sys.argv[1] == "discover":
|
if len(sys.argv) > 1 and sys.argv[1] == "discover":
|
||||||
run_discover()
|
run_discover()
|
||||||
return
|
return
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] in ("benchmark", "logos"):
|
||||||
|
run_logos_benchmark()
|
||||||
|
return
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "mtf":
|
if len(sys.argv) > 1 and sys.argv[1] == "mtf":
|
||||||
run_mtf_analysis()
|
run_mtf_analysis()
|
||||||
return
|
return
|
||||||
|
|||||||
31
strategy.py
31
strategy.py
@@ -489,29 +489,44 @@ def evaluate_discovered_live(
|
|||||||
최신 3분 봉 시점에서 discovered_rules + 전 봉 BB·일목 조합으로 신호 1건.
|
최신 3분 봉 시점에서 discovered_rules + 전 봉 BB·일목 조합으로 신호 1건.
|
||||||
"""
|
"""
|
||||||
from candle_features import build_master_feature_matrix
|
from candle_features import build_master_feature_matrix
|
||||||
from rule_discovery import buy_mask, load_rules, rules_have_buy, sell_mask
|
from rule_discovery import (
|
||||||
|
_trigger_at,
|
||||||
|
buy_mask,
|
||||||
|
load_rules,
|
||||||
|
rules_have_buy,
|
||||||
|
sell_mask,
|
||||||
|
)
|
||||||
|
|
||||||
rules = load_rules()
|
rules = load_rules()
|
||||||
if rules is None or not rules_have_buy(rules):
|
if rules is None or not rules_have_buy(rules):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
matrix = build_master_feature_matrix(frames)
|
matrix = build_master_feature_matrix(frames)
|
||||||
if len(matrix) < 22:
|
if len(matrix) < 23:
|
||||||
return None
|
return None
|
||||||
last = matrix.iloc[[-1]]
|
tail = matrix.iloc[-2:]
|
||||||
ts = last.index[-1]
|
i = 1
|
||||||
close = float(last["Close"].iloc[-1])
|
ts = tail.index[-1]
|
||||||
|
close = float(tail["Close"].iloc[-1])
|
||||||
trend = get_trend_at(df_1d, df_1h, ts)
|
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)
|
position = float(balances.get(symbol, {}).get("balance", 0) or 0)
|
||||||
if position >= 1.0:
|
if position >= 1.0:
|
||||||
if rules.sell_stop and sell_mask(last, rules, stop=True)[0]:
|
if rules.sell_stop and _trigger_at(st_m, i):
|
||||||
return TradeSignal("sell", SIGNAL_SELL_STOP, close, trend)
|
return TradeSignal("sell", SIGNAL_SELL_STOP, close, trend)
|
||||||
if sell_mask(last, rules, stop=False)[0]:
|
if _trigger_at(s_m, i):
|
||||||
return TradeSignal("sell", SIGNAL_SELL_UPPER, close, trend)
|
return TradeSignal("sell", SIGNAL_SELL_UPPER, close, trend)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if buy_mask(last, rules)[0]:
|
if _trigger_at(b_m, i):
|
||||||
return TradeSignal("buy", SIGNAL_BUY_LOWER, close, trend)
|
return TradeSignal("buy", SIGNAL_BUY_LOWER, close, trend)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user