dexbot 1.6.5 → 1.6.7

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.
Files changed (228) hide show
  1. package/CHANGELOG.md +76 -4
  2. package/README.md +108 -36
  3. package/analysis/README.md +11 -16
  4. package/analysis/account_resolver.ts +1 -11
  5. package/analysis/bot_usage/kibana_bot_queries.ts +1 -1
  6. package/analysis/chain_pool.ts +1 -1
  7. package/analysis/fills_source.ts +0 -6
  8. package/analysis/grid_correction_check.ts +20 -26
  9. package/analysis/resolve_source.ts +1 -1
  10. package/analysis/trade_profitability.ts +2 -5
  11. package/analysis/trend_detection/README.md +1 -3
  12. package/analysis/trend_detection/package.json +1 -1
  13. package/claw/package.json +1 -1
  14. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  15. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  16. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  17. package/dist/analysis/account_resolver.d.ts +1 -4
  18. package/dist/analysis/account_resolver.d.ts.map +1 -1
  19. package/dist/analysis/account_resolver.js +1 -1
  20. package/dist/analysis/account_resolver.js.map +1 -1
  21. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +1 -2
  22. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  23. package/dist/analysis/bot_usage/kibana_bot_queries.js +1 -1
  24. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  25. package/dist/analysis/chain_pool.d.ts +1 -3
  26. package/dist/analysis/chain_pool.d.ts.map +1 -1
  27. package/dist/analysis/chain_pool.js +1 -1
  28. package/dist/analysis/chain_pool.js.map +1 -1
  29. package/dist/analysis/fills_source.d.ts +1 -51
  30. package/dist/analysis/fills_source.d.ts.map +1 -1
  31. package/dist/analysis/fills_source.js +1 -1
  32. package/dist/analysis/fills_source.js.map +1 -1
  33. package/dist/analysis/grid_correction_check.d.ts +2 -3
  34. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  35. package/dist/analysis/grid_correction_check.js +18 -26
  36. package/dist/analysis/grid_correction_check.js.map +1 -1
  37. package/dist/analysis/resolve_source.d.ts +1 -1
  38. package/dist/analysis/resolve_source.d.ts.map +1 -1
  39. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  40. package/dist/analysis/trade_profitability.js +2 -7
  41. package/dist/analysis/trade_profitability.js.map +1 -1
  42. package/dist/dexbot.d.ts.map +1 -1
  43. package/dist/dexbot.js +82 -83
  44. package/dist/dexbot.js.map +1 -1
  45. package/dist/market_adapter/core/kibana_candles.d.ts +1 -15
  46. package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
  47. package/dist/market_adapter/core/kibana_candles.js +1 -1
  48. package/dist/market_adapter/core/kibana_candles.js.map +1 -1
  49. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +1 -35
  50. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  51. package/dist/market_adapter/inputs/fetch_lp_data.js +10 -54
  52. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  53. package/dist/market_adapter/inputs/kibana_feed_source.d.ts +1 -8
  54. package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -1
  55. package/dist/market_adapter/inputs/kibana_feed_source.js +1 -1
  56. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
  57. package/dist/market_adapter/inputs/window_cache.d.ts +2 -63
  58. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
  59. package/dist/market_adapter/inputs/window_cache.js +22 -102
  60. package/dist/market_adapter/inputs/window_cache.js.map +1 -1
  61. package/dist/modules/account_bots.d.ts +16 -2
  62. package/dist/modules/account_bots.d.ts.map +1 -1
  63. package/dist/modules/account_bots.js +108 -44
  64. package/dist/modules/account_bots.js.map +1 -1
  65. package/dist/modules/account_orders.d.ts +20 -2
  66. package/dist/modules/account_orders.d.ts.map +1 -1
  67. package/dist/modules/account_orders.js +63 -3
  68. package/dist/modules/account_orders.js.map +1 -1
  69. package/dist/modules/bitshares-native/chain_client.d.ts +10 -0
  70. package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
  71. package/dist/modules/bitshares-native/chain_client.js +197 -28
  72. package/dist/modules/bitshares-native/chain_client.js.map +1 -1
  73. package/dist/modules/bitshares-native/crypto/ecc.browser.d.ts.map +1 -1
  74. package/dist/modules/bitshares-native/crypto/ecc.browser.js +0 -31
  75. package/dist/modules/bitshares-native/crypto/ecc.browser.js.map +1 -1
  76. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  77. package/dist/modules/bitshares-native/subscriptions.js +312 -10
  78. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  79. package/dist/modules/bitshares-native/transport.d.ts +1 -0
  80. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  81. package/dist/modules/bitshares-native/transport.js +97 -3
  82. package/dist/modules/bitshares-native/transport.js.map +1 -1
  83. package/dist/modules/bitshares_client.d.ts +20 -0
  84. package/dist/modules/bitshares_client.d.ts.map +1 -1
  85. package/dist/modules/bitshares_client.js +29 -0
  86. package/dist/modules/bitshares_client.js.map +1 -1
  87. package/dist/modules/chain_keys.d.ts +37 -22
  88. package/dist/modules/chain_keys.d.ts.map +1 -1
  89. package/dist/modules/chain_keys.js +145 -44
  90. package/dist/modules/chain_keys.js.map +1 -1
  91. package/dist/modules/cli_start_onboarding.d.ts +10 -0
  92. package/dist/modules/cli_start_onboarding.d.ts.map +1 -0
  93. package/dist/modules/cli_start_onboarding.js +15 -0
  94. package/dist/modules/cli_start_onboarding.js.map +1 -0
  95. package/dist/modules/cli_start_output.d.ts +6 -0
  96. package/dist/modules/cli_start_output.d.ts.map +1 -0
  97. package/dist/modules/cli_start_output.js +8 -0
  98. package/dist/modules/cli_start_output.js.map +1 -0
  99. package/dist/modules/constants.d.ts +13 -0
  100. package/dist/modules/constants.d.ts.map +1 -1
  101. package/dist/modules/constants.js +103 -1
  102. package/dist/modules/constants.js.map +1 -1
  103. package/dist/modules/credit_runtime.d.ts +0 -2
  104. package/dist/modules/credit_runtime.d.ts.map +1 -1
  105. package/dist/modules/credit_runtime.js +1 -10
  106. package/dist/modules/credit_runtime.js.map +1 -1
  107. package/dist/modules/dexbot_class.d.ts +13 -10
  108. package/dist/modules/dexbot_class.d.ts.map +1 -1
  109. package/dist/modules/dexbot_class.js +0 -9
  110. package/dist/modules/dexbot_class.js.map +1 -1
  111. package/dist/modules/dexbot_cow_runtime.d.ts +13 -0
  112. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  113. package/dist/modules/dexbot_cow_runtime.js +62 -10
  114. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  115. package/dist/modules/dexbot_fill_runtime.d.ts +6 -1
  116. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  117. package/dist/modules/dexbot_fill_runtime.js +22 -3
  118. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  119. package/dist/modules/dexbot_maintenance_runtime.d.ts +24 -1
  120. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  121. package/dist/modules/dexbot_maintenance_runtime.js +45 -1
  122. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  123. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  124. package/dist/modules/dexbot_state_recovery.js +13 -1
  125. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  126. package/dist/modules/order/accounting.d.ts +4 -18
  127. package/dist/modules/order/accounting.d.ts.map +1 -1
  128. package/dist/modules/order/accounting.js +4 -34
  129. package/dist/modules/order/accounting.js.map +1 -1
  130. package/dist/modules/order/export.d.ts +9 -12
  131. package/dist/modules/order/export.d.ts.map +1 -1
  132. package/dist/modules/order/export.js +9 -12
  133. package/dist/modules/order/export.js.map +1 -1
  134. package/dist/modules/order/grid.d.ts +35 -40
  135. package/dist/modules/order/grid.d.ts.map +1 -1
  136. package/dist/modules/order/grid.js +76 -44
  137. package/dist/modules/order/grid.js.map +1 -1
  138. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  139. package/dist/modules/order/grid_reconcile.js +111 -20
  140. package/dist/modules/order/grid_reconcile.js.map +1 -1
  141. package/dist/modules/order/grid_reconcile_internal.d.ts +5 -3
  142. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  143. package/dist/modules/order/grid_reconcile_internal.js +29 -3
  144. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  145. package/dist/modules/order/logger.d.ts +12 -2
  146. package/dist/modules/order/logger.d.ts.map +1 -1
  147. package/dist/modules/order/logger.js +29 -7
  148. package/dist/modules/order/logger.js.map +1 -1
  149. package/dist/modules/order/manager.d.ts +49 -8
  150. package/dist/modules/order/manager.d.ts.map +1 -1
  151. package/dist/modules/order/manager.js +72 -34
  152. package/dist/modules/order/manager.js.map +1 -1
  153. package/dist/modules/order/utils/order.d.ts +22 -7
  154. package/dist/modules/order/utils/order.d.ts.map +1 -1
  155. package/dist/modules/order/utils/order.js +200 -33
  156. package/dist/modules/order/utils/order.js.map +1 -1
  157. package/dist/modules/order/utils/system.d.ts +97 -0
  158. package/dist/modules/order/utils/system.d.ts.map +1 -1
  159. package/dist/modules/order/utils/system.js +274 -2
  160. package/dist/modules/order/utils/system.js.map +1 -1
  161. package/dist/modules/order/utils/withPoolRef.d.ts +12 -0
  162. package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -1
  163. package/dist/modules/order/utils/withPoolRef.js +17 -0
  164. package/dist/modules/order/utils/withPoolRef.js.map +1 -1
  165. package/dist/modules/order/working_grid.d.ts +8 -13
  166. package/dist/modules/order/working_grid.d.ts.map +1 -1
  167. package/dist/modules/order/working_grid.js +8 -29
  168. package/dist/modules/order/working_grid.js.map +1 -1
  169. package/dist/modules/paths.d.ts +7 -1
  170. package/dist/modules/paths.d.ts.map +1 -1
  171. package/dist/modules/paths.js +10 -3
  172. package/dist/modules/paths.js.map +1 -1
  173. package/dist/modules/utils/text_width.d.ts +19 -0
  174. package/dist/modules/utils/text_width.d.ts.map +1 -0
  175. package/dist/modules/utils/text_width.js +97 -0
  176. package/dist/modules/utils/text_width.js.map +1 -0
  177. package/dist/pm2.d.ts.map +1 -1
  178. package/dist/pm2.js +95 -18
  179. package/dist/pm2.js.map +1 -1
  180. package/dist/scripts/sync-version.js +0 -22
  181. package/dist/scripts/sync-version.js.map +1 -1
  182. package/dist/scripts/update.js +62 -13
  183. package/dist/scripts/update.js.map +1 -1
  184. package/dist/scripts/update_dist_freshness.d.ts +12 -0
  185. package/dist/scripts/update_dist_freshness.d.ts.map +1 -1
  186. package/dist/scripts/update_dist_freshness.js +30 -1
  187. package/dist/scripts/update_dist_freshness.js.map +1 -1
  188. package/dist/unlock.d.ts +14 -2
  189. package/dist/unlock.d.ts.map +1 -1
  190. package/dist/unlock.js +73 -4
  191. package/dist/unlock.js.map +1 -1
  192. package/docs/BITSHARES_ONBOARDING.md +9 -6
  193. package/docs/COW_INVARIANTS.md +9 -0
  194. package/docs/CREDENTIAL_SECURITY.md +26 -0
  195. package/docs/DEXBOT_COMPARISON.md +11 -11
  196. package/docs/EVOLUTION.md +9 -7
  197. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  198. package/docs/GRID_PRICE_INVARIANT.md +573 -0
  199. package/docs/GRID_RECALCULATION.md +1 -1
  200. package/docs/GRID_RECONCILE.md +60 -39
  201. package/docs/LOGGING.md +17 -2
  202. package/docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md +555 -0
  203. package/docs/README.md +3 -3
  204. package/docs/WORKFLOW.md +12 -6
  205. package/docs/architecture.md +40 -38
  206. package/docs/developer_guide.md +4 -8
  207. package/market_adapter/README.md +16 -12
  208. package/modules/README.md +1 -1
  209. package/package.json +8 -4
  210. package/scripts/README.md +0 -9
  211. package/scripts/reset-settings.sh +1 -1
  212. package/analysis/ama_fitting/package.json +0 -12
  213. package/analysis/analyze_derivatives.ts +0 -320
  214. package/analysis/derivative_chart_generator.ts +0 -874
  215. package/analysis/trend_detection/SIGNAL_DOCUMENTATION.md +0 -403
  216. package/analysis/trend_detection/derivative_analyzer.ts +0 -1059
  217. package/dist/analysis/analyze_derivatives.d.ts +0 -68
  218. package/dist/analysis/analyze_derivatives.d.ts.map +0 -1
  219. package/dist/analysis/analyze_derivatives.js +0 -287
  220. package/dist/analysis/analyze_derivatives.js.map +0 -1
  221. package/dist/analysis/derivative_chart_generator.d.ts +0 -12
  222. package/dist/analysis/derivative_chart_generator.d.ts.map +0 -1
  223. package/dist/analysis/derivative_chart_generator.js +0 -891
  224. package/dist/analysis/derivative_chart_generator.js.map +0 -1
  225. package/dist/analysis/trend_detection/derivative_analyzer.d.ts +0 -248
  226. package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +0 -1
  227. package/dist/analysis/trend_detection/derivative_analyzer.js +0 -910
  228. package/dist/analysis/trend_detection/derivative_analyzer.js.map +0 -1
@@ -1,1059 +0,0 @@
1
- 'use strict';
2
-
3
- import { roundTo } from '../../modules/order/utils/math.js';
4
-
5
- /**
6
- * Derivative Analyzer
7
- *
8
- * Trend detection from the sign (and magnitude) of the slow SMA derivative.
9
- *
10
- * The analyzer intentionally stays narrow:
11
- * - SMA for direction / regime
12
- * - MACD for momentum
13
- * - RSI for exhaustion and counter-trend filtering
14
- */
15
-
16
- // ─── Simple Moving Average ───────────────────────────────────────────────────
17
-
18
- class SMA {
19
- period: number;
20
- values: number[];
21
-
22
- constructor(period: number) {
23
- this.period = period;
24
- this.values = [];
25
- }
26
-
27
- update(price: number): number | null {
28
- this.values.push(price);
29
- if (this.values.length > this.period) this.values.shift();
30
- if (this.values.length < this.period) return null;
31
- return this.values.reduce((a, b) => a + b, 0) / this.period;
32
- }
33
-
34
- isReady(): boolean { return this.values.length >= this.period; }
35
-
36
- reset(period: number): void { this.period = period; this.values = []; }
37
- }
38
-
39
- // ─── Exponential Moving Average ──────────────────────────────────────────────
40
-
41
- class EMA {
42
- period: number;
43
- multiplier: number;
44
- value: number | null;
45
- count: number;
46
- _sum: number;
47
-
48
- constructor(period: number) {
49
- this.period = period;
50
- this.multiplier = 2 / (period + 1);
51
- this.value = null;
52
- this.count = 0;
53
- this._sum = 0;
54
- }
55
-
56
- update(price: number): number | null {
57
- this.count++;
58
- if (this.count < this.period) {
59
- this._sum += price;
60
- return null;
61
- }
62
- if (this.count === this.period) {
63
- this._sum += price;
64
- this.value = this._sum / this.period;
65
- return this.value;
66
- }
67
- this.value = price * this.multiplier + this.value! * (1 - this.multiplier);
68
- return this.value;
69
- }
70
-
71
- isReady(): boolean { return this.count >= this.period; }
72
-
73
- reset(period?: number): void {
74
- this.period = period ?? this.period;
75
- this.multiplier = 2 / (this.period + 1);
76
- this.value = null;
77
- this.count = 0;
78
- this._sum = 0;
79
- }
80
- }
81
-
82
- // ─── MACD ─────────────────────────────────────────────────────────────────────
83
-
84
- class MACD {
85
- fastPeriod: number;
86
- slowPeriod: number;
87
- signalPeriod: number;
88
- fastEma: EMA;
89
- slowEma: EMA;
90
- signalEma: EMA;
91
-
92
- /**
93
- * Moving Average Convergence Divergence
94
- * macdLine = EMA(fast) - EMA(slow)
95
- * signal = EMA(signalPeriod) of macdLine
96
- * histogram = macdLine - signal
97
- */
98
- constructor(fastPeriod: number = 12, slowPeriod: number = 26, signalPeriod: number = 9) {
99
- this.fastPeriod = fastPeriod;
100
- this.slowPeriod = slowPeriod;
101
- this.signalPeriod = signalPeriod;
102
- this.fastEma = new EMA(fastPeriod);
103
- this.slowEma = new EMA(slowPeriod);
104
- this.signalEma = new EMA(signalPeriod);
105
- }
106
-
107
- update(price: number): { macd: number; signal: number; histogram: number } | null {
108
- const fast = this.fastEma.update(price);
109
- const slow = this.slowEma.update(price);
110
- if (fast === null || slow === null) return null;
111
- const macdLine = fast - slow;
112
- const signal = this.signalEma.update(macdLine);
113
- if (signal === null) return null;
114
- return { macd: macdLine, signal, histogram: macdLine - signal };
115
- }
116
-
117
- isReady(): boolean { return this.slowEma.isReady() && this.signalEma.isReady(); }
118
-
119
- reset(fastPeriod?: number, slowPeriod?: number, signalPeriod?: number): void {
120
- this.fastPeriod = fastPeriod ?? this.fastPeriod;
121
- this.slowPeriod = slowPeriod ?? this.slowPeriod;
122
- this.signalPeriod = signalPeriod ?? this.signalPeriod;
123
- this.fastEma.reset(this.fastPeriod);
124
- this.slowEma.reset(this.slowPeriod);
125
- this.signalEma.reset(this.signalPeriod);
126
- }
127
- }
128
-
129
- // ─── RSI ──────────────────────────────────────────────────────────────────────
130
-
131
- class RSI {
132
- period: number;
133
- count: number;
134
- prevPrice: number | null;
135
- avgGain: number;
136
- avgLoss: number;
137
-
138
- /**
139
- * Relative Strength Index (Wilder smoothing)
140
- * Overbought > 70, Oversold < 30
141
- */
142
- constructor(period: number = 14) {
143
- this.period = period;
144
- this.count = 0;
145
- this.prevPrice = null;
146
- this.avgGain = 0;
147
- this.avgLoss = 0;
148
- }
149
-
150
- update(price: number): number | null {
151
- if (this.prevPrice === null) { this.prevPrice = price; return null; }
152
- const change = price - this.prevPrice;
153
- this.prevPrice = price;
154
- const gain = change > 0 ? change : 0;
155
- const loss = change < 0 ? -change : 0;
156
- this.count++;
157
- if (this.count < this.period) {
158
- this.avgGain += gain;
159
- this.avgLoss += loss;
160
- return null;
161
- }
162
- if (this.count === this.period) {
163
- this.avgGain = (this.avgGain + gain) / this.period;
164
- this.avgLoss = (this.avgLoss + loss) / this.period;
165
- } else {
166
- this.avgGain = (this.avgGain * (this.period - 1) + gain) / this.period;
167
- this.avgLoss = (this.avgLoss * (this.period - 1) + loss) / this.period;
168
- }
169
- if (this.avgLoss === 0) return 100;
170
- return 100 - (100 / (1 + this.avgGain / this.avgLoss));
171
- }
172
-
173
- isReady(): boolean { return this.count >= this.period; }
174
-
175
- reset(period?: number): void {
176
- this.period = period ?? this.period;
177
- this.count = 0;
178
- this.prevPrice = null;
179
- this.avgGain = 0;
180
- this.avgLoss = 0;
181
- }
182
- }
183
-
184
- // ─── Derivative Analyzer ─────────────────────────────────────────────────────
185
-
186
- type DerivativeAnalyzerConfig = {
187
- slowSmaPeriod?: number;
188
- fastSmaPeriod?: number;
189
- minBarsForConfirmation?: number;
190
- macdFastPeriod?: number;
191
- macdSlowPeriod?: number;
192
- macdSignalPeriod?: number;
193
- rsiPeriod?: number;
194
- interpConfirmBars?: number;
195
- interpHoldBars?: number;
196
- rsiOverboughtLevel?: number;
197
- rsiOversoldLevel?: number;
198
- rsiBullThreshold?: number;
199
- rsiBearThreshold?: number;
200
- macdMinHist?: number;
201
- fastSmaCommitmentBars?: number;
202
- trendFilterEnabled?: boolean;
203
- trendFilterMinBars?: number;
204
- momentumGateEnabled?: boolean;
205
- momentumGateMinBars?: number;
206
- momentumGateRsiZone?: number;
207
- priceRegimeGateEnabled?: boolean;
208
- priceRegimeMinDistancePct?: number;
209
- };
210
-
211
- type MacdValues = {
212
- macd: number;
213
- signal: number;
214
- histogram: number;
215
- };
216
-
217
- class DerivativeAnalyzer {
218
- minBarsForConfirmation: number;
219
- slowSmaPeriod: number;
220
- sma: SMA | null;
221
- fastSmaPeriod: number;
222
- fastSma: SMA | null;
223
- macdConfig: { fastPeriod: number; slowPeriod: number; signalPeriod: number };
224
- macd: MACD;
225
- currMacd: MacdValues | null;
226
- prevMacd: MacdValues | null;
227
- rsiPeriod: number;
228
- rsi: RSI;
229
- currRsi: number | null;
230
- prevRsi: number | null;
231
- interpConfirmBars: number;
232
- interpHoldBars: number;
233
- rsiOverboughtLevel: number;
234
- rsiOversoldLevel: number;
235
- rsiBullThreshold: number;
236
- rsiBearThreshold: number;
237
- macdMinHist: number;
238
- // Resolved threshold for the MACD-line gates: falls back to the historical
239
- // 0.02 default only when unset/NaN, so an explicit 0 (strict sign test)
240
- // stays expressible via the API.
241
- macdLineGateThreshold: number;
242
- fastSmaCommitmentBars: number;
243
- trendFilterEnabled: boolean;
244
- trendFilterMinBars: number;
245
- momentumGateEnabled: boolean;
246
- momentumGateMinBars: number;
247
- momentumGateRsiZone: number;
248
- barsInMacdDivergence: number;
249
- barsInRsiDivergence: number;
250
- priceRegimeGateEnabled: boolean;
251
- priceRegimeMinDistancePct: number;
252
- prevRawInterp: string | null;
253
- barsInRawInterp: number;
254
- currInterpretation: string;
255
- currInterpretationRaw: string;
256
- pendingInterp: string | null;
257
- pendingInterpBars: number;
258
- entryBias: string;
259
- isBullWeakEntry: boolean;
260
- isBullConfirmation: boolean;
261
- isLateBullWithoutWeak: boolean;
262
- isBearWeakEntry: boolean;
263
- isBearConfirmation: boolean;
264
- isLateBearWithoutWeak: boolean;
265
- bullEntrySetupActive: boolean;
266
- bullEntrySetupConfirmed: boolean;
267
- bearEntrySetupActive: boolean;
268
- bearEntrySetupConfirmed: boolean;
269
- prevSma: number | null;
270
- currSma: number | null;
271
- prevFastSma: number | null;
272
- currFastSma: number | null;
273
- prevPrice: number | null;
274
- currPrice: number | null;
275
- prevRawSmaTrend: string | null;
276
- barsInSmaTrend: number;
277
- prevRawFastSmaTrend: string | null;
278
- barsInFastSmaTrend: number;
279
- barsAboveFastSma: number;
280
- barsBelowFastSma: number;
281
- updateCount: number;
282
-
283
- constructor(config: DerivativeAnalyzerConfig = {}) {
284
- this.minBarsForConfirmation = config.minBarsForConfirmation || 3;
285
-
286
- // SMA (slow)
287
- this.slowSmaPeriod = config.slowSmaPeriod ?? 500;
288
- this.sma = this.slowSmaPeriod ? new SMA(this.slowSmaPeriod) : null;
289
-
290
- // SMA (fast)
291
- this.fastSmaPeriod = config.fastSmaPeriod ?? 100;
292
- this.fastSma = this.fastSmaPeriod ? new SMA(this.fastSmaPeriod) : null;
293
-
294
- // MACD
295
- this.macdConfig = {
296
- fastPeriod: config.macdFastPeriod ?? 12,
297
- slowPeriod: config.macdSlowPeriod ?? 26,
298
- signalPeriod: config.macdSignalPeriod ?? 9,
299
- };
300
- this.macd = new MACD(this.macdConfig.fastPeriod, this.macdConfig.slowPeriod, this.macdConfig.signalPeriod);
301
- this.currMacd = null;
302
- this.prevMacd = null;
303
-
304
- // RSI
305
- this.rsiPeriod = config.rsiPeriod ?? 14;
306
- this.rsi = new RSI(this.rsiPeriod);
307
- this.currRsi = null;
308
- this.prevRsi = null;
309
-
310
- // Interpretation state
311
- this.interpConfirmBars = config.interpConfirmBars ?? 3;
312
- this.interpHoldBars = config.interpHoldBars ?? 0;
313
- this.rsiOverboughtLevel = config.rsiOverboughtLevel ?? 70;
314
- this.rsiOversoldLevel = config.rsiOversoldLevel ?? 30;
315
- this.rsiBullThreshold = config.rsiBullThreshold ?? 55;
316
- this.rsiBearThreshold = config.rsiBearThreshold ?? 45;
317
- this.macdMinHist = config.macdMinHist ?? 0;
318
- // Unset/NaN → historical 0.02 gate default; explicit 0 → strict sign test.
319
- // (`this.macdMinHist || 0.02` at the gates made explicit 0 unreachable.)
320
- this.macdLineGateThreshold = config.macdMinHist != null && Number.isFinite(config.macdMinHist)
321
- ? config.macdMinHist
322
- : 0.02;
323
- this.fastSmaCommitmentBars = config.fastSmaCommitmentBars ?? 2;
324
- this.trendFilterEnabled = config.trendFilterEnabled ?? false;
325
- this.trendFilterMinBars = config.trendFilterMinBars ?? 3;
326
- this.momentumGateEnabled = config.momentumGateEnabled ?? false;
327
- this.momentumGateMinBars = config.momentumGateMinBars ?? 3;
328
- this.momentumGateRsiZone = config.momentumGateRsiZone ?? 35;
329
- this.barsInMacdDivergence = 0;
330
- this.barsInRsiDivergence = 0;
331
- this.priceRegimeGateEnabled = config.priceRegimeGateEnabled ?? true;
332
- this.priceRegimeMinDistancePct = config.priceRegimeMinDistancePct ?? 0.35;
333
- this.prevRawInterp = null;
334
- this.barsInRawInterp = 0;
335
- this.currInterpretation = 'NEUTRAL';
336
- this.currInterpretationRaw = 'NEUTRAL';
337
- this.pendingInterp = null;
338
- this.pendingInterpBars = 0;
339
- this.entryBias = 'NONE';
340
- this.isBullWeakEntry = false;
341
- this.isBullConfirmation = false;
342
- this.isLateBullWithoutWeak = false;
343
- this.isBearWeakEntry = false;
344
- this.isBearConfirmation = false;
345
- this.isLateBearWithoutWeak = false;
346
- this.bullEntrySetupActive = false;
347
- this.bullEntrySetupConfirmed = false;
348
- this.bearEntrySetupActive = false;
349
- this.bearEntrySetupConfirmed = false;
350
-
351
- // Previous / current SMA values (for derivative)
352
- this.prevSma = null;
353
- this.currSma = null;
354
- this.prevFastSma = null;
355
- this.currFastSma = null;
356
- this.prevPrice = null;
357
- this.currPrice = null;
358
-
359
- // Independent trend states
360
- this.prevRawSmaTrend = null;
361
- this.barsInSmaTrend = 0;
362
- this.prevRawFastSmaTrend = null;
363
- this.barsInFastSmaTrend = 0;
364
- this.barsAboveFastSma = 0;
365
- this.barsBelowFastSma = 0;
366
-
367
- this.updateCount = 0;
368
- }
369
-
370
- update(price: number, timestamp: number | null = null): ReturnType<DerivativeAnalyzer['getAnalysis']> {
371
- if (!Number.isFinite(price) || price <= 0) {
372
- throw new Error('price must be a positive finite number');
373
- }
374
-
375
- // Update current price at the start of the bar (before interpretation)
376
- this.prevPrice = this.currPrice;
377
- this.currPrice = price;
378
-
379
- // SMA (slow)
380
- if (this.sma) {
381
- this.prevSma = this.currSma;
382
- this.currSma = this.sma.update(price);
383
- }
384
-
385
- // SMA (fast)
386
- if (this.fastSma) {
387
- this.prevFastSma = this.currFastSma;
388
- this.currFastSma = this.fastSma.update(price);
389
- }
390
-
391
- // MACD — normalized as % of price so scale is comparable across all price levels
392
- if (this.macd) {
393
- this.prevMacd = this.currMacd;
394
- const raw = this.macd.update(price);
395
- if (raw !== null && price > 0) {
396
- this.currMacd = {
397
- macd: (raw.macd / price) * 100,
398
- signal: (raw.signal / price) * 100,
399
- histogram: (raw.histogram / price) * 100,
400
- };
401
- } else {
402
- this.currMacd = null;
403
- }
404
- }
405
-
406
- // RSI
407
- if (this.rsi) {
408
- this.prevRsi = this.currRsi;
409
- this.currRsi = this.rsi.update(price);
410
- }
411
-
412
- // Advance momentum divergence counters (for Momentum Gate optimization)
413
- this._advanceMomentumDivergence();
414
-
415
- // Fast-SMA commitment tracking: price vs fast MA position
416
- this._advancePriceFastSmaPosition();
417
-
418
- // Interpretation (computed after MACD + RSI are updated)
419
- if (this.macd || this.rsi) {
420
- const prevInterpretation = this.currInterpretation;
421
- this._resetEntryBias();
422
- let rawInterp = this.trendFilterEnabled
423
- ? this._applyTrendFilter(this._computeInterpretation())
424
- : this._computeInterpretation();
425
-
426
- // Opt 13 — Post-filter MACD line gate (bull/bear trap suppression)
427
- // The MACD line is the authoritative momentum regime indicator:
428
- // line > threshold → bullish regime; line < -threshold → bearish regime.
429
- // Any signal that contradicts the confirmed regime is suppressed to NEUTRAL.
430
- // This ignores marginal crossings at ~0 and ensures signals only start when
431
- // momentum is clearly confirmed.
432
- if (this.macd && this.currMacd !== null) {
433
- const threshold = this.macdLineGateThreshold;
434
- if ((rawInterp === 'BEAR' || rawInterp === 'BEAR_WEAK') && this.currMacd.macd > -threshold)
435
- rawInterp = 'NEUTRAL';
436
- if ((rawInterp === 'BULL' || rawInterp === 'BULL_WEAK') && this.currMacd.macd < threshold)
437
- rawInterp = 'NEUTRAL';
438
- }
439
-
440
- this.currInterpretationRaw = rawInterp;
441
- // BULL/BEAR require confirmation bars; other states are immediate
442
- if (rawInterp === 'BULL' || rawInterp === 'BEAR') {
443
- // Check if fast-SMA commitment is met (required before counting confirmation)
444
- let fastSmaPassesCommitment = true;
445
- if (this.fastSma && this.currFastSma !== null && this.currPrice !== null) {
446
- if (rawInterp === 'BULL') {
447
- fastSmaPassesCommitment = this.barsAboveFastSma >= this.fastSmaCommitmentBars;
448
- } else if (rawInterp === 'BEAR') {
449
- fastSmaPassesCommitment = this.barsBelowFastSma >= this.fastSmaCommitmentBars;
450
- }
451
- }
452
-
453
- // Only count confirmation bars if the fast-SMA commitment passes
454
- if (fastSmaPassesCommitment) {
455
- if (rawInterp === this.prevRawInterp) {
456
- this.barsInRawInterp++;
457
- } else {
458
- this.prevRawInterp = rawInterp;
459
- this.barsInRawInterp = 1;
460
- }
461
- } else {
462
- // Commitment not met — reset confirmation counter
463
- this.prevRawInterp = rawInterp === 'BULL' ? 'BEAR' : 'BULL';
464
- this.barsInRawInterp = 0;
465
- }
466
-
467
- const confirmed = this.barsInRawInterp >= this.interpConfirmBars
468
- ? rawInterp : 'NEUTRAL';
469
- this._applyWithHysteresis(confirmed);
470
- } else {
471
- this.prevRawInterp = rawInterp;
472
- this.barsInRawInterp = 0;
473
- this._applyWithHysteresis(rawInterp);
474
- }
475
- this._advanceEntryBias(prevInterpretation);
476
- }
477
-
478
- this.updateCount++;
479
-
480
- // Update trend states
481
- const smaRaw = this._rawSmaTrend();
482
- const fastSmaRaw = this._rawFastSmaTrend();
483
- this._advanceTrend(smaRaw, 'Sma');
484
- this._advanceTrend(fastSmaRaw, 'FastSma');
485
-
486
- const result = this.getAnalysis();
487
- if (timestamp !== null) result.timestamp = timestamp;
488
- return result;
489
- }
490
-
491
- _advanceTrend(raw: string, key: string): void {
492
- const prevKey = `prevRaw${key}Trend` as keyof this;
493
- const barsKey = `barsIn${key}Trend` as keyof this;
494
- if (raw !== (this as any)[prevKey]) {
495
- (this as any)[prevKey] = raw;
496
- (this as any)[barsKey] = 1;
497
- } else {
498
- (this as any)[barsKey]++;
499
- }
500
- }
501
-
502
- // ── Derivative signs ───────────────────────────────────────
503
-
504
- _rawSmaTrend(): string {
505
- if (!this.sma || this.prevSma === null || this.currSma === null) return 'NEUTRAL';
506
- if (this.currSma > this.prevSma) return 'UP';
507
- if (this.currSma < this.prevSma) return 'DOWN';
508
- return 'NEUTRAL';
509
- }
510
-
511
- _rawFastSmaTrend(): string {
512
- if (!this.fastSma || this.prevFastSma === null || this.currFastSma === null) return 'NEUTRAL';
513
- if (this.currFastSma > this.prevFastSma) return 'UP';
514
- if (this.currFastSma < this.prevFastSma) return 'DOWN';
515
- return 'NEUTRAL';
516
- }
517
-
518
- _advancePriceFastSmaPosition(): void {
519
- if (!this.fastSma || this.currFastSma === null || this.currPrice === null) {
520
- this.barsAboveFastSma = 0;
521
- this.barsBelowFastSma = 0;
522
- return;
523
- }
524
- if (this.currPrice > this.currFastSma) {
525
- this.barsAboveFastSma++;
526
- this.barsBelowFastSma = 0;
527
- } else {
528
- this.barsBelowFastSma++;
529
- this.barsAboveFastSma = 0;
530
- }
531
- }
532
-
533
- _advanceMomentumDivergence(): void {
534
- if (!this.momentumGateEnabled || !this.macd || !this.rsi || !this.sma) return;
535
- if (this.currMacd === null || this.currRsi === null) return;
536
-
537
- const slowDir = this._rawSmaTrend();
538
- if (slowDir === 'NEUTRAL') {
539
- this.barsInMacdDivergence = 0;
540
- this.barsInRsiDivergence = 0;
541
- return;
542
- }
543
-
544
- const macdOpposes =
545
- (slowDir === 'UP' && this.currMacd.histogram < -this.macdMinHist) ||
546
- (slowDir === 'DOWN' && this.currMacd.histogram > this.macdMinHist);
547
-
548
- const rsiOpposes =
549
- (slowDir === 'UP' && this.currRsi < this.momentumGateRsiZone) ||
550
- (slowDir === 'DOWN' && this.currRsi > (100 - this.momentumGateRsiZone));
551
-
552
- this.barsInMacdDivergence = macdOpposes ? this.barsInMacdDivergence + 1 : 0;
553
- this.barsInRsiDivergence = rsiOpposes ? this.barsInRsiDivergence + 1 : 0;
554
- }
555
-
556
- // ── Trend filter ─────────────────────────────────────────
557
-
558
- _applyTrendFilter(interp: string): string {
559
- // OB/OS are exit signals — not suppressed by trend direction
560
- if (interp === 'OVERBOUGHT' || interp === 'OVERSOLD') return interp;
561
-
562
- // Compute fast MA direction and sustained bars count
563
- let fastDir: string, fastBarsInDir: number;
564
- if (this.fastSma) {
565
- fastDir = this._rawFastSmaTrend();
566
- fastBarsInDir = (fastDir === this.prevRawFastSmaTrend ? this.barsInFastSmaTrend + 1 : 1);
567
- } else if (this.sma) {
568
- fastDir = this._rawSmaTrend();
569
- fastBarsInDir = (fastDir === this.prevRawSmaTrend ? this.barsInSmaTrend + 1 : 1);
570
- } else {
571
- return interp;
572
- }
573
-
574
- if (fastDir === 'NEUTRAL') return interp;
575
-
576
- // Primary gate: fast MA sustained in opposite direction → suppress to NEUTRAL
577
- let suppressedByTrendFilter = false;
578
- if (fastBarsInDir >= this.trendFilterMinBars) {
579
- if (fastDir === 'UP' && (interp === 'BEAR' || interp === 'BEAR_WEAK')) {
580
- interp = 'NEUTRAL';
581
- suppressedByTrendFilter = true;
582
- }
583
- if (fastDir === 'DOWN' && (interp === 'BULL' || interp === 'BULL_WEAK')) {
584
- interp = 'NEUTRAL';
585
- suppressedByTrendFilter = true;
586
- }
587
- }
588
-
589
- // Momentum Gate (Opt 11): if MACD + RSI both diverge from SMA(500) for
590
- // >= momentumGateMinBars, allow signal through as WEAK instead of suppressing to NEUTRAL.
591
- if (suppressedByTrendFilter && this.momentumGateEnabled && this.sma) {
592
- const slowDir = this._rawSmaTrend();
593
- const macdConf = this.barsInMacdDivergence >= this.momentumGateMinBars;
594
- const rsiConf = this.barsInRsiDivergence >= this.momentumGateMinBars;
595
-
596
- if (macdConf && rsiConf && slowDir !== 'NEUTRAL') {
597
- if (slowDir === 'UP' && (this.currMacd?.histogram ?? 0) < -this.macdMinHist) {
598
- interp = 'BEAR_WEAK';
599
- }
600
- if (slowDir === 'DOWN' && (this.currMacd?.histogram ?? 0) > this.macdMinHist) {
601
- interp = 'BULL_WEAK';
602
- }
603
- }
604
- }
605
-
606
- // Macro regime gate: when fastSMA and SMA(slow) are both present and disagree,
607
- // cap full BULL/BEAR to WEAK — a short-term dip/bounce against the macro trend
608
- // does not warrant a confirmed directional signal.
609
- if (this._violatesMacroDisagreeGate(interp, fastDir)) {
610
- if (interp === 'BULL') return 'BULL_WEAK';
611
- if (interp === 'BEAR') return 'BEAR_WEAK';
612
- }
613
-
614
- // Fast-SMA commitment gate — price vs fast MA cross-check (N-bar commitment required)
615
- if (this.fastSma && this.currFastSma !== null && this.currPrice !== null) {
616
- if (interp === 'BULL' && this.barsAboveFastSma < this.fastSmaCommitmentBars) return 'BULL_WEAK';
617
- if (interp === 'BEAR' && this.barsBelowFastSma < this.fastSmaCommitmentBars) return 'BEAR_WEAK';
618
- }
619
-
620
- // Opt 12 — MACD Line & Histogram regime check
621
- if (this.macd && this.currMacd !== null) {
622
- if (interp === 'BULL' && this.currMacd.macd <= 0) return 'BULL_WEAK';
623
- if (interp === 'BEAR' && this.currMacd.macd >= 0) return 'BEAR_WEAK';
624
- if (interp === 'BEAR' && this.currMacd.histogram > 0) return 'BEAR_WEAK';
625
- }
626
-
627
- // Macro price regime gate: require a minimum clearance beyond slow SMA.
628
- if (this._violatesPriceRegimeGate(interp)) return 'NEUTRAL';
629
-
630
- return interp;
631
- }
632
-
633
- // ── Hysteresis ────────────────────────────────────────────
634
-
635
- _applyWithHysteresis(newInterp: string): void {
636
- // OB/OS are exit signals — always immediate, bypass hold
637
- if (newInterp === 'OVERBOUGHT' || newInterp === 'OVERSOLD') {
638
- this.currInterpretation = newInterp;
639
- this.pendingInterp = null;
640
- this.pendingInterpBars = 0;
641
- return;
642
- }
643
-
644
- const curr = this.currInterpretation;
645
- const isDowngrade = (curr === 'BULL' && newInterp !== 'BULL')
646
- || (curr === 'BEAR' && newInterp !== 'BEAR');
647
-
648
- // Hard regime gates define when a confirmed signal is no longer valid at all.
649
- // In those cases hysteresis must not keep a stale BULL/BEAR alive after invalidation.
650
- const hardInvalidation = this._isHardInvalidation(curr);
651
-
652
- if (!isDowngrade || this.interpHoldBars <= 0 || hardInvalidation) {
653
- this.currInterpretation = newInterp;
654
- this.pendingInterp = null;
655
- this.pendingInterpBars = 0;
656
- return;
657
- }
658
-
659
- // Debounce the downgrade — must hold for interpHoldBars bars
660
- if (newInterp === this.pendingInterp) {
661
- this.pendingInterpBars++;
662
- if (this.pendingInterpBars >= this.interpHoldBars) {
663
- this.currInterpretation = newInterp;
664
- this.pendingInterp = null;
665
- this.pendingInterpBars = 0;
666
- }
667
- } else {
668
- this.pendingInterp = newInterp;
669
- this.pendingInterpBars = 1;
670
- }
671
- }
672
-
673
- _resetEntryBias(): void {
674
- this.entryBias = 'NONE';
675
- this.isBullWeakEntry = false;
676
- this.isBullConfirmation = false;
677
- this.isLateBullWithoutWeak = false;
678
- this.isBearWeakEntry = false;
679
- this.isBearConfirmation = false;
680
- this.isLateBearWithoutWeak = false;
681
- }
682
-
683
- _advanceEntryBias(prevInterpretation: string): void {
684
- const curr = this.currInterpretation;
685
- const slowDir = this._rawSmaTrend();
686
- const fastDir = this.fastSma ? this._rawFastSmaTrend() : slowDir;
687
- const prevBullish = prevInterpretation === 'BULL' || prevInterpretation === 'BULL_WEAK';
688
- const currBullish = curr === 'BULL' || curr === 'BULL_WEAK';
689
- const prevBearish = prevInterpretation === 'BEAR' || prevInterpretation === 'BEAR_WEAK';
690
- const currBearish = curr === 'BEAR' || curr === 'BEAR_WEAK';
691
- const bullMacroAligned = slowDir === 'UP';
692
- const bearMacroAligned = slowDir === 'DOWN';
693
- const bullEntryAligned = bullMacroAligned && fastDir === 'UP';
694
- const bearEntryAligned = bearMacroAligned && fastDir === 'DOWN';
695
-
696
- if (!currBullish) {
697
- this.bullEntrySetupActive = false;
698
- this.bullEntrySetupConfirmed = false;
699
- }
700
-
701
- // Keep weak bullish states visible on the chart, but do not emit long
702
- // entry events until both slow and fast SMA trends are aligned up.
703
- if (!bullEntryAligned) {
704
- this.bullEntrySetupActive = false;
705
- this.bullEntrySetupConfirmed = false;
706
- } else if (curr === 'BULL_WEAK' && (!prevBullish || !this.bullEntrySetupActive)) {
707
- this.bullEntrySetupActive = true;
708
- this.bullEntrySetupConfirmed = false;
709
- this.entryBias = 'EARLY_LONG';
710
- this.isBullWeakEntry = true;
711
- return;
712
- }
713
-
714
- if (curr === 'BULL' && prevInterpretation !== 'BULL') {
715
- if (this.bullEntrySetupActive && !this.bullEntrySetupConfirmed) {
716
- this.bullEntrySetupConfirmed = true;
717
- this.entryBias = 'CONFIRM_LONG';
718
- this.isBullConfirmation = true;
719
- }
720
- }
721
-
722
- if (!currBearish) {
723
- this.bearEntrySetupActive = false;
724
- this.bearEntrySetupConfirmed = false;
725
- }
726
-
727
- // Keep weak bearish states visible on the chart, but do not emit short
728
- // entry events until both slow and fast SMA trends are aligned down.
729
- if (!bearEntryAligned) {
730
- this.bearEntrySetupActive = false;
731
- this.bearEntrySetupConfirmed = false;
732
- return;
733
- }
734
-
735
- if (curr === 'BEAR_WEAK' && (!prevBearish || !this.bearEntrySetupActive)) {
736
- this.bearEntrySetupActive = true;
737
- this.bearEntrySetupConfirmed = false;
738
- this.entryBias = 'EARLY_SHORT';
739
- this.isBearWeakEntry = true;
740
- return;
741
- }
742
-
743
- if (curr === 'BEAR' && prevInterpretation !== 'BEAR') {
744
- if (this.bearEntrySetupActive && !this.bearEntrySetupConfirmed) {
745
- this.bearEntrySetupConfirmed = true;
746
- this.entryBias = 'CONFIRM_SHORT';
747
- this.isBearConfirmation = true;
748
- }
749
- }
750
- }
751
-
752
- _isHardInvalidation(interp: string): boolean {
753
- if (interp !== 'BULL' && interp !== 'BEAR') return false;
754
-
755
- if (this.macd && this.currMacd !== null) {
756
- const threshold = this.macdLineGateThreshold;
757
- if (interp === 'BULL' && this.currMacd.macd < threshold) return true;
758
- if (interp === 'BEAR' && this.currMacd.macd > -threshold) return true;
759
- }
760
-
761
- if (this._violatesMacroDisagreeGate(interp)) return true;
762
- if (this._violatesPriceRegimeGate(interp)) return true;
763
-
764
- return false;
765
- }
766
-
767
- _violatesMacroDisagreeGate(interp: string, fastDirOverride: string | null = null): boolean {
768
- if (!this.trendFilterEnabled || !this.fastSma || !this.sma) return false;
769
- if (interp !== 'BULL' && interp !== 'BEAR') return false;
770
-
771
- const fastDir = fastDirOverride ?? this._rawFastSmaTrend();
772
- const slowDir = this._rawSmaTrend();
773
- const slowBarsInDir = (slowDir === this.prevRawSmaTrend ? this.barsInSmaTrend + 1 : 1);
774
-
775
- if (fastDir === 'NEUTRAL' || slowDir === 'NEUTRAL') return false;
776
- if (slowBarsInDir < this.trendFilterMinBars) return false;
777
- if (fastDir === slowDir) return false;
778
-
779
- return fastDir !== slowDir;
780
- }
781
-
782
- _violatesPriceRegimeGate(interp: string): boolean {
783
- if (!this.priceRegimeGateEnabled || !this.sma || this.currSma === null || this.currPrice === null) {
784
- return false;
785
- }
786
- if (!['BULL', 'BULL_WEAK', 'BEAR', 'BEAR_WEAK'].includes(interp)) return false;
787
-
788
- const distanceMultiplier = this.priceRegimeMinDistancePct / 100;
789
- const bullFloor = this.currSma * (1 + distanceMultiplier);
790
- const bearCeiling = this.currSma * (1 - distanceMultiplier);
791
-
792
- if ((interp === 'BULL' || interp === 'BULL_WEAK') && this.currPrice < bullFloor) return true;
793
- if ((interp === 'BEAR' || interp === 'BEAR_WEAK') && this.currPrice > bearCeiling) return true;
794
- return false;
795
- }
796
-
797
- // ── Interpretation ────────────────────────────────────────
798
-
799
- _computeInterpretation(): string {
800
- const hist = this.currMacd?.histogram ?? null;
801
- const macdLine = this.currMacd?.macd ?? null;
802
- const prevHist = this.prevMacd?.histogram ?? null;
803
- const prevMacdLine = this.prevMacd?.macd ?? null;
804
- const rsi = this.currRsi;
805
-
806
- if (hist === null && rsi === null) return 'NEUTRAL';
807
-
808
- // 1. MACD histogram sign with minimum magnitude filter
809
- const histBull = hist !== null && hist > this.macdMinHist;
810
- const histBear = hist !== null && hist < -this.macdMinHist;
811
-
812
- // 2. MACD histogram momentum
813
- const histMom = (hist !== null && prevHist !== null) ? hist - prevHist : null;
814
- const histWeakeningBull = histBull && histMom !== null && histMom < 0;
815
- const histWeakeningBear = histBear && histMom !== null && histMom > 0;
816
-
817
- // 3. MACD zero-line position
818
- const macdAboveZero = macdLine !== null && macdLine > 0;
819
- const macdBelowZero = macdLine !== null && macdLine < 0;
820
-
821
- // 5. MACD line slope — is the MACD line itself rising or falling?
822
- const macdLineSlope = (macdLine !== null && prevMacdLine !== null) ? macdLine - prevMacdLine : null;
823
- const macdLineRising = macdLineSlope !== null && macdLineSlope > 0;
824
- const macdLineFalling = macdLineSlope !== null && macdLineSlope < 0;
825
-
826
- // 6. RSI extremes (highest priority — exit signals)
827
- const rsiOverbought = rsi !== null && rsi > this.rsiOverboughtLevel;
828
- const rsiOversold = rsi !== null && rsi < this.rsiOversoldLevel;
829
-
830
- // 7. RSI direction — rising or falling?
831
- const rsiSlope = (rsi !== null && this.prevRsi !== null) ? rsi - this.prevRsi : null;
832
- const rsiRising = rsiSlope !== null && rsiSlope > 0;
833
- const rsiFalling = rsiSlope !== null && rsiSlope < 0;
834
-
835
- // 8. RSI counter-trend: in opposing zone AND not recovering (direction confirms divergence)
836
- const rsiCounterBull = rsi !== null && rsi < this.rsiBearThreshold && !rsiRising;
837
- const rsiCounterBear = rsi !== null && rsi > this.rsiBullThreshold && !rsiFalling;
838
-
839
- // Base signal driven by MACD histogram (RSI no longer hard-gates)
840
- let raw = 'NEUTRAL';
841
- if (histBull) raw = 'BULL';
842
- if (histBear) raw = 'BEAR';
843
-
844
- // RSI extreme exit — override immediately
845
- if (raw === 'BULL' && rsiOverbought) return 'OVERBOUGHT';
846
- if (raw === 'BEAR' && rsiOversold) return 'OVERSOLD';
847
-
848
- // Weak conditions:
849
- // histFading: histogram shrinking AND MACD line not supporting (any zero position)
850
- // dualWeak: histogram positive but MACD line still on wrong side of zero AND not recovering
851
- // rsiCounter: RSI in counter-trend territory AND direction confirms (standalone)
852
- //
853
- // Note: histogram = MACD line − Signal line, so macdBelowSignal is always false when
854
- // histBull is true. Signal line crossover is not a valid structural check here.
855
- const histFadingBull = histWeakeningBull && !macdLineRising;
856
- const histFadingBear = histWeakeningBear && !macdLineFalling;
857
- const dualWeakBull = histBull && macdBelowZero && !macdLineRising;
858
- const dualWeakBear = histBear && macdAboveZero && !macdLineFalling;
859
-
860
- const bullWeak = histFadingBull || dualWeakBull || rsiCounterBull;
861
- const bearWeak = histFadingBear || dualWeakBear || rsiCounterBear;
862
-
863
- if (raw === 'BULL' && bullWeak) return 'BULL_WEAK';
864
- if (raw === 'BEAR' && bearWeak) return 'BEAR_WEAK';
865
-
866
- return raw;
867
- }
868
-
869
- // ── Analysis ───────────────────────────────────────────────
870
-
871
- getAnalysis(): {
872
- isReady: boolean;
873
- reason?: string;
874
- trend: string;
875
- confidence: number;
876
- isConfirmed: boolean;
877
- rawTrend?: string;
878
- barsInTrend?: number;
879
- smaRawTrend: string;
880
- smaTrend: string;
881
- smaBarsInTrend: number;
882
- smaConfidence: number;
883
- fastSmaRawTrend: string;
884
- fastSmaTrend: string;
885
- fastSmaBarsInTrend: number;
886
- fastSmaConfidence: number;
887
- fastSmaValue: number | null;
888
- slowSma: number | null;
889
- macdLine: number | null;
890
- macdSignal: number | null;
891
- macdHistogram: number | null;
892
- macdTrend: string | null;
893
- rsi: number | null;
894
- rsiZone: string | null;
895
- interpretation: string;
896
- interpretationRaw: string;
897
- interpretationBars: number;
898
- entryBias: string;
899
- isBullWeakEntry: boolean;
900
- isBullConfirmation: boolean;
901
- isLateBullWithoutWeak: boolean;
902
- isBearWeakEntry: boolean;
903
- isBearConfirmation: boolean;
904
- isLateBearWithoutWeak: boolean;
905
- price: number | null;
906
- updateCount: number;
907
- timestamp?: number;
908
- } {
909
- const warmupNeeded = Math.max(
910
- this.sma ? this.slowSmaPeriod : 0,
911
- this.fastSma ? this.fastSmaPeriod : 0,
912
- this.macd ? this.macdConfig.slowPeriod + this.macdConfig.signalPeriod : 0,
913
- this.rsi ? this.rsiPeriod + 1 : 0
914
- );
915
-
916
- if (this.updateCount < warmupNeeded) {
917
- return {
918
- isReady: false,
919
- reason: `Warming up: ${this.updateCount}/${warmupNeeded} candles`,
920
- trend: 'NEUTRAL', confidence: 0, isConfirmed: false,
921
- ...this._emptySignals(),
922
- price: this.currPrice, updateCount: this.updateCount,
923
- };
924
- }
925
-
926
- const smaRaw = this._rawSmaTrend();
927
- const fastSmaRaw = this._rawFastSmaTrend();
928
- const smaConf = this._conf(smaRaw, this.barsInSmaTrend);
929
- const fastSmaConf = this._conf(fastSmaRaw, this.barsInFastSmaTrend);
930
-
931
- const smaConfirmed = smaRaw !== 'NEUTRAL' && this.barsInSmaTrend >= this.minBarsForConfirmation;
932
- const fastSmaConfirmed = fastSmaRaw !== 'NEUTRAL' && this.barsInFastSmaTrend >= this.minBarsForConfirmation;
933
-
934
- // Primary: fastSMA > SMA
935
- const primary = this.fastSma ? fastSmaRaw : smaRaw;
936
- const primConf = this.fastSma ? fastSmaConf : smaConf;
937
- const primConfirm = this.fastSma ? fastSmaConfirmed : smaConfirmed;
938
-
939
- return {
940
- isReady: true,
941
- trend: primConfirm ? primary : 'NEUTRAL',
942
- confidence: primConf,
943
- isConfirmed: primConfirm,
944
- rawTrend: primary,
945
- barsInTrend: this.fastSma ? this.barsInFastSmaTrend : this.barsInSmaTrend,
946
- // SMA (slow)
947
- smaRawTrend: smaRaw,
948
- smaTrend: smaConfirmed ? smaRaw : 'NEUTRAL',
949
- smaBarsInTrend: this.barsInSmaTrend,
950
- smaConfidence: smaConf,
951
- // SMA (fast)
952
- fastSmaRawTrend: fastSmaRaw,
953
- fastSmaTrend: fastSmaConfirmed ? fastSmaRaw : 'NEUTRAL',
954
- fastSmaBarsInTrend: this.barsInFastSmaTrend,
955
- fastSmaConfidence: fastSmaConf,
956
- fastSmaValue: this.currFastSma !== null ? roundTo(this.currFastSma, 1e6) : null,
957
- // MACD
958
- macdLine: this.currMacd ? roundTo(this.currMacd.macd, 1e8) : null,
959
- macdSignal: this.currMacd ? roundTo(this.currMacd.signal, 1e8) : null,
960
- macdHistogram: this.currMacd ? roundTo(this.currMacd.histogram, 1e8) : null,
961
- macdTrend: this.currMacd
962
- ? (this.currMacd.histogram > 0 ? 'BULL' : this.currMacd.histogram < 0 ? 'BEAR' : 'NEUTRAL')
963
- : null,
964
- // RSI
965
- rsi: this.currRsi !== null ? roundTo(this.currRsi, 100) : null,
966
- rsiZone: this.currRsi !== null
967
- ? (this.currRsi > this.rsiOverboughtLevel ? 'OVERBOUGHT' : this.currRsi < this.rsiOversoldLevel ? 'OVERSOLD' : 'NEUTRAL')
968
- : null,
969
- // Combined interpretation
970
- interpretation: this.currInterpretation,
971
- interpretationRaw: this.currInterpretationRaw,
972
- interpretationBars: this.barsInRawInterp,
973
- entryBias: this.entryBias,
974
- isBullWeakEntry: this.isBullWeakEntry,
975
- isBullConfirmation: this.isBullConfirmation,
976
- isLateBullWithoutWeak: this.isLateBullWithoutWeak,
977
- isBearWeakEntry: this.isBearWeakEntry,
978
- isBearConfirmation: this.isBearConfirmation,
979
- isLateBearWithoutWeak: this.isLateBearWithoutWeak,
980
- // Values
981
- price: this.currPrice,
982
- slowSma: this.currSma !== null ? roundTo(this.currSma, 1e6) : null,
983
- updateCount: this.updateCount,
984
- };
985
- }
986
-
987
- _conf(raw: string, bars: number): number {
988
- return raw !== 'NEUTRAL' ? Math.min(100, Math.round((bars / 20) * 100)) : 0;
989
- }
990
-
991
- _emptySignals(): {
992
- smaRawTrend: string; smaTrend: string; smaBarsInTrend: number; smaConfidence: number;
993
- fastSmaRawTrend: string; fastSmaTrend: string; fastSmaBarsInTrend: number; fastSmaConfidence: number; fastSmaValue: null;
994
- slowSma: null;
995
- macdLine: null; macdSignal: null; macdHistogram: null; macdTrend: null;
996
- rsi: null; rsiZone: null;
997
- interpretation: string; interpretationRaw: string; interpretationBars: number;
998
- entryBias: string;
999
- isBullWeakEntry: boolean; isBullConfirmation: boolean; isLateBullWithoutWeak: boolean;
1000
- isBearWeakEntry: boolean; isBearConfirmation: boolean; isLateBearWithoutWeak: boolean;
1001
- } {
1002
- return {
1003
- smaRawTrend: 'NEUTRAL', smaTrend: 'NEUTRAL', smaBarsInTrend: 0, smaConfidence: 0,
1004
- fastSmaRawTrend: 'NEUTRAL', fastSmaTrend: 'NEUTRAL', fastSmaBarsInTrend: 0, fastSmaConfidence: 0, fastSmaValue: null,
1005
- slowSma: null,
1006
- macdLine: null, macdSignal: null, macdHistogram: null, macdTrend: null,
1007
- rsi: null, rsiZone: null,
1008
- interpretation: 'NEUTRAL', interpretationRaw: 'NEUTRAL', interpretationBars: 0,
1009
- entryBias: 'NONE',
1010
- isBullWeakEntry: false,
1011
- isBullConfirmation: false,
1012
- isLateBullWithoutWeak: false,
1013
- isBearWeakEntry: false,
1014
- isBearConfirmation: false,
1015
- isLateBearWithoutWeak: false,
1016
- };
1017
- }
1018
-
1019
- reset(): void {
1020
- if (this.sma) this.sma.reset(this.slowSmaPeriod);
1021
- if (this.fastSma) this.fastSma.reset(this.fastSmaPeriod);
1022
- if (this.macd) this.macd.reset(this.macdConfig.fastPeriod, this.macdConfig.slowPeriod, this.macdConfig.signalPeriod);
1023
- if (this.rsi) this.rsi.reset(this.rsiPeriod);
1024
- this.currMacd = null; this.prevMacd = null;
1025
- this.currRsi = null;
1026
- this.prevRawInterp = null; this.barsInRawInterp = 0;
1027
- this.currInterpretation = 'NEUTRAL'; this.currInterpretationRaw = 'NEUTRAL';
1028
- this.pendingInterp = null;
1029
- this.pendingInterpBars = 0;
1030
- this.entryBias = 'NONE';
1031
- this.isBullWeakEntry = false;
1032
- this.isBullConfirmation = false;
1033
- this.isLateBullWithoutWeak = false;
1034
- this.isBearWeakEntry = false;
1035
- this.isBearConfirmation = false;
1036
- this.isLateBearWithoutWeak = false;
1037
- this.bullEntrySetupActive = false;
1038
- this.bullEntrySetupConfirmed = false;
1039
- this.bearEntrySetupActive = false;
1040
- this.bearEntrySetupConfirmed = false;
1041
- this.prevSma = null;
1042
- this.currSma = null;
1043
- this.prevFastSma = null;
1044
- this.currFastSma = null;
1045
- this.currPrice = null;
1046
- this.prevRawSmaTrend = null;
1047
- this.barsInSmaTrend = 0;
1048
- this.prevRawFastSmaTrend = null;
1049
- this.barsInFastSmaTrend = 0;
1050
- this.barsAboveFastSma = 0;
1051
- this.barsBelowFastSma = 0;
1052
- this.updateCount = 0;
1053
- }
1054
-
1055
- getUpdateCount(): number { return this.updateCount; }
1056
- }
1057
-
1058
- export { DerivativeAnalyzer }
1059
-