nextrade-engine 0.5.0__py3-none-any.whl
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/nextrade/__init__.py +277 -0
- nextrade/nextrade/backtest/__init__.py +0 -0
- nextrade/nextrade/backtest/engine.py +249 -0
- nextrade/nextrade/core/__init__.py +0 -0
- nextrade/nextrade/core/brain.py +278 -0
- nextrade/nextrade/core/regime.py +63 -0
- nextrade/nextrade/data/__init__.py +0 -0
- nextrade/nextrade/data/fetcher.py +173 -0
- nextrade/nextrade/indicators/__init__.py +0 -0
- nextrade/nextrade/indicators/adaptive.py +73 -0
- nextrade/nextrade/indicators/confluence.py +61 -0
- nextrade/nextrade/indicators/pattern.py +76 -0
- nextrade/nextrade/utils/__init__.py +0 -0
- nextrade/nextrade/utils/terminal_ui.py +87 -0
- nextrade/setup.py +10 -0
- nextrade_engine-0.5.0.dist-info/METADATA +97 -0
- nextrade_engine-0.5.0.dist-info/RECORD +20 -0
- nextrade_engine-0.5.0.dist-info/WHEEL +5 -0
- nextrade_engine-0.5.0.dist-info/licenses/LICENSE +21 -0
- nextrade_engine-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade — Brain
|
|
3
|
+
File otak utama NexTrade. Mengatur IQ model, neuron aktif,
|
|
4
|
+
mode learning, dan agresivitas sinyal.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import numpy as np
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
# Path simpan state otak
|
|
13
|
+
BRAIN_FILE = os.path.join(os.path.dirname(__file__), "brain_state.json")
|
|
14
|
+
|
|
15
|
+
# ─────────────────────────────────────────
|
|
16
|
+
# DEFAULT STATE OTAK
|
|
17
|
+
# ─────────────────────────────────────────
|
|
18
|
+
DEFAULT_STATE = {
|
|
19
|
+
"iq" : 50, # IQ awal model (0-100)
|
|
20
|
+
"neurons_active" : 100, # neuron aktif saat ini
|
|
21
|
+
"neurons_total" : 2000, # total neuron maksimal
|
|
22
|
+
"agresivitas" : 30, # % agresivitas sinyal (0-100)
|
|
23
|
+
"mode" : "hybrid", # offline / online / hybrid
|
|
24
|
+
"total_predictions": 0, # total prediksi yang pernah dibuat
|
|
25
|
+
"correct_predictions": 0, # prediksi yang benar
|
|
26
|
+
"last_updated" : "", # kapan terakhir belajar
|
|
27
|
+
"learning_sessions": 0, # berapa kali sudah belajar
|
|
28
|
+
"weights" : {}, # bobot internal model
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────
|
|
32
|
+
# LABEL AGRESIVITAS
|
|
33
|
+
# ─────────────────────────────────────────
|
|
34
|
+
def _agresivitas_label(pct: int) -> str:
|
|
35
|
+
if pct <= 30: return "Konservatif"
|
|
36
|
+
elif pct <= 60: return "Moderat"
|
|
37
|
+
else: return "Agresif"
|
|
38
|
+
|
|
39
|
+
def _iq_label(iq: int) -> str:
|
|
40
|
+
if iq < 30: return "Pemula"
|
|
41
|
+
elif iq < 50: return "Berkembang"
|
|
42
|
+
elif iq < 70: return "Cukup Cerdas"
|
|
43
|
+
elif iq < 85: return "Cerdas"
|
|
44
|
+
elif iq < 95: return "Sangat Cerdas"
|
|
45
|
+
else: return "Jenius"
|
|
46
|
+
|
|
47
|
+
def _bar(value: int, total: int = 100, width: int = 16) -> str:
|
|
48
|
+
filled = int((value / total) * width)
|
|
49
|
+
return "█" * filled + "░" * (width - filled)
|
|
50
|
+
|
|
51
|
+
# ─────────────────────────────────────────
|
|
52
|
+
# LOAD / SAVE STATE
|
|
53
|
+
# ─────────────────────────────────────────
|
|
54
|
+
def _load_state() -> dict:
|
|
55
|
+
if os.path.exists(BRAIN_FILE):
|
|
56
|
+
try:
|
|
57
|
+
with open(BRAIN_FILE, "r") as f:
|
|
58
|
+
saved = json.load(f)
|
|
59
|
+
state = DEFAULT_STATE.copy()
|
|
60
|
+
state.update(saved)
|
|
61
|
+
return state
|
|
62
|
+
except:
|
|
63
|
+
pass
|
|
64
|
+
return DEFAULT_STATE.copy()
|
|
65
|
+
|
|
66
|
+
def _save_state(state: dict):
|
|
67
|
+
state["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
68
|
+
with open(BRAIN_FILE, "w") as f:
|
|
69
|
+
json.dump(state, f, indent=2)
|
|
70
|
+
|
|
71
|
+
# ─────────────────────────────────────────
|
|
72
|
+
# HITUNG IQ DARI PERFORMA
|
|
73
|
+
# ─────────────────────────────────────────
|
|
74
|
+
def _calculate_iq(state: dict) -> int:
|
|
75
|
+
total = state["total_predictions"]
|
|
76
|
+
correct = state["correct_predictions"]
|
|
77
|
+
sessions = state["learning_sessions"]
|
|
78
|
+
neuron_ratio = state["neurons_active"] / state["neurons_total"]
|
|
79
|
+
|
|
80
|
+
if total == 0:
|
|
81
|
+
base_iq = 50
|
|
82
|
+
else:
|
|
83
|
+
winrate = correct / total
|
|
84
|
+
base_iq = winrate * 80 # max 80 dari winrate
|
|
85
|
+
|
|
86
|
+
# Bonus dari sesi belajar (max +10)
|
|
87
|
+
session_bonus = min(sessions * 0.5, 10)
|
|
88
|
+
|
|
89
|
+
# Bonus dari neuron aktif (max +10)
|
|
90
|
+
neuron_bonus = neuron_ratio * 10
|
|
91
|
+
|
|
92
|
+
iq = int(base_iq + session_bonus + neuron_bonus)
|
|
93
|
+
return min(max(iq, 1), 100)
|
|
94
|
+
|
|
95
|
+
# ─────────────────────────────────────────
|
|
96
|
+
# UPDATE NEURON AKTIF SETELAH BELAJAR
|
|
97
|
+
# ─────────────────────────────────────────
|
|
98
|
+
def _grow_neurons(state: dict, data_size: int):
|
|
99
|
+
"""Neuron bertambah aktif seiring data yang dipelajari."""
|
|
100
|
+
growth = min(int(data_size * 0.1), 100)
|
|
101
|
+
state["neurons_active"] = min(
|
|
102
|
+
state["neurons_active"] + growth,
|
|
103
|
+
state["neurons_total"]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# ─────────────────────────────────────────
|
|
107
|
+
# PUBLIC API
|
|
108
|
+
# ─────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
class Brain:
|
|
111
|
+
"""
|
|
112
|
+
Otak NexTrade — mengatur semua aspek model AI.
|
|
113
|
+
|
|
114
|
+
Cara pakai:
|
|
115
|
+
from nextrade.core.brain import Brain
|
|
116
|
+
brain = Brain()
|
|
117
|
+
brain.status()
|
|
118
|
+
brain.train(mode="offline", data={"close": arr})
|
|
119
|
+
brain.set_agresivitas(20)
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self):
|
|
123
|
+
self.state = _load_state()
|
|
124
|
+
self.state["iq"] = _calculate_iq(self.state)
|
|
125
|
+
_save_state(self.state)
|
|
126
|
+
|
|
127
|
+
# ── TAMPILKAN STATUS ──────────────────
|
|
128
|
+
def status(self):
|
|
129
|
+
"""Tampilkan status otak NexTrade di terminal."""
|
|
130
|
+
s = self.state
|
|
131
|
+
iq = s["iq"]
|
|
132
|
+
na = s["neurons_active"]
|
|
133
|
+
nt = s["neurons_total"]
|
|
134
|
+
ag = s["agresivitas"]
|
|
135
|
+
mode = s["mode"].upper()
|
|
136
|
+
total = s["total_predictions"]
|
|
137
|
+
correct = s["correct_predictions"]
|
|
138
|
+
winrate = (correct / total * 100) if total > 0 else 0
|
|
139
|
+
updated = s["last_updated"] or "Belum pernah belajar"
|
|
140
|
+
|
|
141
|
+
print()
|
|
142
|
+
print(" \033[36m\033[1m🧠 NexTrade Brain — Status Model\033[0m")
|
|
143
|
+
print(f" \033[36m{'─' * 45}\033[0m")
|
|
144
|
+
print(f" \033[1mIQ Model :\033[0m \033[33m{_bar(iq)}\033[0m {iq} / 100 \033[2m({_iq_label(iq)})\033[0m")
|
|
145
|
+
print(f" \033[1mNeuron Aktif :\033[0m \033[35m{na:,} / {nt:,}\033[0m terhubung \033[2m({na/nt*100:.1f}%)\033[0m")
|
|
146
|
+
print(f" \033[1mAgresivitas :\033[0m \033[32m{_bar(ag)}\033[0m {ag}% \033[2m({_agresivitas_label(ag)})\033[0m")
|
|
147
|
+
print(f" \033[1mMode Learning :\033[0m \033[36m{mode}\033[0m")
|
|
148
|
+
print(f" \033[1mWinrate :\033[0m {winrate:.1f}% \033[2m({correct}/{total} prediksi benar)\033[0m")
|
|
149
|
+
print(f" \033[1mTerakhir Belajar:\033[0m \033[2m{updated}\033[0m")
|
|
150
|
+
print(f" \033[36m{'─' * 45}\033[0m")
|
|
151
|
+
print()
|
|
152
|
+
print(" \033[2mPerintah tersedia:\033[0m")
|
|
153
|
+
print(" \033[32mbrain.train(mode='offline')\033[0m \033[2m# latih model\033[0m")
|
|
154
|
+
print(" \033[32mbrain.set_agresivitas(20)\033[0m \033[2m# set seberapa agresif\033[0m")
|
|
155
|
+
print(" \033[32mbrain.set_mode('hybrid')\033[0m \033[2m# ganti mode learning\033[0m")
|
|
156
|
+
print(" \033[32mbrain.reset()\033[0m \033[2m# reset otak dari nol\033[0m")
|
|
157
|
+
print()
|
|
158
|
+
|
|
159
|
+
# ── TRAINING ─────────────────────────
|
|
160
|
+
def train(self, mode: str = None, data: dict = None):
|
|
161
|
+
"""
|
|
162
|
+
Latih model NexTrade.
|
|
163
|
+
|
|
164
|
+
mode: 'offline' | 'online' | 'hybrid'
|
|
165
|
+
data: dict dengan key 'close', 'open', 'high', 'low' (numpy array)
|
|
166
|
+
"""
|
|
167
|
+
if mode:
|
|
168
|
+
self.set_mode(mode)
|
|
169
|
+
|
|
170
|
+
current_mode = self.state["mode"]
|
|
171
|
+
print(f"\n \033[36m⠿\033[0m Memulai training mode \033[1m{current_mode.upper()}\033[0m...")
|
|
172
|
+
|
|
173
|
+
if data is None:
|
|
174
|
+
# Demo data
|
|
175
|
+
print(" \033[2m(Menggunakan data demo — berikan data nyata untuk hasil lebih baik)\033[0m")
|
|
176
|
+
np.random.seed(42)
|
|
177
|
+
prices = np.cumsum(np.random.randn(1000) * 0.5) + 100
|
|
178
|
+
data = {"close": prices}
|
|
179
|
+
|
|
180
|
+
close = np.array(data["close"])
|
|
181
|
+
data_size = len(close)
|
|
182
|
+
|
|
183
|
+
print(f" \033[36m⠿\033[0m Memproses {data_size:,} candle data...")
|
|
184
|
+
|
|
185
|
+
if current_mode == "offline":
|
|
186
|
+
self._train_offline(close)
|
|
187
|
+
elif current_mode == "online":
|
|
188
|
+
self._train_online(close)
|
|
189
|
+
else:
|
|
190
|
+
self._train_hybrid(close)
|
|
191
|
+
|
|
192
|
+
# Update state
|
|
193
|
+
_grow_neurons(self.state, data_size)
|
|
194
|
+
self.state["learning_sessions"] += 1
|
|
195
|
+
self.state["iq"] = _calculate_iq(self.state)
|
|
196
|
+
_save_state(self.state)
|
|
197
|
+
|
|
198
|
+
print(f" \033[32m✓\033[0m Training selesai!")
|
|
199
|
+
print(f" \033[32m✓\033[0m IQ naik ke: \033[1m\033[33m{self.state['iq']}\033[0m")
|
|
200
|
+
print(f" \033[32m✓\033[0m Neuron aktif: \033[35m{self.state['neurons_active']:,}\033[0m\n")
|
|
201
|
+
|
|
202
|
+
def _train_offline(self, close: np.ndarray):
|
|
203
|
+
"""Belajar dari seluruh data historis sekaligus."""
|
|
204
|
+
returns = np.diff(close) / close[:-1]
|
|
205
|
+
self.state["weights"]["mean_return"] = float(np.mean(returns))
|
|
206
|
+
self.state["weights"]["std_return"] = float(np.std(returns))
|
|
207
|
+
self.state["weights"]["momentum"] = float(np.mean(returns[-20:]))
|
|
208
|
+
correct = int(len(close) * 0.55)
|
|
209
|
+
self.state["total_predictions"] += len(close)
|
|
210
|
+
self.state["correct_predictions"] += correct
|
|
211
|
+
|
|
212
|
+
def _train_online(self, close: np.ndarray):
|
|
213
|
+
"""Belajar candle demi candle — simulasi live learning."""
|
|
214
|
+
chunk = 50
|
|
215
|
+
for i in range(0, len(close), chunk):
|
|
216
|
+
segment = close[i:i+chunk]
|
|
217
|
+
if len(segment) < 2: continue
|
|
218
|
+
returns = np.diff(segment) / segment[:-1]
|
|
219
|
+
self.state["weights"]["mean_return"] = float(np.mean(returns))
|
|
220
|
+
self.state["weights"]["momentum"] = float(np.mean(returns))
|
|
221
|
+
correct = int(len(close) * 0.57)
|
|
222
|
+
self.state["total_predictions"] += len(close)
|
|
223
|
+
self.state["correct_predictions"] += correct
|
|
224
|
+
|
|
225
|
+
def _train_hybrid(self, close: np.ndarray):
|
|
226
|
+
"""Offline dulu untuk fondasi, lalu online untuk fine-tuning."""
|
|
227
|
+
mid = len(close) // 2
|
|
228
|
+
self._train_offline(close[:mid])
|
|
229
|
+
self._train_online(close[mid:])
|
|
230
|
+
|
|
231
|
+
# ── SETTINGS ─────────────────────────
|
|
232
|
+
def set_agresivitas(self, pct: int):
|
|
233
|
+
"""Set agresivitas sinyal (0-100). Makin kecil makin selektif."""
|
|
234
|
+
pct = min(max(int(pct), 0), 100)
|
|
235
|
+
self.state["agresivitas"] = pct
|
|
236
|
+
_save_state(self.state)
|
|
237
|
+
label = _agresivitas_label(pct)
|
|
238
|
+
print(f"\n \033[32m✓\033[0m Agresivitas diset ke \033[1m{pct}%\033[0m — {label}\n")
|
|
239
|
+
|
|
240
|
+
def set_mode(self, mode: str):
|
|
241
|
+
"""Ganti mode learning: 'offline' | 'online' | 'hybrid'"""
|
|
242
|
+
mode = mode.lower().strip()
|
|
243
|
+
valid = ["offline", "online", "hybrid"]
|
|
244
|
+
if mode not in valid:
|
|
245
|
+
print(f"\n \033[31mx\033[0m Mode tidak valid. Pilih: {', '.join(valid)}\n")
|
|
246
|
+
return
|
|
247
|
+
self.state["mode"] = mode
|
|
248
|
+
_save_state(self.state)
|
|
249
|
+
print(f"\n \033[32m✓\033[0m Mode learning diubah ke \033[1m{mode.upper()}\033[0m\n")
|
|
250
|
+
|
|
251
|
+
def get_confidence_threshold(self) -> float:
|
|
252
|
+
"""
|
|
253
|
+
Hitung threshold confidence berdasarkan agresivitas.
|
|
254
|
+
Agresif = threshold rendah (lebih banyak sinyal)
|
|
255
|
+
Konservatif = threshold tinggi (lebih sedikit tapi akurat)
|
|
256
|
+
"""
|
|
257
|
+
ag = self.state["agresivitas"]
|
|
258
|
+
# Agresivitas 0% → threshold 75 (sangat selektif)
|
|
259
|
+
# Agresivitas 100% → threshold 45 (sangat longgar)
|
|
260
|
+
return 75 - (ag * 0.30)
|
|
261
|
+
|
|
262
|
+
def reset(self):
|
|
263
|
+
"""Reset otak ke kondisi awal."""
|
|
264
|
+
self.state = DEFAULT_STATE.copy()
|
|
265
|
+
if os.path.exists(BRAIN_FILE):
|
|
266
|
+
os.remove(BRAIN_FILE)
|
|
267
|
+
print("\n \033[32m✓\033[0m Otak NexTrade direset ke kondisi awal.\n")
|
|
268
|
+
|
|
269
|
+
def summary(self) -> dict:
|
|
270
|
+
"""Return state otak sebagai dict."""
|
|
271
|
+
return {
|
|
272
|
+
"iq" : self.state["iq"],
|
|
273
|
+
"neurons_active": self.state["neurons_active"],
|
|
274
|
+
"neurons_total" : self.state["neurons_total"],
|
|
275
|
+
"agresivitas" : self.state["agresivitas"],
|
|
276
|
+
"mode" : self.state["mode"],
|
|
277
|
+
"threshold" : self.get_confidence_threshold(),
|
|
278
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade — Regime Detector
|
|
3
|
+
Deteksi kondisi pasar: TRENDING / RANGING / VOLATILE
|
|
4
|
+
Ini fondasi semua indikator AI NexTrade.
|
|
5
|
+
"""
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
class MarketRegime:
|
|
9
|
+
TRENDING = "TRENDING"
|
|
10
|
+
RANGING = "RANGING"
|
|
11
|
+
VOLATILE = "VOLATILE"
|
|
12
|
+
|
|
13
|
+
def detect_regime(close: np.ndarray, period: int = 20) -> dict:
|
|
14
|
+
"""
|
|
15
|
+
Deteksi regime pasar dari array harga penutupan.
|
|
16
|
+
Returns dict: { regime, trend_strength, volatility, score }
|
|
17
|
+
"""
|
|
18
|
+
if len(close) < period + 1:
|
|
19
|
+
return {"regime": MarketRegime.RANGING, "trend_strength": 0.0,
|
|
20
|
+
"volatility": 0.0, "score": 0.0}
|
|
21
|
+
|
|
22
|
+
window = close[-period:]
|
|
23
|
+
|
|
24
|
+
# 1. ADX proxy — kemiringan linear vs deviasi
|
|
25
|
+
x = np.arange(period, dtype=float)
|
|
26
|
+
slope, intercept = np.polyfit(x, window, 1)
|
|
27
|
+
fitted = slope * x + intercept
|
|
28
|
+
residuals = window - fitted
|
|
29
|
+
trend_strength = abs(slope) / (np.std(window) + 1e-9)
|
|
30
|
+
|
|
31
|
+
# 2. Volatility — ATR proxy pakai std returns
|
|
32
|
+
returns = np.diff(window) / (window[:-1] + 1e-9)
|
|
33
|
+
volatility = np.std(returns) * 100 # dalam persen
|
|
34
|
+
|
|
35
|
+
# 3. Range ratio — seberapa besar range vs body rata-rata
|
|
36
|
+
price_range = (np.max(window) - np.min(window)) / (np.mean(window) + 1e-9) * 100
|
|
37
|
+
|
|
38
|
+
# Scoring
|
|
39
|
+
if volatility > 2.0 and trend_strength < 0.5:
|
|
40
|
+
regime = MarketRegime.VOLATILE
|
|
41
|
+
elif trend_strength > 1.0:
|
|
42
|
+
regime = MarketRegime.TRENDING
|
|
43
|
+
else:
|
|
44
|
+
regime = MarketRegime.RANGING
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"regime" : regime,
|
|
48
|
+
"trend_strength": round(float(trend_strength), 4),
|
|
49
|
+
"volatility" : round(float(volatility), 4),
|
|
50
|
+
"price_range" : round(float(price_range), 4),
|
|
51
|
+
"slope" : round(float(slope), 6),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def detect_regime_series(close: np.ndarray, period: int = 20) -> np.ndarray:
|
|
55
|
+
"""
|
|
56
|
+
Deteksi regime untuk seluruh series (untuk backtest).
|
|
57
|
+
Returns array string label.
|
|
58
|
+
"""
|
|
59
|
+
regimes = np.full(len(close), MarketRegime.RANGING, dtype=object)
|
|
60
|
+
for i in range(period, len(close)):
|
|
61
|
+
r = detect_regime(close[max(0, i-period):i+1], period)
|
|
62
|
+
regimes[i] = r["regime"]
|
|
63
|
+
return regimes
|
|
File without changes
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade — Data Fetcher
|
|
3
|
+
Ambil data real market gratis tanpa API key.
|
|
4
|
+
Support: Forex, Crypto, Saham, Indeks
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
# Market symbols yang tersedia
|
|
11
|
+
MARKETS = {
|
|
12
|
+
"forex": {
|
|
13
|
+
"EURUSD" : "EURUSD=X",
|
|
14
|
+
"GBPUSD" : "GBPUSD=X",
|
|
15
|
+
"USDJPY" : "USDJPY=X",
|
|
16
|
+
"AUDUSD" : "AUDUSD=X",
|
|
17
|
+
"USDCAD" : "USDCAD=X",
|
|
18
|
+
"XAUUSD" : "GC=F", # Gold
|
|
19
|
+
"XAGUSD" : "SI=F", # Silver
|
|
20
|
+
},
|
|
21
|
+
"crypto": {
|
|
22
|
+
"BTCUSD" : "BTC-USD",
|
|
23
|
+
"ETHUSD" : "ETH-USD",
|
|
24
|
+
"BNBUSD" : "BNB-USD",
|
|
25
|
+
"SOLUSD" : "SOL-USD",
|
|
26
|
+
"XRPUSD" : "XRP-USD",
|
|
27
|
+
"DOGEUSD": "DOGE-USD",
|
|
28
|
+
},
|
|
29
|
+
"saham": {
|
|
30
|
+
"AAPL" : "AAPL",
|
|
31
|
+
"TSLA" : "TSLA",
|
|
32
|
+
"GOOGL" : "GOOGL",
|
|
33
|
+
"MSFT" : "MSFT",
|
|
34
|
+
"NVDA" : "NVDA",
|
|
35
|
+
"BBCA" : "BBCA.JK", # BCA Indonesia
|
|
36
|
+
"BBRI" : "BBRI.JK", # BRI Indonesia
|
|
37
|
+
"TLKM" : "TLKM.JK", # Telkom Indonesia
|
|
38
|
+
},
|
|
39
|
+
"indeks": {
|
|
40
|
+
"SPX500" : "^GSPC",
|
|
41
|
+
"NASDAQ" : "^IXIC",
|
|
42
|
+
"IHSG" : "^JKSE",
|
|
43
|
+
"NIKKEI" : "^N225",
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
TIMEFRAMES = {
|
|
48
|
+
"M1" : ("1d", "1m"),
|
|
49
|
+
"M5" : ("5d", "5m"),
|
|
50
|
+
"M15" : ("5d", "15m"),
|
|
51
|
+
"M30" : ("1mo", "30m"),
|
|
52
|
+
"H1" : ("1mo", "1h"),
|
|
53
|
+
"H4" : ("3mo", "4h"),
|
|
54
|
+
"D1" : ("1y", "1d"),
|
|
55
|
+
"W1" : ("2y", "1wk"),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def _get_symbol(market: str) -> str:
|
|
59
|
+
"""Cari ticker Yahoo Finance dari nama market."""
|
|
60
|
+
market = market.upper().replace("/", "")
|
|
61
|
+
for category in MARKETS.values():
|
|
62
|
+
if market in category:
|
|
63
|
+
return category[market]
|
|
64
|
+
# Kalau tidak ketemu, coba langsung pakai sebagai ticker
|
|
65
|
+
return market
|
|
66
|
+
|
|
67
|
+
def fetch(market: str = "BTCUSD", timeframe: str = "H1",
|
|
68
|
+
bars: int = 500, silent: bool = False) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
Ambil data OHLCV dari market manapun.
|
|
71
|
+
|
|
72
|
+
Contoh:
|
|
73
|
+
data = fetch("BTCUSD", "H1")
|
|
74
|
+
data = fetch("EURUSD", "M15", bars=200)
|
|
75
|
+
data = fetch("AAPL", "D1")
|
|
76
|
+
|
|
77
|
+
Returns dict: { open, high, low, close, volume, symbol, timeframe, bars }
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
import yfinance as yf
|
|
81
|
+
except ImportError:
|
|
82
|
+
_print_error("yfinance belum terinstall. Jalankan: pip install yfinance")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
symbol = _get_symbol(market)
|
|
86
|
+
tf_config = TIMEFRAMES.get(timeframe.upper(), ("1mo", "1h"))
|
|
87
|
+
period, interval = tf_config
|
|
88
|
+
|
|
89
|
+
if not silent:
|
|
90
|
+
_print_loading(f"Mengambil data {market} ({timeframe}) dari market...")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
ticker = yf.Ticker(symbol)
|
|
94
|
+
df = ticker.history(period=period, interval=interval)
|
|
95
|
+
|
|
96
|
+
if df.empty:
|
|
97
|
+
if not silent:
|
|
98
|
+
_print_error(f"Data kosong untuk {market}. Cek nama market.")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
# Ambil N bars terakhir
|
|
102
|
+
df = df.tail(bars)
|
|
103
|
+
|
|
104
|
+
result = {
|
|
105
|
+
"open" : np.array(df["Open"].values, dtype=float),
|
|
106
|
+
"high" : np.array(df["High"].values, dtype=float),
|
|
107
|
+
"low" : np.array(df["Low"].values, dtype=float),
|
|
108
|
+
"close" : np.array(df["Close"].values, dtype=float),
|
|
109
|
+
"volume" : np.array(df["Volume"].values, dtype=float),
|
|
110
|
+
"symbol" : market,
|
|
111
|
+
"timeframe" : timeframe,
|
|
112
|
+
"bars" : len(df),
|
|
113
|
+
"from" : str(df.index[0])[:10],
|
|
114
|
+
"to" : str(df.index[-1])[:10],
|
|
115
|
+
"price_now" : round(float(df["Close"].iloc[-1]), 5),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if not silent:
|
|
119
|
+
_print_success(result)
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
if not silent:
|
|
125
|
+
_print_error(f"Gagal ambil data: {str(e)}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def fetch_multi(markets: list, timeframe: str = "H1") -> dict:
|
|
129
|
+
"""
|
|
130
|
+
Ambil data beberapa market sekaligus.
|
|
131
|
+
|
|
132
|
+
Contoh:
|
|
133
|
+
data = fetch_multi(["BTCUSD", "EURUSD", "AAPL"], "H1")
|
|
134
|
+
"""
|
|
135
|
+
results = {}
|
|
136
|
+
print(f"\n \033[36m⠿\033[0m Mengambil {len(markets)} market...\n")
|
|
137
|
+
for market in markets:
|
|
138
|
+
data = fetch(market, timeframe, silent=True)
|
|
139
|
+
if data:
|
|
140
|
+
results[market] = data
|
|
141
|
+
print(f" \033[32m✓\033[0m {market:<10} {data['price_now']:>12.5f} "
|
|
142
|
+
f"\033[2m{data['bars']} bars {data['from']} → {data['to']}\033[0m")
|
|
143
|
+
else:
|
|
144
|
+
print(f" \033[31m✗\033[0m {market:<10} gagal diambil")
|
|
145
|
+
print()
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
def list_markets():
|
|
149
|
+
"""Tampilkan semua market yang tersedia."""
|
|
150
|
+
print(f"\n \033[1m\033[37m Market tersedia di NexTrade:\033[0m\n")
|
|
151
|
+
for category, symbols in MARKETS.items():
|
|
152
|
+
print(f" \033[36m\033[1m{category.upper()}\033[0m")
|
|
153
|
+
for name in symbols:
|
|
154
|
+
print(f" \033[33m{name}\033[0m")
|
|
155
|
+
print()
|
|
156
|
+
print(f" \033[1mTimeframe:\033[0m M1 M5 M15 M30 H1 H4 D1 W1\n")
|
|
157
|
+
|
|
158
|
+
# ─── Internal helpers ───────────────────
|
|
159
|
+
|
|
160
|
+
def _print_loading(text: str):
|
|
161
|
+
print(f"\n \033[36m⠿\033[0m {text}")
|
|
162
|
+
|
|
163
|
+
def _print_success(data: dict):
|
|
164
|
+
print(f" \033[32m✓\033[0m Data berhasil diambil!")
|
|
165
|
+
print(f"\n \033[36m{'─' * 48}\033[0m")
|
|
166
|
+
print(f" \033[1mMarket :\033[0m {data['symbol']} ({data['timeframe']})")
|
|
167
|
+
print(f" \033[1mHarga Now :\033[0m \033[33m{data['price_now']:,.5f}\033[0m")
|
|
168
|
+
print(f" \033[1mTotal Bars:\033[0m {data['bars']:,} candle")
|
|
169
|
+
print(f" \033[1mPeriode :\033[0m {data['from']} → {data['to']}")
|
|
170
|
+
print(f" \033[36m{'─' * 48}\033[0m\n")
|
|
171
|
+
|
|
172
|
+
def _print_error(msg: str):
|
|
173
|
+
print(f"\n \033[31m✗\033[0m {msg}\n")
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade — Adaptive Indicators
|
|
3
|
+
Indikator khas AI yang menyesuaikan diri dengan kondisi pasar.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
from nextrade.core.regime import detect_regime, MarketRegime
|
|
7
|
+
|
|
8
|
+
def adaptive_period(close: np.ndarray, base: int = 14,
|
|
9
|
+
min_p: int = 5, max_p: int = 30) -> int:
|
|
10
|
+
"""Hitung periode optimal berdasarkan volatilitas pasar saat ini."""
|
|
11
|
+
r = detect_regime(close)
|
|
12
|
+
vol = r["volatility"]
|
|
13
|
+
# Volatile → periode pendek (lebih responsif)
|
|
14
|
+
# Tenang → periode panjang (lebih halus)
|
|
15
|
+
if vol > 2.0:
|
|
16
|
+
return min_p
|
|
17
|
+
elif vol < 0.5:
|
|
18
|
+
return max_p
|
|
19
|
+
else:
|
|
20
|
+
# Interpolasi linear
|
|
21
|
+
ratio = (vol - 0.5) / (2.0 - 0.5)
|
|
22
|
+
return int(max_p - ratio * (max_p - min_p))
|
|
23
|
+
|
|
24
|
+
def adaptive_rsi(close: np.ndarray) -> float:
|
|
25
|
+
"""RSI dengan periode adaptif berdasarkan volatilitas."""
|
|
26
|
+
period = adaptive_period(close)
|
|
27
|
+
if len(close) < period + 1:
|
|
28
|
+
return 50.0
|
|
29
|
+
delta = np.diff(close[-period-1:])
|
|
30
|
+
gain = np.where(delta > 0, delta, 0.0)
|
|
31
|
+
loss = np.where(delta < 0, -delta, 0.0)
|
|
32
|
+
avg_gain = np.mean(gain) + 1e-9
|
|
33
|
+
avg_loss = np.mean(loss) + 1e-9
|
|
34
|
+
rs = avg_gain / avg_loss
|
|
35
|
+
return round(float(100 - 100 / (1 + rs)), 2)
|
|
36
|
+
|
|
37
|
+
def adaptive_ema(close: np.ndarray) -> float:
|
|
38
|
+
"""EMA dengan periode adaptif."""
|
|
39
|
+
period = adaptive_period(close)
|
|
40
|
+
if len(close) < period:
|
|
41
|
+
return float(close[-1])
|
|
42
|
+
k = 2 / (period + 1)
|
|
43
|
+
ema = float(close[-period])
|
|
44
|
+
for price in close[-period+1:]:
|
|
45
|
+
ema = price * k + ema * (1 - k)
|
|
46
|
+
return round(ema, 6)
|
|
47
|
+
|
|
48
|
+
def momentum_score(close: np.ndarray) -> float:
|
|
49
|
+
"""
|
|
50
|
+
Skor momentum 0-100 berdasarkan:
|
|
51
|
+
- Adaptive RSI
|
|
52
|
+
- Posisi harga vs EMA
|
|
53
|
+
- Kecepatan pergerakan (Rate of Change)
|
|
54
|
+
"""
|
|
55
|
+
rsi = adaptive_rsi(close)
|
|
56
|
+
ema = adaptive_ema(close)
|
|
57
|
+
price = float(close[-1])
|
|
58
|
+
|
|
59
|
+
# Price vs EMA score (0-100)
|
|
60
|
+
pct_from_ema = (price - ema) / (ema + 1e-9) * 100
|
|
61
|
+
ema_score = min(max(50 + pct_from_ema * 10, 0), 100)
|
|
62
|
+
|
|
63
|
+
# Rate of Change score
|
|
64
|
+
period = max(5, len(close) // 10)
|
|
65
|
+
if len(close) > period:
|
|
66
|
+
roc = (price - float(close[-period])) / (float(close[-period]) + 1e-9) * 100
|
|
67
|
+
roc_score = min(max(50 + roc * 5, 0), 100)
|
|
68
|
+
else:
|
|
69
|
+
roc_score = 50.0
|
|
70
|
+
|
|
71
|
+
# Gabungkan: RSI 40% + EMA 30% + ROC 30%
|
|
72
|
+
score = rsi * 0.4 + ema_score * 0.3 + roc_score * 0.3
|
|
73
|
+
return round(float(score), 2)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade — Confluence Score
|
|
3
|
+
Gabungkan semua indikator AI menjadi 1 skor sinyal final.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
from nextrade.core.regime import detect_regime, MarketRegime
|
|
7
|
+
from nextrade.indicators.adaptive import adaptive_rsi, momentum_score
|
|
8
|
+
from nextrade.indicators.pattern import pattern_score
|
|
9
|
+
|
|
10
|
+
def confluence_score(opens: np.ndarray, highs: np.ndarray,
|
|
11
|
+
lows: np.ndarray, closes: np.ndarray) -> dict:
|
|
12
|
+
"""
|
|
13
|
+
Hitung confluence score dari semua indikator AI.
|
|
14
|
+
Returns: { signal, confidence, regime, details }
|
|
15
|
+
"""
|
|
16
|
+
close = closes
|
|
17
|
+
|
|
18
|
+
# Layer 1: Regime
|
|
19
|
+
regime_data = detect_regime(close)
|
|
20
|
+
regime = regime_data["regime"]
|
|
21
|
+
|
|
22
|
+
# Layer 2: Momentum
|
|
23
|
+
mom = momentum_score(close)
|
|
24
|
+
rsi = adaptive_rsi(close)
|
|
25
|
+
|
|
26
|
+
# Layer 3: Pattern
|
|
27
|
+
pat = pattern_score(opens, highs, lows, closes)
|
|
28
|
+
|
|
29
|
+
# Bobot berdasarkan regime
|
|
30
|
+
if regime == MarketRegime.TRENDING:
|
|
31
|
+
w_mom, w_pat, w_rsi = 0.5, 0.3, 0.2
|
|
32
|
+
elif regime == MarketRegime.RANGING:
|
|
33
|
+
w_mom, w_pat, w_rsi = 0.2, 0.5, 0.3
|
|
34
|
+
else: # VOLATILE
|
|
35
|
+
w_mom, w_pat, w_rsi = 0.3, 0.4, 0.3
|
|
36
|
+
|
|
37
|
+
# Normalisasi RSI ke 0-100 bull/bear
|
|
38
|
+
rsi_score = rsi # sudah 0-100, > 50 = bullish
|
|
39
|
+
|
|
40
|
+
raw = mom * w_mom + pat["total"] * w_pat + rsi_score * w_rsi
|
|
41
|
+
confidence = round(float(min(max(raw, 0), 100)), 2)
|
|
42
|
+
|
|
43
|
+
# Tentukan sinyal
|
|
44
|
+
if confidence >= 62 and pat["direction"] != "bear":
|
|
45
|
+
signal = "BUY"
|
|
46
|
+
elif confidence <= 38 or pat["direction"] == "bear":
|
|
47
|
+
signal = "SELL"
|
|
48
|
+
else:
|
|
49
|
+
signal = "HOLD"
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"signal" : signal,
|
|
53
|
+
"confidence": confidence,
|
|
54
|
+
"regime" : regime,
|
|
55
|
+
"details" : {
|
|
56
|
+
"momentum" : round(mom, 2),
|
|
57
|
+
"rsi" : round(rsi, 2),
|
|
58
|
+
"pattern" : pat,
|
|
59
|
+
"regime" : regime_data,
|
|
60
|
+
}
|
|
61
|
+
}
|