nextrade-engine 0.6.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nextrade_engine-0.6.0/nextrade/nextrade_engine.egg-info → nextrade_engine-0.7.0}/PKG-INFO +2 -1
- nextrade_engine-0.7.0/nextrade/nextrade/__init__.py +330 -0
- nextrade_engine-0.7.0/nextrade/nextrade/ml/features.py +185 -0
- nextrade_engine-0.7.0/nextrade/nextrade/ml/model.py +250 -0
- nextrade_engine-0.7.0/nextrade/nextrade/utils/__init__.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0/nextrade/nextrade_engine.egg-info}/PKG-INFO +2 -1
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade_engine.egg-info/SOURCES.txt +3 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade_engine.egg-info/requires.txt +1 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/pyproject.toml +2 -1
- nextrade_engine-0.6.0/nextrade/nextrade/__init__.py +0 -194
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/LICENSE +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/README.md +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/backtest/__init__.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/backtest/engine.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/core/__init__.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/core/brain.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/core/regime.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/data/__init__.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/data/fetcher.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/indicators/__init__.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/indicators/adaptive.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/indicators/confluence.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/indicators/pattern.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/lang.py +0 -0
- {nextrade_engine-0.6.0/nextrade/nextrade/utils → nextrade_engine-0.7.0/nextrade/nextrade/ml}/__init__.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade/utils/terminal_ui.py +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade_engine.egg-info/dependency_links.txt +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade_engine.egg-info/top_level.txt +0 -0
- {nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextrade-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: AI Trading Engine — Ringan, Multi-Market, Bisa di HP
|
|
5
5
|
Author-email: NexTrade <wafiqrazy035@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
15
15
|
Requires-Dist: yfinance
|
|
16
|
+
Requires-Dist: scikit-learn
|
|
16
17
|
Dynamic: license-file
|
|
17
18
|
|
|
18
19
|
# NexTrade 🧠
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade — AI Trading Engine
|
|
3
|
+
import nextrade → wizard muncul otomatis di terminal
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from nextrade.lang import set_language, list_languages, get, current as _lang
|
|
7
|
+
from nextrade.utils.terminal_ui import (
|
|
8
|
+
print_welcome, print_signal, print_error,
|
|
9
|
+
print_loading, print_success, list_strategies, STRATEGIES
|
|
10
|
+
)
|
|
11
|
+
from nextrade.indicators.confluence import confluence_score
|
|
12
|
+
from nextrade.core.regime import detect_regime
|
|
13
|
+
from nextrade.core.brain import Brain
|
|
14
|
+
from nextrade.data.fetcher import fetch, fetch_multi, list_markets
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
__version__ = "0.7.0"
|
|
18
|
+
__author__ = "NexTrade"
|
|
19
|
+
|
|
20
|
+
# Tampilkan wizard saat pertama kali import
|
|
21
|
+
print_welcome()
|
|
22
|
+
|
|
23
|
+
# Global instances
|
|
24
|
+
_brain = Brain.__new__(Brain)
|
|
25
|
+
_brain.state = None
|
|
26
|
+
_ml_engine = None
|
|
27
|
+
|
|
28
|
+
def _get_brain():
|
|
29
|
+
global _brain
|
|
30
|
+
if _brain.state is None:
|
|
31
|
+
_brain = Brain()
|
|
32
|
+
return _brain
|
|
33
|
+
|
|
34
|
+
def _get_ml():
|
|
35
|
+
global _ml_engine
|
|
36
|
+
return _ml_engine
|
|
37
|
+
|
|
38
|
+
# ─────────────────────────────────────────
|
|
39
|
+
# PUBLIC API
|
|
40
|
+
# ─────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
def start(strategy: str = "adaptive",
|
|
43
|
+
market: str = "BTCUSD",
|
|
44
|
+
timeframe: str = "H1",
|
|
45
|
+
data: dict = None,
|
|
46
|
+
use_ml: bool = False):
|
|
47
|
+
"""
|
|
48
|
+
Jalankan NexTrade dengan strategi pilihan.
|
|
49
|
+
|
|
50
|
+
Contoh:
|
|
51
|
+
nextrade.start("momentum", market="BTCUSD")
|
|
52
|
+
nextrade.start("adaptive", market="EURUSD", use_ml=True)
|
|
53
|
+
"""
|
|
54
|
+
strategy = strategy.lower().strip()
|
|
55
|
+
if strategy not in STRATEGIES:
|
|
56
|
+
print_error(f"{get('error_strategy')}: {', '.join(STRATEGIES)}")
|
|
57
|
+
list_strategies()
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
print_loading(f"{get('loading_strategy')} {strategy.upper()}...")
|
|
61
|
+
|
|
62
|
+
if data is None:
|
|
63
|
+
data = fetch(market, timeframe, bars=500)
|
|
64
|
+
if data is None:
|
|
65
|
+
print_error(get('error_data'))
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
print_loading(f"{get('loading_indicators')}...")
|
|
69
|
+
|
|
70
|
+
# ── Sinyal dari rule-based engine ─────────────────────
|
|
71
|
+
result = confluence_score(
|
|
72
|
+
opens=data["open"], highs=data["high"],
|
|
73
|
+
lows=data["low"], closes=data["close"],
|
|
74
|
+
)
|
|
75
|
+
brain = _get_brain()
|
|
76
|
+
threshold = brain.get_confidence_threshold()
|
|
77
|
+
confidence = result["confidence"]
|
|
78
|
+
|
|
79
|
+
if confidence >= threshold and result["details"]["pattern"]["direction"] != "bear":
|
|
80
|
+
signal_rb = "BUY"
|
|
81
|
+
elif confidence <= (100 - threshold) or result["details"]["pattern"]["direction"] == "bear":
|
|
82
|
+
signal_rb = "SELL"
|
|
83
|
+
else:
|
|
84
|
+
signal_rb = "HOLD"
|
|
85
|
+
|
|
86
|
+
# ── Sinyal dari ML engine (opsional) ──────────────────
|
|
87
|
+
ml_signal = None
|
|
88
|
+
ml_conf = None
|
|
89
|
+
ml_proba = None
|
|
90
|
+
|
|
91
|
+
if use_ml:
|
|
92
|
+
ml = _get_ml()
|
|
93
|
+
if ml is None or not ml.is_trained:
|
|
94
|
+
print(f" \033[33m⚠\033[0m ML belum ditraining. "
|
|
95
|
+
f"Jalankan \033[1mnextrade.train_ml()\033[0m dulu.\n"
|
|
96
|
+
f" \033[2mMenggunakan rule-based engine sementara...\033[0m\n")
|
|
97
|
+
else:
|
|
98
|
+
print_loading("Menjalankan ML engine...")
|
|
99
|
+
ml_signal, ml_conf, ml_proba = ml.predict(
|
|
100
|
+
data["open"], data["high"],
|
|
101
|
+
data["low"], data["close"],
|
|
102
|
+
data.get("volume"))
|
|
103
|
+
|
|
104
|
+
# ── Gabungkan sinyal (ML + Rule-based) ────────────────
|
|
105
|
+
if ml_signal and ml_conf:
|
|
106
|
+
# Voting: ML 60% + Rule-based 40%
|
|
107
|
+
signals_vote = {"BUY": 0.0, "SELL": 0.0, "HOLD": 0.0}
|
|
108
|
+
signals_vote[ml_signal] += ml_conf * 0.6
|
|
109
|
+
signals_vote[signal_rb] += confidence * 0.4
|
|
110
|
+
signal = max(signals_vote, key=signals_vote.get)
|
|
111
|
+
confidence = round(signals_vote[signal], 2)
|
|
112
|
+
source = "ML + Rule-based"
|
|
113
|
+
else:
|
|
114
|
+
signal = signal_rb
|
|
115
|
+
source = "Rule-based"
|
|
116
|
+
|
|
117
|
+
print_signal(signal=signal, confidence=confidence,
|
|
118
|
+
strategy=strategy, market=market)
|
|
119
|
+
|
|
120
|
+
regime = result["details"]["regime"]["regime"]
|
|
121
|
+
rsi = result["details"]["rsi"]
|
|
122
|
+
price = data["price_now"]
|
|
123
|
+
|
|
124
|
+
print(f" \033[2m{get('detail_analysis')}:\033[0m")
|
|
125
|
+
print(f" \033[36m{get('price_now'):<18}:\033[0m {price:,.5f}")
|
|
126
|
+
print(f" \033[36m{get('market_condition'):<18}:\033[0m {get(regime)}")
|
|
127
|
+
print(f" \033[36mRSI Adaptif :\033[0m {rsi:.1f}")
|
|
128
|
+
print(f" \033[36m{get('iq_model'):<18}:\033[0m {brain.state['iq']} / 100")
|
|
129
|
+
print(f" \033[36mEngine :\033[0m {source}")
|
|
130
|
+
|
|
131
|
+
if ml_signal:
|
|
132
|
+
print(f" \033[36mML Signal :\033[0m {ml_signal} "
|
|
133
|
+
f"\033[2m({ml_conf:.1f}%)\033[0m")
|
|
134
|
+
print(f" \033[36mML Probabilitas :\033[0m "
|
|
135
|
+
f"BUY={ml_proba.get('BUY',0):.2f} "
|
|
136
|
+
f"SELL={ml_proba.get('SELL',0):.2f} "
|
|
137
|
+
f"HOLD={ml_proba.get('HOLD',0):.2f}")
|
|
138
|
+
print()
|
|
139
|
+
|
|
140
|
+
return {**result, "signal": signal, "market": market,
|
|
141
|
+
"price": price, "strategy": strategy, "source": source}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def train_ml(market: str = "BTCUSD",
|
|
145
|
+
timeframe: str = "H1",
|
|
146
|
+
bars: int = 1000,
|
|
147
|
+
forward: int = 3,
|
|
148
|
+
threshold: float = 0.005):
|
|
149
|
+
"""
|
|
150
|
+
Train ML engine NexTrade dengan data real market.
|
|
151
|
+
|
|
152
|
+
Contoh:
|
|
153
|
+
nextrade.train_ml("BTCUSD", "H1")
|
|
154
|
+
nextrade.train_ml("EURUSD", "H4", bars=2000)
|
|
155
|
+
nextrade.train_ml("AAPL", "D1")
|
|
156
|
+
"""
|
|
157
|
+
global _ml_engine
|
|
158
|
+
from nextrade.ml.model import NexTradeML
|
|
159
|
+
|
|
160
|
+
print(f"\n \033[1m\033[37m NexTrade ML Training\033[0m")
|
|
161
|
+
print(f" \033[36m{'─' * 44}\033[0m")
|
|
162
|
+
|
|
163
|
+
data = fetch(market, timeframe, bars=bars)
|
|
164
|
+
if data is None:
|
|
165
|
+
print_error(get('error_data'))
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
_ml_engine = NexTradeML()
|
|
169
|
+
|
|
170
|
+
# Coba load model tersimpan dulu
|
|
171
|
+
if _ml_engine.load():
|
|
172
|
+
print(f" \033[32m✓\033[0m Model tersimpan ditemukan — "
|
|
173
|
+
f"winrate sebelumnya: {_ml_engine.winrate:.1f}%")
|
|
174
|
+
print(f" \033[36m⠿\033[0m Melatih ulang dengan data terbaru...\n")
|
|
175
|
+
|
|
176
|
+
result = _ml_engine.train(
|
|
177
|
+
opens=data["open"], highs=data["high"],
|
|
178
|
+
lows=data["low"], closes=data["close"],
|
|
179
|
+
volume=data.get("volume"),
|
|
180
|
+
forward=forward, threshold=threshold,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Update brain IQ berdasarkan winrate ML
|
|
184
|
+
brain = _get_brain()
|
|
185
|
+
brain.state["correct_predictions"] += int(
|
|
186
|
+
result["winrate"] / 100 * result["samples_train"])
|
|
187
|
+
brain.state["total_predictions"] += result["samples_train"]
|
|
188
|
+
brain.state["iq"] = min(int(result["winrate"]), 100)
|
|
189
|
+
|
|
190
|
+
print(f" \033[32m✓\033[0m IQ Model diupdate ke: "
|
|
191
|
+
f"\033[1m\033[33m{brain.state['iq']}\033[0m\n")
|
|
192
|
+
|
|
193
|
+
return _ml_engine
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def ml_status():
|
|
197
|
+
"""Tampilkan status ML engine."""
|
|
198
|
+
ml = _get_ml()
|
|
199
|
+
if ml is None or not ml.is_trained:
|
|
200
|
+
print(f"\n \033[33m⚠\033[0m ML belum ditraining. "
|
|
201
|
+
f"Jalankan \033[1mnextrade.train_ml()\033[0m dulu.\n")
|
|
202
|
+
return
|
|
203
|
+
print(f"\n \033[1m\033[37m NexTrade ML Status\033[0m")
|
|
204
|
+
print(f" \033[36m{'─' * 40}\033[0m")
|
|
205
|
+
if ml.train_history:
|
|
206
|
+
h = ml.train_history[-1]
|
|
207
|
+
print(f" \033[1mWinrate :\033[0m \033[33m{h['winrate']:.1f}%\033[0m")
|
|
208
|
+
print(f" \033[1mPrice Predictor:\033[0m {h['acc_price']:.1f}%")
|
|
209
|
+
print(f" \033[1mPattern Classif:\033[0m {h['acc_pattern']:.1f}%")
|
|
210
|
+
print(f" \033[1mSignal Filter :\033[0m {h['acc_filter']:.1f}%")
|
|
211
|
+
print(f" \033[1mTotal Features :\033[0m {h['features']}")
|
|
212
|
+
print(f" \033[1mSamples Train :\033[0m {h['samples_train']:,}")
|
|
213
|
+
print(f" \033[36m{'─' * 40}\033[0m\n")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def train(mode: str = "hybrid", market: str = "BTCUSD",
|
|
217
|
+
timeframe: str = "D1", bars: int = 1000):
|
|
218
|
+
"""Latih brain NexTrade dengan data real market."""
|
|
219
|
+
print(f"\n \033[1m\033[37m Training NexTrade Brain\033[0m")
|
|
220
|
+
print(f" \033[36m{'─' * 40}\033[0m")
|
|
221
|
+
data = fetch(market, timeframe, bars=bars)
|
|
222
|
+
if data is None:
|
|
223
|
+
print_error(get('error_data'))
|
|
224
|
+
return
|
|
225
|
+
_get_brain().train(mode=mode, data=data)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def brain_status():
|
|
229
|
+
"""Tampilkan status otak NexTrade."""
|
|
230
|
+
_get_brain().status()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def set_agresivitas(pct: int):
|
|
234
|
+
"""Set agresivitas sinyal (0-100)."""
|
|
235
|
+
_get_brain().set_agresivitas(pct)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def scan(markets: list = None, timeframe: str = "H1",
|
|
239
|
+
use_ml: bool = False):
|
|
240
|
+
"""Scan banyak market sekaligus."""
|
|
241
|
+
if markets is None:
|
|
242
|
+
markets = ["BTCUSD", "EURUSD", "XAUUSD", "AAPL", "TSLA"]
|
|
243
|
+
|
|
244
|
+
print(f"\n \033[1m\033[37m NexTrade Market Scanner\033[0m")
|
|
245
|
+
print(f" \033[36m{'─' * 58}\033[0m")
|
|
246
|
+
print(f" \033[2m{'MARKET':<12} {'HARGA':>14} {get('signal'):>8} "
|
|
247
|
+
f"{'CONF':>8} {'ENGINE'}\033[0m")
|
|
248
|
+
print(f" \033[36m{'─' * 58}\033[0m")
|
|
249
|
+
|
|
250
|
+
results = []
|
|
251
|
+
ml = _get_ml()
|
|
252
|
+
|
|
253
|
+
for market in markets:
|
|
254
|
+
data = fetch(market, timeframe, bars=300, silent=True)
|
|
255
|
+
if data is None: continue
|
|
256
|
+
|
|
257
|
+
result = confluence_score(
|
|
258
|
+
opens=data["open"], highs=data["high"],
|
|
259
|
+
lows=data["low"], closes=data["close"],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
signal = result["signal"]
|
|
263
|
+
confidence = result["confidence"]
|
|
264
|
+
price = data["price_now"]
|
|
265
|
+
engine_tag = "rule"
|
|
266
|
+
|
|
267
|
+
if use_ml and ml and ml.is_trained:
|
|
268
|
+
ms, mc, _ = ml.predict(
|
|
269
|
+
data["open"], data["high"],
|
|
270
|
+
data["low"], data["close"],
|
|
271
|
+
data.get("volume"))
|
|
272
|
+
vote = {"BUY":0.0,"SELL":0.0,"HOLD":0.0}
|
|
273
|
+
vote[ms] += mc * 0.6
|
|
274
|
+
vote[signal] += confidence * 0.4
|
|
275
|
+
signal = max(vote, key=vote.get)
|
|
276
|
+
confidence = round(vote[signal], 2)
|
|
277
|
+
engine_tag = "ML+rule"
|
|
278
|
+
|
|
279
|
+
sc = "\033[32m" if signal=="BUY" else "\033[31m" if signal=="SELL" else "\033[33m"
|
|
280
|
+
print(f" {market:<12} {price:>14,.5f} {sc}{signal:>8}\033[0m "
|
|
281
|
+
f"{confidence:>6.1f}% \033[2m{engine_tag}\033[0m")
|
|
282
|
+
results.append({**result, "market": market,
|
|
283
|
+
"price": price, "signal": signal})
|
|
284
|
+
|
|
285
|
+
print(f" \033[36m{'─' * 58}\033[0m\n")
|
|
286
|
+
buys = [r for r in results if r["signal"]=="BUY"]
|
|
287
|
+
sells = [r for r in results if r["signal"]=="SELL"]
|
|
288
|
+
if buys:
|
|
289
|
+
best = max(buys, key=lambda x: x["confidence"])
|
|
290
|
+
print(f" \033[32m★ BUY : {best['market']} ({best['confidence']:.1f}%)\033[0m")
|
|
291
|
+
if sells:
|
|
292
|
+
best = max(sells, key=lambda x: x["confidence"])
|
|
293
|
+
print(f" \033[31m★ SELL : {best['market']} ({best['confidence']:.1f}%)\033[0m")
|
|
294
|
+
print()
|
|
295
|
+
return results
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def backtest(strategy: str = "adaptive",
|
|
299
|
+
market: str = "BTCUSD",
|
|
300
|
+
timeframe: str = "D1",
|
|
301
|
+
bars: int = 500,
|
|
302
|
+
agresivitas: int = 30,
|
|
303
|
+
sl_pct: float = 2.0,
|
|
304
|
+
tp_pct: float = 4.0):
|
|
305
|
+
"""Backtest strategi dengan data real historis."""
|
|
306
|
+
from nextrade.backtest.engine import run as _run
|
|
307
|
+
data = fetch(market, timeframe, bars=bars)
|
|
308
|
+
if data is None:
|
|
309
|
+
print_error(get('error_data'))
|
|
310
|
+
return
|
|
311
|
+
return _run(data=data, strategy=strategy, agresivitas=agresivitas,
|
|
312
|
+
sl_pct=sl_pct, tp_pct=tp_pct)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def info():
|
|
316
|
+
"""Tampilkan semua strategi dan market."""
|
|
317
|
+
list_strategies()
|
|
318
|
+
list_markets()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def regime(market: str = "BTCUSD", timeframe: str = "H1") -> dict:
|
|
322
|
+
"""Deteksi kondisi pasar saat ini."""
|
|
323
|
+
data = fetch(market, timeframe, bars=100, silent=True)
|
|
324
|
+
if data is None: return {}
|
|
325
|
+
return detect_regime(data["close"])
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def language():
|
|
329
|
+
"""Tampilkan semua bahasa yang tersedia."""
|
|
330
|
+
list_languages()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade ML — Feature Engineering Layer
|
|
3
|
+
30 fitur dari data OHLCV untuk model ML.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
def _safe_div(a, b, fill=0.0):
|
|
8
|
+
return np.where(np.abs(b) > 1e-9, a / b, fill)
|
|
9
|
+
|
|
10
|
+
def _rolling(arr, n, func):
|
|
11
|
+
out = np.full(len(arr), np.nan)
|
|
12
|
+
for i in range(n-1, len(arr)):
|
|
13
|
+
out[i] = func(arr[i-n+1:i+1])
|
|
14
|
+
return out
|
|
15
|
+
|
|
16
|
+
def _ema(arr, period):
|
|
17
|
+
k = 2 / (period + 1)
|
|
18
|
+
out = np.full(len(arr), np.nan)
|
|
19
|
+
out[period-1] = np.mean(arr[:period])
|
|
20
|
+
for i in range(period, len(arr)):
|
|
21
|
+
out[i] = arr[i] * k + out[i-1] * (1 - k)
|
|
22
|
+
return out
|
|
23
|
+
|
|
24
|
+
def _rsi(close, period=14):
|
|
25
|
+
delta = np.diff(close, prepend=close[0])
|
|
26
|
+
gain = np.where(delta > 0, delta, 0.0)
|
|
27
|
+
loss = np.where(delta < 0, -delta, 0.0)
|
|
28
|
+
avg_g = _rolling(gain, period, np.mean)
|
|
29
|
+
avg_l = _rolling(loss, period, np.mean)
|
|
30
|
+
rs = _safe_div(avg_g, avg_l + 1e-9)
|
|
31
|
+
return 100 - 100 / (1 + rs)
|
|
32
|
+
|
|
33
|
+
def _atr(high, low, close, period=14):
|
|
34
|
+
tr = np.maximum(high - low,
|
|
35
|
+
np.maximum(np.abs(high - np.roll(close, 1)),
|
|
36
|
+
np.abs(low - np.roll(close, 1))))
|
|
37
|
+
return _rolling(tr, period, np.mean)
|
|
38
|
+
|
|
39
|
+
def _bollinger(close, period=20):
|
|
40
|
+
ma = _rolling(close, period, np.mean)
|
|
41
|
+
std = _rolling(close, period, np.std)
|
|
42
|
+
upper = ma + 2 * std
|
|
43
|
+
lower = ma - 2 * std
|
|
44
|
+
pct_b = _safe_div(close - lower, upper - lower + 1e-9)
|
|
45
|
+
width = _safe_div(upper - lower, ma + 1e-9)
|
|
46
|
+
return pct_b, width
|
|
47
|
+
|
|
48
|
+
def _macd(close, fast=12, slow=26, signal=9):
|
|
49
|
+
ema_f = _ema(close, fast)
|
|
50
|
+
ema_s = _ema(close, slow)
|
|
51
|
+
macd = ema_f - ema_s
|
|
52
|
+
sig = _ema(np.nan_to_num(macd), signal)
|
|
53
|
+
return macd, sig, macd - sig
|
|
54
|
+
|
|
55
|
+
def _stochastic(high, low, close, k=14, d=3):
|
|
56
|
+
lowest = _rolling(low, k, np.min)
|
|
57
|
+
highest = _rolling(high, k, np.max)
|
|
58
|
+
stoch_k = _safe_div(close - lowest, highest - lowest + 1e-9) * 100
|
|
59
|
+
stoch_d = _rolling(stoch_k, d, np.mean)
|
|
60
|
+
return stoch_k, stoch_d
|
|
61
|
+
|
|
62
|
+
def _williams_r(high, low, close, period=14):
|
|
63
|
+
highest = _rolling(high, period, np.max)
|
|
64
|
+
lowest = _rolling(low, period, np.min)
|
|
65
|
+
return _safe_div(highest - close, highest - lowest + 1e-9) * -100
|
|
66
|
+
|
|
67
|
+
def _cci(high, low, close, period=20):
|
|
68
|
+
tp = (high + low + close) / 3
|
|
69
|
+
ma = _rolling(tp, period, np.mean)
|
|
70
|
+
mad = _rolling(tp, period, lambda x: np.mean(np.abs(x - np.mean(x))))
|
|
71
|
+
return _safe_div(tp - ma, 0.015 * mad + 1e-9)
|
|
72
|
+
|
|
73
|
+
def _obv(close, volume):
|
|
74
|
+
direction = np.sign(np.diff(close, prepend=close[0]))
|
|
75
|
+
return np.cumsum(direction * volume)
|
|
76
|
+
|
|
77
|
+
def _vwap(high, low, close, volume):
|
|
78
|
+
tp = (high + low + close) / 3
|
|
79
|
+
cvol = np.cumsum(volume)
|
|
80
|
+
ctpv = np.cumsum(tp * volume)
|
|
81
|
+
return _safe_div(ctpv, cvol + 1e-9)
|
|
82
|
+
|
|
83
|
+
def build_features(opens: np.ndarray, highs: np.ndarray,
|
|
84
|
+
lows: np.ndarray, closes: np.ndarray,
|
|
85
|
+
volume: np.ndarray = None) -> np.ndarray:
|
|
86
|
+
"""
|
|
87
|
+
Bangun 30 fitur ML dari data OHLCV.
|
|
88
|
+
Returns: np.ndarray shape (n, 30)
|
|
89
|
+
"""
|
|
90
|
+
n = len(closes)
|
|
91
|
+
if volume is None:
|
|
92
|
+
volume = np.ones(n)
|
|
93
|
+
|
|
94
|
+
# Price returns
|
|
95
|
+
ret1 = _safe_div(np.diff(closes, prepend=closes[0]), closes)
|
|
96
|
+
ret5 = _safe_div(closes - np.roll(closes, 5), closes)
|
|
97
|
+
ret10 = _safe_div(closes - np.roll(closes, 10), closes)
|
|
98
|
+
ret20 = _safe_div(closes - np.roll(closes, 20), closes)
|
|
99
|
+
|
|
100
|
+
# Trend
|
|
101
|
+
ema8 = _ema(closes, 8)
|
|
102
|
+
ema21 = _ema(closes, 21)
|
|
103
|
+
ema50 = _ema(closes, 50)
|
|
104
|
+
p_e8 = _safe_div(closes - ema8, closes)
|
|
105
|
+
p_e21 = _safe_div(closes - ema21, closes)
|
|
106
|
+
p_e50 = _safe_div(closes - ema50, closes)
|
|
107
|
+
e8_e21 = _safe_div(ema8 - ema21, closes)
|
|
108
|
+
e21_e50= _safe_div(ema21 - ema50, closes)
|
|
109
|
+
|
|
110
|
+
# Momentum
|
|
111
|
+
rsi14 = _rsi(closes, 14)
|
|
112
|
+
rsi7 = _rsi(closes, 7)
|
|
113
|
+
rsi21 = _rsi(closes, 21)
|
|
114
|
+
_, _, macd_hist = _macd(closes)
|
|
115
|
+
macd_n = _safe_div(macd_hist, closes)
|
|
116
|
+
stoch_k, stoch_d = _stochastic(highs, lows, closes)
|
|
117
|
+
will_r = _williams_r(highs, lows, closes)
|
|
118
|
+
cci = _cci(highs, lows, closes)
|
|
119
|
+
|
|
120
|
+
# Volatility
|
|
121
|
+
atr_n = _safe_div(_atr(highs, lows, closes, 14), closes)
|
|
122
|
+
bb_pct, bb_width = _bollinger(closes)
|
|
123
|
+
vs5 = _rolling(ret1, 5, np.std)
|
|
124
|
+
vs20 = _rolling(ret1, 20, np.std)
|
|
125
|
+
|
|
126
|
+
# Volume
|
|
127
|
+
vol_ma = _rolling(volume, 20, np.mean)
|
|
128
|
+
vol_n = _safe_div(volume, vol_ma + 1)
|
|
129
|
+
obv_v = _obv(closes, volume)
|
|
130
|
+
obv_n = _safe_div(obv_v, np.abs(obv_v).max() + 1e-9)
|
|
131
|
+
vwap_d = _safe_div(closes - _vwap(highs, lows, closes, volume), closes)
|
|
132
|
+
|
|
133
|
+
# Candle pattern
|
|
134
|
+
body = np.abs(closes - opens)
|
|
135
|
+
rng = highs - lows + 1e-9
|
|
136
|
+
body_p = _safe_div(body, rng)
|
|
137
|
+
upper = highs - np.maximum(opens, closes)
|
|
138
|
+
lower = np.minimum(opens, closes) - lows
|
|
139
|
+
up_p = _safe_div(upper, rng)
|
|
140
|
+
lo_p = _safe_div(lower, rng)
|
|
141
|
+
is_bull= (closes > opens).astype(float)
|
|
142
|
+
|
|
143
|
+
# Position in range
|
|
144
|
+
h20 = _rolling(highs, 20, np.max)
|
|
145
|
+
l20 = _rolling(lows, 20, np.min)
|
|
146
|
+
pos_r = _safe_div(closes - l20, h20 - l20 + 1e-9)
|
|
147
|
+
|
|
148
|
+
features = np.column_stack([
|
|
149
|
+
ret1, ret5, ret10, ret20,
|
|
150
|
+
p_e8, p_e21, p_e50, e8_e21, e21_e50,
|
|
151
|
+
rsi14, rsi7, rsi21,
|
|
152
|
+
macd_n, stoch_k, stoch_d, will_r, cci,
|
|
153
|
+
atr_n, bb_pct, bb_width, vs5, vs20,
|
|
154
|
+
vol_n, obv_n, vwap_d,
|
|
155
|
+
body_p, up_p, lo_p, is_bull,
|
|
156
|
+
pos_r,
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
return np.nan_to_num(features, nan=0.0, posinf=0.0, neginf=0.0)
|
|
160
|
+
|
|
161
|
+
def build_labels(closes: np.ndarray,
|
|
162
|
+
forward: int = 3,
|
|
163
|
+
threshold: float = 0.005) -> np.ndarray:
|
|
164
|
+
"""
|
|
165
|
+
Label: 1=BUY, -1=SELL, 0=HOLD
|
|
166
|
+
Berdasarkan pergerakan harga forward bar ke depan.
|
|
167
|
+
"""
|
|
168
|
+
labels = np.zeros(len(closes), dtype=int)
|
|
169
|
+
for i in range(len(closes) - forward):
|
|
170
|
+
ret = (closes[i+forward] - closes[i]) / (closes[i] + 1e-9)
|
|
171
|
+
if ret > threshold: labels[i] = 1
|
|
172
|
+
elif ret < -threshold: labels[i] = -1
|
|
173
|
+
return labels
|
|
174
|
+
|
|
175
|
+
def feature_names() -> list:
|
|
176
|
+
return [
|
|
177
|
+
"ret1","ret5","ret10","ret20",
|
|
178
|
+
"p_ema8","p_ema21","p_ema50","ema8_21","ema21_50",
|
|
179
|
+
"rsi14","rsi7","rsi21",
|
|
180
|
+
"macd","stoch_k","stoch_d","williams_r","cci",
|
|
181
|
+
"atr","bb_pct","bb_width","vol_std5","vol_std20",
|
|
182
|
+
"vol_norm","obv_norm","vwap_dist",
|
|
183
|
+
"body_pct","upper_wick","lower_wick","is_bull",
|
|
184
|
+
"pos_range",
|
|
185
|
+
]
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade ML — 3 Model Spesialis + Ensemble
|
|
3
|
+
Price Predictor + Pattern Classifier + Signal Filter
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import os
|
|
7
|
+
import pickle
|
|
8
|
+
from nextrade.ml.features import build_features, build_labels, feature_names
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
|
|
12
|
+
from sklearn.linear_model import LogisticRegression
|
|
13
|
+
from sklearn.preprocessing import StandardScaler
|
|
14
|
+
from sklearn.calibration import CalibratedClassifierCV
|
|
15
|
+
SKLEARN_OK = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
SKLEARN_OK = False
|
|
18
|
+
|
|
19
|
+
MODEL_PATH = os.path.join(os.path.dirname(__file__), "saved_model.pkl")
|
|
20
|
+
|
|
21
|
+
class NexTradeML:
|
|
22
|
+
"""
|
|
23
|
+
Engine ML NexTrade — 3 spesialis + ensemble.
|
|
24
|
+
|
|
25
|
+
Cara pakai:
|
|
26
|
+
ml = NexTradeML()
|
|
27
|
+
ml.train(opens, highs, lows, closes, volume)
|
|
28
|
+
signal, confidence, proba = ml.predict(opens, highs, lows, closes)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
if not SKLEARN_OK:
|
|
33
|
+
raise ImportError("scikit-learn belum terinstall. "
|
|
34
|
+
"Jalankan: pip install scikit-learn")
|
|
35
|
+
self.scaler = StandardScaler()
|
|
36
|
+
self.is_trained = False
|
|
37
|
+
self.weights = np.array([0.4, 0.35, 0.25]) # bobot awal
|
|
38
|
+
self.train_history = []
|
|
39
|
+
self._build_models()
|
|
40
|
+
|
|
41
|
+
def _build_models(self):
|
|
42
|
+
# Model 1: Price Predictor — Random Forest
|
|
43
|
+
self.price_model = RandomForestClassifier(
|
|
44
|
+
n_estimators = 200,
|
|
45
|
+
max_depth = 8,
|
|
46
|
+
min_samples_leaf= 5,
|
|
47
|
+
class_weight = "balanced",
|
|
48
|
+
random_state = 42,
|
|
49
|
+
n_jobs = -1,
|
|
50
|
+
)
|
|
51
|
+
# Model 2: Pattern Classifier — Gradient Boosting
|
|
52
|
+
self.pattern_model = GradientBoostingClassifier(
|
|
53
|
+
n_estimators = 150,
|
|
54
|
+
max_depth = 5,
|
|
55
|
+
learning_rate = 0.05,
|
|
56
|
+
subsample = 0.8,
|
|
57
|
+
random_state = 42,
|
|
58
|
+
)
|
|
59
|
+
# Model 3: Signal Filter — Logistic Regression
|
|
60
|
+
self.filter_model = LogisticRegression(
|
|
61
|
+
C = 1.0,
|
|
62
|
+
class_weight = "balanced",
|
|
63
|
+
max_iter = 500,
|
|
64
|
+
random_state = 42,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def train(self, opens: np.ndarray, highs: np.ndarray,
|
|
68
|
+
lows: np.ndarray, closes: np.ndarray,
|
|
69
|
+
volume: np.ndarray = None,
|
|
70
|
+
forward: int = 3,
|
|
71
|
+
threshold: float = 0.005,
|
|
72
|
+
silent: bool = False) -> dict:
|
|
73
|
+
"""
|
|
74
|
+
Latih semua model dengan data OHLCV.
|
|
75
|
+
Returns dict hasil training.
|
|
76
|
+
"""
|
|
77
|
+
if not silent:
|
|
78
|
+
print(f"\n \033[36m⠿\033[0m Membangun {len(closes):,} fitur ML...")
|
|
79
|
+
|
|
80
|
+
X = build_features(opens, highs, lows, closes, volume)
|
|
81
|
+
y = build_labels(closes, forward=forward, threshold=threshold)
|
|
82
|
+
|
|
83
|
+
# Potong label terakhir yang tidak valid
|
|
84
|
+
valid = len(closes) - forward
|
|
85
|
+
X, y = X[:valid], y[:valid]
|
|
86
|
+
|
|
87
|
+
# Split train/val
|
|
88
|
+
split = int(valid * 0.8)
|
|
89
|
+
X_tr, X_val = X[:split], X[split:]
|
|
90
|
+
y_tr, y_val = y[:split], y[split:]
|
|
91
|
+
|
|
92
|
+
# Scale
|
|
93
|
+
X_tr_s = self.scaler.fit_transform(X_tr)
|
|
94
|
+
X_val_s = self.scaler.transform(X_val)
|
|
95
|
+
|
|
96
|
+
if not silent:
|
|
97
|
+
dist = {1: int((y_tr==1).sum()), -1: int((y_tr==-1).sum()),
|
|
98
|
+
0: int((y_tr==0).sum())}
|
|
99
|
+
print(f" \033[36m⠿\033[0m Label: BUY={dist[1]} SELL={dist[-1]} HOLD={dist[0]}")
|
|
100
|
+
print(f" \033[36m⠿\033[0m Training 3 model spesialis...")
|
|
101
|
+
|
|
102
|
+
# Train semua model
|
|
103
|
+
self.price_model.fit(X_tr_s, y_tr)
|
|
104
|
+
if not silent: print(f" \033[32m✓\033[0m Price Predictor (RandomForest) selesai")
|
|
105
|
+
|
|
106
|
+
self.pattern_model.fit(X_tr_s, y_tr)
|
|
107
|
+
if not silent: print(f" \033[32m✓\033[0m Pattern Classifier (GradientBoosting) selesai")
|
|
108
|
+
|
|
109
|
+
self.filter_model.fit(X_tr_s, y_tr)
|
|
110
|
+
if not silent: print(f" \033[32m✓\033[0m Signal Filter (LogisticRegression) selesai")
|
|
111
|
+
|
|
112
|
+
# Evaluasi dan update bobot adaptif
|
|
113
|
+
acc1 = self.price_model.score(X_val_s, y_val)
|
|
114
|
+
acc2 = self.pattern_model.score(X_val_s, y_val)
|
|
115
|
+
acc3 = self.filter_model.score(X_val_s, y_val)
|
|
116
|
+
|
|
117
|
+
# Bobot proporsional ke akurasi
|
|
118
|
+
accs = np.array([acc1, acc2, acc3])
|
|
119
|
+
self.weights = accs / (accs.sum() + 1e-9)
|
|
120
|
+
|
|
121
|
+
# Hitung winrate keseluruhan
|
|
122
|
+
preds_val = self._ensemble_predict(X_val_s)
|
|
123
|
+
winrate = float(np.mean(preds_val == y_val) * 100)
|
|
124
|
+
|
|
125
|
+
result = {
|
|
126
|
+
"winrate" : round(winrate, 2),
|
|
127
|
+
"acc_price" : round(acc1 * 100, 2),
|
|
128
|
+
"acc_pattern" : round(acc2 * 100, 2),
|
|
129
|
+
"acc_filter" : round(acc3 * 100, 2),
|
|
130
|
+
"weights" : self.weights.tolist(),
|
|
131
|
+
"samples_train" : split,
|
|
132
|
+
"samples_val" : valid - split,
|
|
133
|
+
"features" : X.shape[1],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
self.is_trained = True
|
|
137
|
+
self.train_history.append(result)
|
|
138
|
+
self._save()
|
|
139
|
+
|
|
140
|
+
if not silent:
|
|
141
|
+
print(f"\n \033[1m\033[37m Hasil Training ML\033[0m")
|
|
142
|
+
print(f" \033[36m{'─' * 44}\033[0m")
|
|
143
|
+
print(f" \033[1mWinrate Overall :\033[0m \033[33m{winrate:.1f}%\033[0m")
|
|
144
|
+
print(f" \033[1mPrice Predictor :\033[0m {acc1*100:.1f}% "
|
|
145
|
+
f"\033[2m(bobot {self.weights[0]:.2f})\033[0m")
|
|
146
|
+
print(f" \033[1mPattern Classif. :\033[0m {acc2*100:.1f}% "
|
|
147
|
+
f"\033[2m(bobot {self.weights[1]:.2f})\033[0m")
|
|
148
|
+
print(f" \033[1mSignal Filter :\033[0m {acc3*100:.1f}% "
|
|
149
|
+
f"\033[2m(bobot {self.weights[2]:.2f})\033[0m")
|
|
150
|
+
print(f" \033[36m{'─' * 44}\033[0m\n")
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def predict(self, opens: np.ndarray, highs: np.ndarray,
|
|
155
|
+
lows: np.ndarray, closes: np.ndarray,
|
|
156
|
+
volume: np.ndarray = None) -> tuple:
|
|
157
|
+
"""
|
|
158
|
+
Prediksi sinyal untuk candle terbaru.
|
|
159
|
+
Returns: (signal, confidence, proba_dict)
|
|
160
|
+
signal: 'BUY' | 'SELL' | 'HOLD'
|
|
161
|
+
"""
|
|
162
|
+
if not self.is_trained:
|
|
163
|
+
return "HOLD", 50.0, {"BUY": 0.33, "SELL": 0.33, "HOLD": 0.34}
|
|
164
|
+
|
|
165
|
+
X = build_features(opens, highs, lows, closes, volume)
|
|
166
|
+
X_s = self.scaler.transform(X[-1:])
|
|
167
|
+
|
|
168
|
+
pred = self._ensemble_predict(X_s)[0]
|
|
169
|
+
probas = self._ensemble_proba(X_s)[0]
|
|
170
|
+
|
|
171
|
+
# Map label ke signal
|
|
172
|
+
label_map = {1: "BUY", -1: "SELL", 0: "HOLD"}
|
|
173
|
+
signal = label_map.get(pred, "HOLD")
|
|
174
|
+
|
|
175
|
+
# Confidence = probabilitas kelas yang dipilih * 100
|
|
176
|
+
classes = list(self.price_model.classes_)
|
|
177
|
+
idx = classes.index(pred) if pred in classes else 0
|
|
178
|
+
confidence = float(probas[idx] * 100)
|
|
179
|
+
|
|
180
|
+
proba_dict = {}
|
|
181
|
+
for i, cls in enumerate(classes):
|
|
182
|
+
key = label_map.get(cls, "HOLD")
|
|
183
|
+
proba_dict[key] = round(float(probas[i]), 4)
|
|
184
|
+
|
|
185
|
+
return signal, round(confidence, 2), proba_dict
|
|
186
|
+
|
|
187
|
+
def _ensemble_predict(self, X_s: np.ndarray) -> np.ndarray:
|
|
188
|
+
"""Gabungkan prediksi 3 model dengan bobot adaptif."""
|
|
189
|
+
p1 = self._get_proba(self.price_model, X_s)
|
|
190
|
+
p2 = self._get_proba(self.pattern_model, X_s)
|
|
191
|
+
p3 = self._get_proba(self.filter_model, X_s)
|
|
192
|
+
combined = (p1 * self.weights[0] +
|
|
193
|
+
p2 * self.weights[1] +
|
|
194
|
+
p3 * self.weights[2])
|
|
195
|
+
classes = list(self.price_model.classes_)
|
|
196
|
+
return np.array([classes[i] for i in combined.argmax(axis=1)])
|
|
197
|
+
|
|
198
|
+
def _ensemble_proba(self, X_s: np.ndarray) -> np.ndarray:
|
|
199
|
+
p1 = self._get_proba(self.price_model, X_s)
|
|
200
|
+
p2 = self._get_proba(self.pattern_model, X_s)
|
|
201
|
+
p3 = self._get_proba(self.filter_model, X_s)
|
|
202
|
+
return (p1 * self.weights[0] +
|
|
203
|
+
p2 * self.weights[1] +
|
|
204
|
+
p3 * self.weights[2])
|
|
205
|
+
|
|
206
|
+
def _get_proba(self, model, X_s):
|
|
207
|
+
try:
|
|
208
|
+
return model.predict_proba(X_s)
|
|
209
|
+
except Exception:
|
|
210
|
+
preds = model.predict(X_s)
|
|
211
|
+
classes = list(model.classes_)
|
|
212
|
+
out = np.zeros((len(preds), len(classes)))
|
|
213
|
+
for i, p in enumerate(preds):
|
|
214
|
+
out[i, classes.index(p)] = 1.0
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
def _save(self):
|
|
218
|
+
try:
|
|
219
|
+
with open(MODEL_PATH, "wb") as f:
|
|
220
|
+
pickle.dump({
|
|
221
|
+
"price_model" : self.price_model,
|
|
222
|
+
"pattern_model": self.pattern_model,
|
|
223
|
+
"filter_model" : self.filter_model,
|
|
224
|
+
"scaler" : self.scaler,
|
|
225
|
+
"weights" : self.weights,
|
|
226
|
+
"history" : self.train_history,
|
|
227
|
+
}, f)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def load(self) -> bool:
|
|
232
|
+
try:
|
|
233
|
+
with open(MODEL_PATH, "rb") as f:
|
|
234
|
+
saved = pickle.load(f)
|
|
235
|
+
self.price_model = saved["price_model"]
|
|
236
|
+
self.pattern_model = saved["pattern_model"]
|
|
237
|
+
self.filter_model = saved["filter_model"]
|
|
238
|
+
self.scaler = saved["scaler"]
|
|
239
|
+
self.weights = saved["weights"]
|
|
240
|
+
self.train_history = saved.get("history", [])
|
|
241
|
+
self.is_trained = True
|
|
242
|
+
return True
|
|
243
|
+
except Exception:
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def winrate(self) -> float:
|
|
248
|
+
if self.train_history:
|
|
249
|
+
return self.train_history[-1]["winrate"]
|
|
250
|
+
return 0.0
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextrade-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: AI Trading Engine — Ringan, Multi-Market, Bisa di HP
|
|
5
5
|
Author-email: NexTrade <wafiqrazy035@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
15
15
|
Requires-Dist: yfinance
|
|
16
|
+
Requires-Dist: scikit-learn
|
|
16
17
|
Dynamic: license-file
|
|
17
18
|
|
|
18
19
|
# NexTrade 🧠
|
{nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade_engine.egg-info/SOURCES.txt
RENAMED
|
@@ -14,6 +14,9 @@ nextrade/nextrade/indicators/__init__.py
|
|
|
14
14
|
nextrade/nextrade/indicators/adaptive.py
|
|
15
15
|
nextrade/nextrade/indicators/confluence.py
|
|
16
16
|
nextrade/nextrade/indicators/pattern.py
|
|
17
|
+
nextrade/nextrade/ml/__init__.py
|
|
18
|
+
nextrade/nextrade/ml/features.py
|
|
19
|
+
nextrade/nextrade/ml/model.py
|
|
17
20
|
nextrade/nextrade/utils/__init__.py
|
|
18
21
|
nextrade/nextrade/utils/terminal_ui.py
|
|
19
22
|
nextrade/nextrade_engine.egg-info/PKG-INFO
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nextrade-engine"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "AI Trading Engine — Ringan, Multi-Market, Bisa di HP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -21,6 +21,7 @@ requires-python = ">=3.8"
|
|
|
21
21
|
dependencies = [
|
|
22
22
|
"numpy",
|
|
23
23
|
"yfinance",
|
|
24
|
+
"scikit-learn",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
[tool.setuptools]
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
NexTrade — AI Trading Engine
|
|
3
|
-
import nextrade → wizard muncul otomatis di terminal
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from nextrade.lang import set_language, list_languages, get, current as _lang
|
|
7
|
-
from nextrade.utils.terminal_ui import (
|
|
8
|
-
print_welcome, print_signal, print_error,
|
|
9
|
-
print_loading, print_success, list_strategies, STRATEGIES
|
|
10
|
-
)
|
|
11
|
-
from nextrade.indicators.confluence import confluence_score
|
|
12
|
-
from nextrade.core.regime import detect_regime
|
|
13
|
-
from nextrade.core.brain import Brain
|
|
14
|
-
from nextrade.data.fetcher import fetch, fetch_multi, list_markets
|
|
15
|
-
import numpy as np
|
|
16
|
-
|
|
17
|
-
__version__ = "0.6.0"
|
|
18
|
-
__author__ = "NexTrade"
|
|
19
|
-
|
|
20
|
-
# Tampilkan wizard saat pertama kali import
|
|
21
|
-
print_welcome()
|
|
22
|
-
|
|
23
|
-
# Instance brain global
|
|
24
|
-
_brain = Brain.__new__(Brain)
|
|
25
|
-
_brain.state = None
|
|
26
|
-
|
|
27
|
-
def _get_brain():
|
|
28
|
-
global _brain
|
|
29
|
-
if _brain.state is None:
|
|
30
|
-
_brain = Brain()
|
|
31
|
-
return _brain
|
|
32
|
-
|
|
33
|
-
# ─────────────────────────────────────────
|
|
34
|
-
# PUBLIC API
|
|
35
|
-
# ─────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
def start(strategy: str = "adaptive",
|
|
38
|
-
market: str = "BTCUSD",
|
|
39
|
-
timeframe: str = "H1",
|
|
40
|
-
data: dict = None):
|
|
41
|
-
"""
|
|
42
|
-
Jalankan NexTrade dengan strategi pilihan.
|
|
43
|
-
|
|
44
|
-
Contoh:
|
|
45
|
-
nextrade.start("momentum", market="BTCUSD")
|
|
46
|
-
nextrade.start("reversal", market="EURUSD", timeframe="H4")
|
|
47
|
-
"""
|
|
48
|
-
strategy = strategy.lower().strip()
|
|
49
|
-
|
|
50
|
-
if strategy not in STRATEGIES:
|
|
51
|
-
print_error(f"{get('error_strategy')}: {', '.join(STRATEGIES)}")
|
|
52
|
-
list_strategies()
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
print_loading(f"{get('loading_strategy')} {strategy.upper()}...")
|
|
56
|
-
|
|
57
|
-
if data is None:
|
|
58
|
-
data = fetch(market, timeframe, bars=500)
|
|
59
|
-
if data is None:
|
|
60
|
-
print_error(get('error_data'))
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
print_loading(f"{get('loading_indicators')}...")
|
|
64
|
-
|
|
65
|
-
result = confluence_score(
|
|
66
|
-
opens = data["open"],
|
|
67
|
-
highs = data["high"],
|
|
68
|
-
lows = data["low"],
|
|
69
|
-
closes = data["close"],
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
brain = _get_brain()
|
|
73
|
-
threshold = brain.get_confidence_threshold()
|
|
74
|
-
confidence = result["confidence"]
|
|
75
|
-
|
|
76
|
-
if confidence >= threshold and result["details"]["pattern"]["direction"] != "bear":
|
|
77
|
-
signal = "BUY"
|
|
78
|
-
elif confidence <= (100 - threshold) or result["details"]["pattern"]["direction"] == "bear":
|
|
79
|
-
signal = "SELL"
|
|
80
|
-
else:
|
|
81
|
-
signal = "HOLD"
|
|
82
|
-
|
|
83
|
-
print_signal(signal=signal, confidence=confidence,
|
|
84
|
-
strategy=strategy, market=market)
|
|
85
|
-
|
|
86
|
-
regime = result["details"]["regime"]["regime"]
|
|
87
|
-
rsi = result["details"]["rsi"]
|
|
88
|
-
mom = result["details"]["momentum"]
|
|
89
|
-
price = data["price_now"]
|
|
90
|
-
|
|
91
|
-
print(f" \033[2m{get('detail_analysis')}:\033[0m")
|
|
92
|
-
print(f" \033[36m{get('price_now'):<18}:\033[0m {price:,.5f}")
|
|
93
|
-
print(f" \033[36m{get('market_condition'):<18}:\033[0m {get(regime)}")
|
|
94
|
-
print(f" \033[36mRSI Adaptif :\033[0m {rsi:.1f}")
|
|
95
|
-
print(f" \033[36m{get('iq_model'):<18}:\033[0m {brain.state['iq']} / 100")
|
|
96
|
-
print(f" \033[36m{get('agresivitas'):<18}:\033[0m {brain.state['agresivitas']}%")
|
|
97
|
-
print()
|
|
98
|
-
|
|
99
|
-
return {**result, "signal": signal, "market": market,
|
|
100
|
-
"price": price, "strategy": strategy}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def train(mode: str = "hybrid", market: str = "BTCUSD",
|
|
104
|
-
timeframe: str = "D1", bars: int = 1000):
|
|
105
|
-
"""Latih brain NexTrade dengan data real market."""
|
|
106
|
-
print(f"\n \033[1m\033[37m Training NexTrade Brain\033[0m")
|
|
107
|
-
print(f" \033[36m{'─' * 40}\033[0m")
|
|
108
|
-
data = fetch(market, timeframe, bars=bars)
|
|
109
|
-
if data is None:
|
|
110
|
-
print_error(get('error_data'))
|
|
111
|
-
return
|
|
112
|
-
_get_brain().train(mode=mode, data=data)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def brain_status():
|
|
116
|
-
"""Tampilkan status otak NexTrade."""
|
|
117
|
-
_get_brain().status()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def set_agresivitas(pct: int):
|
|
121
|
-
"""Set agresivitas sinyal (0-100)."""
|
|
122
|
-
_get_brain().set_agresivitas(pct)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def scan(markets: list = None, timeframe: str = "H1"):
|
|
126
|
-
"""Scan banyak market sekaligus."""
|
|
127
|
-
if markets is None:
|
|
128
|
-
markets = ["BTCUSD", "EURUSD", "XAUUSD", "AAPL", "TSLA"]
|
|
129
|
-
|
|
130
|
-
print(f"\n \033[1m\033[37m NexTrade Market Scanner\033[0m")
|
|
131
|
-
print(f" \033[36m{'─' * 55}\033[0m")
|
|
132
|
-
print(f" \033[2m{'MARKET':<12} {'HARGA':>14} {get('signal'):>8} "
|
|
133
|
-
f"{'CONFIDENCE':>12} {get('market_condition')}\033[0m")
|
|
134
|
-
print(f" \033[36m{'─' * 55}\033[0m")
|
|
135
|
-
|
|
136
|
-
results = []
|
|
137
|
-
for market in markets:
|
|
138
|
-
data = fetch(market, timeframe, bars=300, silent=True)
|
|
139
|
-
if data is None: continue
|
|
140
|
-
result = confluence_score(
|
|
141
|
-
opens=data["open"], highs=data["high"],
|
|
142
|
-
lows=data["low"], closes=data["close"],
|
|
143
|
-
)
|
|
144
|
-
signal = result["signal"]
|
|
145
|
-
confidence = result["confidence"]
|
|
146
|
-
regime = result["details"]["regime"]["regime"]
|
|
147
|
-
price = data["price_now"]
|
|
148
|
-
sc = "\033[32m" if signal=="BUY" else "\033[31m" if signal=="SELL" else "\033[33m"
|
|
149
|
-
print(f" {market:<12} {price:>14,.5f} {sc}{signal:>8}\033[0m "
|
|
150
|
-
f"{confidence:>10.1f}% \033[2m{regime}\033[0m")
|
|
151
|
-
results.append({**result, "market": market, "price": price})
|
|
152
|
-
|
|
153
|
-
print(f" \033[36m{'─' * 55}\033[0m\n")
|
|
154
|
-
buys = [r for r in results if r["signal"]=="BUY"]
|
|
155
|
-
sells = [r for r in results if r["signal"]=="SELL"]
|
|
156
|
-
if buys:
|
|
157
|
-
best = max(buys, key=lambda x: x["confidence"])
|
|
158
|
-
print(f" \033[32m★ BUY : {best['market']} ({best['confidence']:.1f}%)\033[0m")
|
|
159
|
-
if sells:
|
|
160
|
-
best = max(sells, key=lambda x: x["confidence"])
|
|
161
|
-
print(f" \033[31m★ SELL : {best['market']} ({best['confidence']:.1f}%)\033[0m")
|
|
162
|
-
print()
|
|
163
|
-
return results
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def backtest(strategy: str = "adaptive", market: str = "BTCUSD",
|
|
167
|
-
timeframe: str = "D1", bars: int = 500,
|
|
168
|
-
agresivitas: int = 30, sl_pct: float = 2.0, tp_pct: float = 4.0):
|
|
169
|
-
"""Backtest strategi dengan data real historis."""
|
|
170
|
-
from nextrade.backtest.engine import run as _run
|
|
171
|
-
data = fetch(market, timeframe, bars=bars)
|
|
172
|
-
if data is None:
|
|
173
|
-
print_error(get('error_data'))
|
|
174
|
-
return
|
|
175
|
-
return _run(data=data, strategy=strategy, agresivitas=agresivitas,
|
|
176
|
-
sl_pct=sl_pct, tp_pct=tp_pct)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def info():
|
|
180
|
-
"""Tampilkan semua strategi dan market."""
|
|
181
|
-
list_strategies()
|
|
182
|
-
list_markets()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def regime(market: str = "BTCUSD", timeframe: str = "H1") -> dict:
|
|
186
|
-
"""Deteksi kondisi pasar saat ini."""
|
|
187
|
-
data = fetch(market, timeframe, bars=100, silent=True)
|
|
188
|
-
if data is None: return {}
|
|
189
|
-
return detect_regime(data["close"])
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def language():
|
|
193
|
-
"""Tampilkan semua bahasa yang tersedia."""
|
|
194
|
-
list_languages()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nextrade_engine-0.6.0 → nextrade_engine-0.7.0}/nextrade/nextrade_engine.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|