nextrade-engine 0.7.0__tar.gz → 0.8.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.7.0/nextrade/nextrade_engine.egg-info → nextrade_engine-0.8.0}/PKG-INFO +2 -1
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/core/regime.py +2 -2
- nextrade_engine-0.8.0/nextrade/nextrade/indicators/adaptive.py +99 -0
- nextrade_engine-0.8.0/nextrade/nextrade/indicators/confluence.py +91 -0
- nextrade_engine-0.8.0/nextrade/nextrade/indicators/pattern.py +82 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0/nextrade/nextrade_engine.egg-info}/PKG-INFO +2 -1
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade_engine.egg-info/requires.txt +1 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/pyproject.toml +3 -2
- nextrade_engine-0.7.0/nextrade/nextrade/indicators/adaptive.py +0 -73
- nextrade_engine-0.7.0/nextrade/nextrade/indicators/confluence.py +0 -61
- nextrade_engine-0.7.0/nextrade/nextrade/indicators/pattern.py +0 -76
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/LICENSE +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/README.md +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/__init__.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/backtest/__init__.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/backtest/engine.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/core/__init__.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/core/brain.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/data/__init__.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/data/fetcher.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/indicators/__init__.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/lang.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/ml/__init__.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/ml/features.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/ml/model.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/utils/__init__.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade/utils/terminal_ui.py +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade_engine.egg-info/SOURCES.txt +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade_engine.egg-info/dependency_links.txt +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade_engine.egg-info/top_level.txt +0 -0
- {nextrade_engine-0.7.0 → nextrade_engine-0.8.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.8.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
|
|
@@ -14,6 +14,7 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: numpy
|
|
15
15
|
Requires-Dist: yfinance
|
|
16
16
|
Requires-Dist: scikit-learn
|
|
17
|
+
Requires-Dist: nexarray
|
|
17
18
|
Dynamic: license-file
|
|
18
19
|
|
|
19
20
|
# NexTrade 🧠
|
|
@@ -29,7 +29,7 @@ def detect_regime(close: np.ndarray, period: int = 20) -> dict:
|
|
|
29
29
|
trend_strength = abs(slope) / (np.std(window) + 1e-9)
|
|
30
30
|
|
|
31
31
|
# 2. Volatility — ATR proxy pakai std returns
|
|
32
|
-
returns = np.diff(
|
|
32
|
+
window_arr = np.array(window); returns = np.diff(window_arr) / (window_arr[:-1] + 1e-9)
|
|
33
33
|
volatility = np.std(returns) * 100 # dalam persen
|
|
34
34
|
|
|
35
35
|
# 3. Range ratio — seberapa besar range vs body rata-rata
|
|
@@ -60,4 +60,4 @@ def detect_regime_series(close: np.ndarray, period: int = 20) -> np.ndarray:
|
|
|
60
60
|
for i in range(period, len(close)):
|
|
61
61
|
r = detect_regime(close[max(0, i-period):i+1], period)
|
|
62
62
|
regimes[i] = r["regime"]
|
|
63
|
-
return regimes
|
|
63
|
+
return regimes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade v0.8.0 — Adaptive Indicators
|
|
3
|
+
Menggunakan NexArray bukan NumPy!
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
sys.path.insert(0, '/home/claude/nexarray')
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import nexarray as nx
|
|
10
|
+
HAS_NX = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_NX = False
|
|
13
|
+
|
|
14
|
+
from nextrade.core.regime import detect_regime, MarketRegime
|
|
15
|
+
|
|
16
|
+
def _to_list(data) -> list:
|
|
17
|
+
if HAS_NX and hasattr(data, '_data'):
|
|
18
|
+
return list(data._data)
|
|
19
|
+
try:
|
|
20
|
+
return list(data)
|
|
21
|
+
except:
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
def adaptive_period(close, base: int = 14,
|
|
25
|
+
min_p: int = 5, max_p: int = 30) -> int:
|
|
26
|
+
close_list = _to_list(close)
|
|
27
|
+
r = detect_regime(close_list)
|
|
28
|
+
vol = r["volatility"]
|
|
29
|
+
if vol > 2.0: return min_p
|
|
30
|
+
elif vol < 0.5: return max_p
|
|
31
|
+
ratio = (vol - 0.5) / (2.0 - 0.5)
|
|
32
|
+
return int(max_p - ratio * (max_p - min_p))
|
|
33
|
+
|
|
34
|
+
def adaptive_rsi(close) -> float:
|
|
35
|
+
close_list = _to_list(close)
|
|
36
|
+
period = adaptive_period(close_list)
|
|
37
|
+
if len(close_list) < period + 1:
|
|
38
|
+
return 50.0
|
|
39
|
+
|
|
40
|
+
if HAS_NX:
|
|
41
|
+
# Pakai NexArray!
|
|
42
|
+
arr = nx.array(close_list[-period-1:])
|
|
43
|
+
delta = nx.diff(arr)
|
|
44
|
+
gain = nx.where(delta > 0, delta, nx.array([0.0]*delta.size))
|
|
45
|
+
loss = nx.where(delta < 0, delta * -1, nx.array([0.0]*delta.size))
|
|
46
|
+
avg_g = gain.mean() + 1e-9
|
|
47
|
+
avg_l = loss.mean() + 1e-9
|
|
48
|
+
rs = avg_g / avg_l
|
|
49
|
+
return round(float(100 - 100 / (1 + rs)), 2)
|
|
50
|
+
else:
|
|
51
|
+
seg = close_list[-period-1:]
|
|
52
|
+
delta = [seg[i]-seg[i-1] for i in range(1, len(seg))]
|
|
53
|
+
gain = [max(d, 0) for d in delta]
|
|
54
|
+
loss = [max(-d, 0) for d in delta]
|
|
55
|
+
avg_g = sum(gain)/len(gain) + 1e-9
|
|
56
|
+
avg_l = sum(loss)/len(loss) + 1e-9
|
|
57
|
+
return round(100 - 100/(1 + avg_g/avg_l), 2)
|
|
58
|
+
|
|
59
|
+
def adaptive_ema(close) -> float:
|
|
60
|
+
close_list = _to_list(close)
|
|
61
|
+
period = adaptive_period(close_list)
|
|
62
|
+
if len(close_list) < period:
|
|
63
|
+
return float(close_list[-1])
|
|
64
|
+
k = 2 / (period + 1)
|
|
65
|
+
ema = float(close_list[-period])
|
|
66
|
+
for price in close_list[-period+1:]:
|
|
67
|
+
ema = price * k + ema * (1 - k)
|
|
68
|
+
return round(ema, 6)
|
|
69
|
+
|
|
70
|
+
def momentum_score(close) -> float:
|
|
71
|
+
close_list = _to_list(close)
|
|
72
|
+
|
|
73
|
+
if HAS_NX:
|
|
74
|
+
arr = nx.array(close_list)
|
|
75
|
+
rsi = adaptive_rsi(close_list)
|
|
76
|
+
ema = adaptive_ema(close_list)
|
|
77
|
+
price = close_list[-1]
|
|
78
|
+
pct = (price - ema) / (ema + 1e-9) * 100
|
|
79
|
+
ema_sc = min(max(50 + pct * 10, 0), 100)
|
|
80
|
+
period = max(5, len(close_list) // 10)
|
|
81
|
+
if len(close_list) > period:
|
|
82
|
+
roc = (price - close_list[-period]) / (close_list[-period]+1e-9)*100
|
|
83
|
+
roc_sc = min(max(50 + roc * 5, 0), 100)
|
|
84
|
+
else:
|
|
85
|
+
roc_sc = 50.0
|
|
86
|
+
score = rsi*0.4 + ema_sc*0.3 + roc_sc*0.3
|
|
87
|
+
return round(float(score), 2)
|
|
88
|
+
else:
|
|
89
|
+
rsi = adaptive_rsi(close_list)
|
|
90
|
+
ema = adaptive_ema(close_list)
|
|
91
|
+
price = close_list[-1]
|
|
92
|
+
pct = (price - ema) / (ema + 1e-9) * 100
|
|
93
|
+
ema_sc = min(max(50 + pct*10, 0), 100)
|
|
94
|
+
period = max(5, len(close_list)//10)
|
|
95
|
+
roc_sc = 50.0
|
|
96
|
+
if len(close_list) > period:
|
|
97
|
+
roc = (price-close_list[-period])/(close_list[-period]+1e-9)*100
|
|
98
|
+
roc_sc = min(max(50+roc*5, 0), 100)
|
|
99
|
+
return round(rsi*0.4 + ema_sc*0.3 + roc_sc*0.3, 2)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade v0.8.0 — Confluence Score
|
|
3
|
+
Gabungkan semua indikator — NexArray powered!
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
sys.path.insert(0, '/home/claude/nexarray')
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import nexarray as nx
|
|
10
|
+
HAS_NX = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_NX = False
|
|
13
|
+
|
|
14
|
+
from nextrade.core.regime import detect_regime, MarketRegime
|
|
15
|
+
from nextrade.indicators.adaptive import adaptive_rsi, momentum_score
|
|
16
|
+
from nextrade.indicators.pattern import pattern_score
|
|
17
|
+
|
|
18
|
+
def _to_list(data) -> list:
|
|
19
|
+
if hasattr(data, '_data'): return list(data._data)
|
|
20
|
+
try: return list(data)
|
|
21
|
+
except: return data
|
|
22
|
+
|
|
23
|
+
def confluence_score(opens, highs, lows, closes) -> dict:
|
|
24
|
+
cl = _to_list(closes)
|
|
25
|
+
|
|
26
|
+
# Regime detection
|
|
27
|
+
regime_data = detect_regime(cl)
|
|
28
|
+
regime = regime_data["regime"]
|
|
29
|
+
|
|
30
|
+
# Momentum & RSI
|
|
31
|
+
mom = momentum_score(cl)
|
|
32
|
+
rsi = adaptive_rsi(cl)
|
|
33
|
+
|
|
34
|
+
# NexArray trading indicators
|
|
35
|
+
if HAS_NX:
|
|
36
|
+
nx_c = nx.array(cl)
|
|
37
|
+
grav = nx.gravity(nx_c, 20)
|
|
38
|
+
snap = nx.snap(nx_c, 5)
|
|
39
|
+
wave = nx.momentum_wave(nx_c, 5, 20)
|
|
40
|
+
grav_v = float(grav._data[-1])
|
|
41
|
+
snap_v = float(snap._data[-1])
|
|
42
|
+
wave_v = float(wave._data[-1])
|
|
43
|
+
else:
|
|
44
|
+
grav_v = snap_v = wave_v = 0.0
|
|
45
|
+
|
|
46
|
+
# Pattern
|
|
47
|
+
pat = pattern_score(opens, highs, lows, closes)
|
|
48
|
+
|
|
49
|
+
# Bobot berdasarkan regime
|
|
50
|
+
if regime == MarketRegime.TRENDING:
|
|
51
|
+
w_mom, w_pat, w_rsi, w_nx = 0.4, 0.25, 0.15, 0.20
|
|
52
|
+
elif regime == MarketRegime.RANGING:
|
|
53
|
+
w_mom, w_pat, w_rsi, w_nx = 0.15, 0.45, 0.25, 0.15
|
|
54
|
+
else:
|
|
55
|
+
w_mom, w_pat, w_rsi, w_nx = 0.25, 0.35, 0.25, 0.15
|
|
56
|
+
|
|
57
|
+
# NexArray score
|
|
58
|
+
nx_score = 50.0
|
|
59
|
+
if HAS_NX:
|
|
60
|
+
nx_score = 50.0
|
|
61
|
+
nx_score -= grav_v * 10 # gravity — jauh dari mean = reversal
|
|
62
|
+
nx_score += wave_v * 0.5 # momentum wave
|
|
63
|
+
nx_score += snap_v * 5 # snap detection
|
|
64
|
+
nx_score = min(max(nx_score, 0), 100)
|
|
65
|
+
|
|
66
|
+
raw = (mom * w_mom + pat["total"] * w_pat +
|
|
67
|
+
rsi * w_rsi + nx_score * w_nx)
|
|
68
|
+
confidence = round(float(min(max(raw, 0), 100)), 2)
|
|
69
|
+
|
|
70
|
+
if confidence >= 62 and pat["direction"] != "bear":
|
|
71
|
+
signal = "BUY"
|
|
72
|
+
elif confidence <= 38 or pat["direction"] == "bear":
|
|
73
|
+
signal = "SELL"
|
|
74
|
+
else:
|
|
75
|
+
signal = "HOLD"
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"signal" : signal,
|
|
79
|
+
"confidence": confidence,
|
|
80
|
+
"regime" : regime,
|
|
81
|
+
"details" : {
|
|
82
|
+
"momentum" : round(mom, 2),
|
|
83
|
+
"rsi" : round(rsi, 2),
|
|
84
|
+
"pattern" : pat,
|
|
85
|
+
"regime" : regime_data,
|
|
86
|
+
"nx_gravity": round(grav_v, 4),
|
|
87
|
+
"nx_snap" : snap_v,
|
|
88
|
+
"nx_wave" : round(wave_v, 4),
|
|
89
|
+
"nx_score" : round(nx_score, 2),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NexTrade v0.8.0 — Pattern Scorer
|
|
3
|
+
Menggunakan NexArray bukan NumPy!
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
sys.path.insert(0, '/home/claude/nexarray')
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import nexarray as nx
|
|
10
|
+
HAS_NX = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_NX = False
|
|
13
|
+
|
|
14
|
+
def _f(x) -> float:
|
|
15
|
+
return float(x._data[-1] if hasattr(x,'_data') else x)
|
|
16
|
+
|
|
17
|
+
def hammer_score(open_: float, high: float,
|
|
18
|
+
low: float, close: float) -> float:
|
|
19
|
+
body = abs(close - open_)
|
|
20
|
+
rng = high - low + 1e-9
|
|
21
|
+
lower = min(open_, close) - low
|
|
22
|
+
upper = high - max(open_, close)
|
|
23
|
+
score = 0.0
|
|
24
|
+
score += min((lower/rng)/0.6, 1.0) * 50
|
|
25
|
+
score += max(1 - (body/rng)/0.3, 0) * 30
|
|
26
|
+
score += max(1 - upper/(rng*0.1+1e-9), 0) * 20
|
|
27
|
+
return round(min(score, 100.0), 2)
|
|
28
|
+
|
|
29
|
+
def engulfing_score(prev_o, prev_c, curr_o, curr_c) -> tuple:
|
|
30
|
+
pb = abs(float(prev_c)-float(prev_o))
|
|
31
|
+
cb = abs(float(curr_c)-float(curr_o))
|
|
32
|
+
if pb == 0: return 0.0, "none"
|
|
33
|
+
ratio = cb / (pb + 1e-9)
|
|
34
|
+
is_bull = prev_c < prev_o and curr_c > curr_o and curr_o < prev_c and curr_c > prev_o
|
|
35
|
+
is_bear = prev_c > prev_o and curr_c < curr_o and curr_o > prev_c and curr_c < prev_o
|
|
36
|
+
if not (is_bull or is_bear): return 0.0, "none"
|
|
37
|
+
score = min(ratio * 60, 80) + 20
|
|
38
|
+
return round(min(score, 100.0), 2), "bull" if is_bull else "bear"
|
|
39
|
+
|
|
40
|
+
def pattern_score(opens, highs, lows, closes) -> dict:
|
|
41
|
+
def _get(arr, idx):
|
|
42
|
+
if hasattr(arr, '_data'): return float(arr._data[idx])
|
|
43
|
+
return float(arr[idx])
|
|
44
|
+
|
|
45
|
+
n = len(closes) if not hasattr(closes,'_data') else closes.size
|
|
46
|
+
if n < 2:
|
|
47
|
+
return {"total":50.0,"hammer":0.0,"engulfing":0.0,"direction":"neutral"}
|
|
48
|
+
|
|
49
|
+
o = _get(opens, -1)
|
|
50
|
+
h = _get(highs, -1)
|
|
51
|
+
l = _get(lows, -1)
|
|
52
|
+
c = _get(closes, -1)
|
|
53
|
+
po = _get(opens, -2)
|
|
54
|
+
pc = _get(closes, -2)
|
|
55
|
+
|
|
56
|
+
if HAS_NX:
|
|
57
|
+
# Pakai NexArray peak/valley untuk context
|
|
58
|
+
c_arr = nx.array(list(closes._data) if hasattr(closes,'_data') else list(closes))
|
|
59
|
+
peaks = nx.peak(c_arr, order=2)
|
|
60
|
+
valleys= nx.valley(c_arr, order=2)
|
|
61
|
+
at_peak = float(peaks._data[-3]) == 1.0
|
|
62
|
+
at_valley = float(valleys._data[-3]) == 1.0
|
|
63
|
+
else:
|
|
64
|
+
at_peak = at_valley = False
|
|
65
|
+
|
|
66
|
+
h_score = hammer_score(o, h, l, c)
|
|
67
|
+
e_score, e_dir = engulfing_score(po, pc, o, c)
|
|
68
|
+
|
|
69
|
+
if e_dir == "bull" or (h_score > 50 and c > o):
|
|
70
|
+
direction = "bull"
|
|
71
|
+
elif e_dir == "bear" or (h_score > 50 and c < o):
|
|
72
|
+
direction = "bear"
|
|
73
|
+
else:
|
|
74
|
+
direction = "neutral"
|
|
75
|
+
|
|
76
|
+
# Boost score kalau di peak/valley
|
|
77
|
+
total = max(h_score, e_score)
|
|
78
|
+
if at_valley and direction == "bull": total = min(total * 1.2, 100)
|
|
79
|
+
if at_peak and direction == "bear": total = min(total * 1.2, 100)
|
|
80
|
+
|
|
81
|
+
return {"total": total, "hammer": h_score,
|
|
82
|
+
"engulfing": e_score, "direction": direction}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextrade-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -14,6 +14,7 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: numpy
|
|
15
15
|
Requires-Dist: yfinance
|
|
16
16
|
Requires-Dist: scikit-learn
|
|
17
|
+
Requires-Dist: nexarray
|
|
17
18
|
Dynamic: license-file
|
|
18
19
|
|
|
19
20
|
# NexTrade 🧠
|
|
@@ -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.8.0"
|
|
8
8
|
description = "AI Trading Engine — Ringan, Multi-Market, Bisa di HP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -20,8 +20,9 @@ classifiers = [
|
|
|
20
20
|
requires-python = ">=3.8"
|
|
21
21
|
dependencies = [
|
|
22
22
|
"numpy",
|
|
23
|
-
"yfinance",
|
|
23
|
+
"yfinance",
|
|
24
24
|
"scikit-learn",
|
|
25
|
+
"nexarray",
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
[tool.setuptools]
|
|
@@ -1,73 +0,0 @@
|
|
|
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)
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
NexTrade — Pattern Scorer
|
|
3
|
-
Beri skor kekuatan pola candlestick, bukan hanya deteksi ya/tidak.
|
|
4
|
-
"""
|
|
5
|
-
import numpy as np
|
|
6
|
-
|
|
7
|
-
def _body(o, c): return abs(c - o)
|
|
8
|
-
def _upper(o, c, h): return h - max(o, c)
|
|
9
|
-
def _lower(o, c, l): return min(o, c) - l
|
|
10
|
-
def _range(h, l): return h - l + 1e-9
|
|
11
|
-
|
|
12
|
-
def hammer_score(open_: float, high: float, low: float, close: float) -> float:
|
|
13
|
-
"""Skor pola hammer / pin bar bawah (0-100)."""
|
|
14
|
-
body = _body(open_, close)
|
|
15
|
-
lower = _lower(open_, close, low)
|
|
16
|
-
upper = _upper(open_, close, high)
|
|
17
|
-
rng = _range(high, low)
|
|
18
|
-
if rng == 0: return 0.0
|
|
19
|
-
lower_ratio = lower / rng
|
|
20
|
-
body_ratio = body / rng
|
|
21
|
-
# Hammer ideal: lower > 60% range, body < 30%, upper < 10%
|
|
22
|
-
score = 0.0
|
|
23
|
-
score += min(lower_ratio / 0.6, 1.0) * 50 # lower shadow besar
|
|
24
|
-
score += max(1 - body_ratio / 0.3, 0) * 30 # body kecil
|
|
25
|
-
score += max(1 - upper / (rng * 0.1 + 1e-9), 0) * 20 # upper shadow kecil
|
|
26
|
-
return round(min(score, 100.0), 2)
|
|
27
|
-
|
|
28
|
-
def engulfing_score(prev_o: float, prev_c: float,
|
|
29
|
-
curr_o: float, curr_c: float) -> tuple:
|
|
30
|
-
"""
|
|
31
|
-
Skor pola engulfing bullish / bearish.
|
|
32
|
-
Returns (score, direction) — direction: 'bull' atau 'bear'
|
|
33
|
-
"""
|
|
34
|
-
prev_body = _body(prev_o, prev_c)
|
|
35
|
-
curr_body = _body(curr_o, curr_c)
|
|
36
|
-
if prev_body == 0: return 0.0, "none"
|
|
37
|
-
|
|
38
|
-
ratio = curr_body / (prev_body + 1e-9)
|
|
39
|
-
is_bull = prev_c < prev_o and curr_c > curr_o and curr_o < prev_c and curr_c > prev_o
|
|
40
|
-
is_bear = prev_c > prev_o and curr_c < curr_o and curr_o > prev_c and curr_c < prev_o
|
|
41
|
-
|
|
42
|
-
if not (is_bull or is_bear): return 0.0, "none"
|
|
43
|
-
score = min(ratio * 60, 80) + 20 # minimum 20 kalau valid
|
|
44
|
-
direction = "bull" if is_bull else "bear"
|
|
45
|
-
return round(min(score, 100.0), 2), direction
|
|
46
|
-
|
|
47
|
-
def pattern_score(opens: np.ndarray, highs: np.ndarray,
|
|
48
|
-
lows: np.ndarray, closes: np.ndarray) -> dict:
|
|
49
|
-
"""
|
|
50
|
-
Gabungan skor semua pola pada candle terbaru.
|
|
51
|
-
Returns dict dengan skor tiap pola dan skor total.
|
|
52
|
-
"""
|
|
53
|
-
if len(closes) < 2:
|
|
54
|
-
return {"total": 50.0, "hammer": 0.0, "engulfing": 0.0, "direction": "neutral"}
|
|
55
|
-
|
|
56
|
-
o, h, l, c = opens[-1], highs[-1], lows[-1], closes[-1]
|
|
57
|
-
po, pc = opens[-2], closes[-2]
|
|
58
|
-
|
|
59
|
-
h_score = hammer_score(o, h, l, c)
|
|
60
|
-
e_score, e_dir = engulfing_score(po, pc, o, c)
|
|
61
|
-
|
|
62
|
-
# Tentukan arah dominan
|
|
63
|
-
if e_dir == "bull" or (h_score > 50 and c > o):
|
|
64
|
-
direction = "bull"
|
|
65
|
-
elif e_dir == "bear" or (h_score > 50 and c < o):
|
|
66
|
-
direction = "bear"
|
|
67
|
-
else:
|
|
68
|
-
direction = "neutral"
|
|
69
|
-
|
|
70
|
-
total = max(h_score, e_score)
|
|
71
|
-
return {
|
|
72
|
-
"total" : total,
|
|
73
|
-
"hammer" : h_score,
|
|
74
|
-
"engulfing" : e_score,
|
|
75
|
-
"direction" : direction,
|
|
76
|
-
}
|
|
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.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade_engine.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{nextrade_engine-0.7.0 → nextrade_engine-0.8.0}/nextrade/nextrade_engine.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|