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.
@@ -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
+ }