dexbot 1.5.3 → 1.6.0

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 (231) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +4 -3
  3. package/analysis/README.md +1 -2
  4. package/analysis/ama_fitting/fetch_lp_candles.ts +1 -9
  5. package/analysis/ama_fitting/package.json +1 -1
  6. package/analysis/analyze_derivatives.ts +1 -1
  7. package/analysis/bot_usage/discover_bot_accounts.ts +12 -6
  8. package/analysis/resolve_source.ts +9 -2
  9. package/analysis/tradingview/README.md +29 -11
  10. package/analysis/tradingview/analyze_tradingview.ts +92 -3
  11. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1120 -182
  12. package/analysis/trend_detection/package.json +1 -1
  13. package/claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md +1 -1
  14. package/claw/modules/dexbot_profiles.ts +22 -2
  15. package/claw/package.json +1 -1
  16. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  17. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  18. package/claw/skills/launcher-ops/references/launcher-workflow.md +1 -1
  19. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  20. package/claw/tests/test_dexbot_profiles.ts +65 -0
  21. package/dist/analysis/ama_fitting/fetch_lp_candles.js +1 -9
  22. package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -1
  23. package/dist/analysis/analyze_derivatives.js +1 -1
  24. package/dist/analysis/bot_usage/discover_bot_accounts.js +11 -6
  25. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  26. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +2 -0
  27. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  28. package/dist/analysis/resolve_source.d.ts +1 -0
  29. package/dist/analysis/resolve_source.d.ts.map +1 -1
  30. package/dist/analysis/resolve_source.js +6 -2
  31. package/dist/analysis/resolve_source.js.map +1 -1
  32. package/dist/analysis/tradingview/analyze_tradingview.d.ts +5 -0
  33. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
  34. package/dist/analysis/tradingview/analyze_tradingview.js +112 -3
  35. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  36. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -1
  37. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  38. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1118 -182
  39. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  40. package/dist/bot.js +1 -1
  41. package/dist/bot.js.map +1 -1
  42. package/dist/dexbot.js +3 -3
  43. package/dist/dexbot.js.map +1 -1
  44. package/dist/market_adapter/ama_signal_runner.js +2 -2
  45. package/dist/market_adapter/candle_utils.d.ts +6 -0
  46. package/dist/market_adapter/candle_utils.d.ts.map +1 -1
  47. package/dist/market_adapter/candle_utils.js +6 -0
  48. package/dist/market_adapter/candle_utils.js.map +1 -1
  49. package/dist/market_adapter/core/kibana_candles.d.ts +17 -1
  50. package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
  51. package/dist/market_adapter/core/kibana_candles.js +101 -19
  52. package/dist/market_adapter/core/kibana_candles.js.map +1 -1
  53. package/dist/market_adapter/core/kibana_client.d.ts +2 -0
  54. package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
  55. package/dist/market_adapter/core/kibana_client.js +31 -4
  56. package/dist/market_adapter/core/kibana_client.js.map +1 -1
  57. package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
  58. package/dist/market_adapter/core/market_adapter_service.js +0 -6
  59. package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
  60. package/dist/market_adapter/inputs/fetch_book_data.d.ts +14 -0
  61. package/dist/market_adapter/inputs/fetch_book_data.d.ts.map +1 -0
  62. package/dist/market_adapter/inputs/fetch_book_data.js +138 -0
  63. package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -0
  64. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
  65. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +16 -2
  66. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  67. package/dist/market_adapter/inputs/fetch_lp_data.js +109 -281
  68. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  69. package/dist/market_adapter/inputs/kibana_feed_source.d.ts +87 -0
  70. package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -0
  71. package/dist/market_adapter/inputs/kibana_feed_source.js +570 -0
  72. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -0
  73. package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
  74. package/dist/market_adapter/inputs/kibana_source.js +10 -5
  75. package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
  76. package/dist/market_adapter/inputs/window_cache.d.ts +166 -0
  77. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -0
  78. package/dist/market_adapter/inputs/window_cache.js +500 -0
  79. package/dist/market_adapter/inputs/window_cache.js.map +1 -0
  80. package/dist/market_adapter/interval_utils.d.ts +8 -1
  81. package/dist/market_adapter/interval_utils.d.ts.map +1 -1
  82. package/dist/market_adapter/interval_utils.js +14 -1
  83. package/dist/market_adapter/interval_utils.js.map +1 -1
  84. package/dist/modules/account_bots.d.ts +1 -0
  85. package/dist/modules/account_bots.d.ts.map +1 -1
  86. package/dist/modules/account_bots.js +21 -5
  87. package/dist/modules/account_bots.js.map +1 -1
  88. package/dist/modules/account_orders.d.ts +25 -4
  89. package/dist/modules/account_orders.d.ts.map +1 -1
  90. package/dist/modules/account_orders.js +67 -4
  91. package/dist/modules/account_orders.js.map +1 -1
  92. package/dist/modules/bitshares-native/chain_client.d.ts +2 -0
  93. package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
  94. package/dist/modules/bitshares-native/chain_client.js +3 -1
  95. package/dist/modules/bitshares-native/chain_client.js.map +1 -1
  96. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  97. package/dist/modules/bitshares-native/subscriptions.js +13 -3
  98. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  99. package/dist/modules/bitshares-native/transport.d.ts +2 -0
  100. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  101. package/dist/modules/bitshares-native/transport.js +122 -28
  102. package/dist/modules/bitshares-native/transport.js.map +1 -1
  103. package/dist/modules/bitshares_client.d.ts.map +1 -1
  104. package/dist/modules/bitshares_client.js +59 -8
  105. package/dist/modules/bitshares_client.js.map +1 -1
  106. package/dist/modules/bot_settings.d.ts.map +1 -1
  107. package/dist/modules/bot_settings.js +11 -0
  108. package/dist/modules/bot_settings.js.map +1 -1
  109. package/dist/modules/constants.d.ts +13 -0
  110. package/dist/modules/constants.d.ts.map +1 -1
  111. package/dist/modules/constants.js +68 -4
  112. package/dist/modules/constants.js.map +1 -1
  113. package/dist/modules/dexbot_class.d.ts +8 -0
  114. package/dist/modules/dexbot_class.d.ts.map +1 -1
  115. package/dist/modules/dexbot_class.js +57 -2
  116. package/dist/modules/dexbot_class.js.map +1 -1
  117. package/dist/modules/dexbot_cow_runtime.d.ts +26 -1
  118. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  119. package/dist/modules/dexbot_cow_runtime.js +157 -28
  120. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  121. package/dist/modules/dexbot_fill_runtime.d.ts +41 -1
  122. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  123. package/dist/modules/dexbot_fill_runtime.js +135 -5
  124. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  125. package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -12
  126. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  127. package/dist/modules/dexbot_maintenance_runtime.js +554 -46
  128. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  129. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  130. package/dist/modules/dexbot_startup_runtime.js +13 -0
  131. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  132. package/dist/modules/dexbot_state_recovery.d.ts +15 -1
  133. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  134. package/dist/modules/dexbot_state_recovery.js +36 -7
  135. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  136. package/dist/modules/fund_registry.d.ts +1 -1
  137. package/dist/modules/fund_registry.js +1 -1
  138. package/dist/modules/node_connect_policy.d.ts +52 -0
  139. package/dist/modules/node_connect_policy.d.ts.map +1 -0
  140. package/dist/modules/node_connect_policy.js +35 -0
  141. package/dist/modules/node_connect_policy.js.map +1 -0
  142. package/dist/modules/node_manager.d.ts +32 -0
  143. package/dist/modules/node_manager.d.ts.map +1 -1
  144. package/dist/modules/node_manager.js +76 -9
  145. package/dist/modules/node_manager.js.map +1 -1
  146. package/dist/modules/order/accounting.d.ts +30 -0
  147. package/dist/modules/order/accounting.d.ts.map +1 -1
  148. package/dist/modules/order/accounting.js +176 -3
  149. package/dist/modules/order/accounting.js.map +1 -1
  150. package/dist/modules/order/export.d.ts.map +1 -1
  151. package/dist/modules/order/export.js +1 -0
  152. package/dist/modules/order/export.js.map +1 -1
  153. package/dist/modules/order/grid.d.ts.map +1 -1
  154. package/dist/modules/order/grid.js +143 -15
  155. package/dist/modules/order/grid.js.map +1 -1
  156. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  157. package/dist/modules/order/grid_reconcile.js +5 -3
  158. package/dist/modules/order/grid_reconcile.js.map +1 -1
  159. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  160. package/dist/modules/order/grid_reconcile_internal.js +182 -6
  161. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  162. package/dist/modules/order/manager.d.ts +64 -2
  163. package/dist/modules/order/manager.d.ts.map +1 -1
  164. package/dist/modules/order/manager.js +231 -14
  165. package/dist/modules/order/manager.js.map +1 -1
  166. package/dist/modules/order/strategy.d.ts +3 -0
  167. package/dist/modules/order/strategy.d.ts.map +1 -1
  168. package/dist/modules/order/strategy.js +79 -7
  169. package/dist/modules/order/strategy.js.map +1 -1
  170. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  171. package/dist/modules/order/sync_engine.js +16 -1
  172. package/dist/modules/order/sync_engine.js.map +1 -1
  173. package/dist/modules/order/utils/math.d.ts +11 -1
  174. package/dist/modules/order/utils/math.d.ts.map +1 -1
  175. package/dist/modules/order/utils/math.js +24 -1
  176. package/dist/modules/order/utils/math.js.map +1 -1
  177. package/dist/modules/order/utils/order.d.ts +258 -13
  178. package/dist/modules/order/utils/order.d.ts.map +1 -1
  179. package/dist/modules/order/utils/order.js +662 -12
  180. package/dist/modules/order/utils/order.js.map +1 -1
  181. package/dist/modules/order/utils/system.d.ts +36 -0
  182. package/dist/modules/order/utils/system.d.ts.map +1 -1
  183. package/dist/modules/order/utils/system.js +102 -6
  184. package/dist/modules/order/utils/system.js.map +1 -1
  185. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  186. package/dist/modules/order/utils/validate.js +6 -2
  187. package/dist/modules/order/utils/validate.js.map +1 -1
  188. package/dist/modules/paths.d.ts +2 -0
  189. package/dist/modules/paths.d.ts.map +1 -1
  190. package/dist/modules/paths.js +1 -0
  191. package/dist/modules/paths.js.map +1 -1
  192. package/dist/modules/runtime_settings.d.ts +13 -0
  193. package/dist/modules/runtime_settings.d.ts.map +1 -1
  194. package/dist/modules/runtime_settings.js +20 -0
  195. package/dist/modules/runtime_settings.js.map +1 -1
  196. package/dist/modules/utils/errors.d.ts +16 -0
  197. package/dist/modules/utils/errors.d.ts.map +1 -1
  198. package/dist/modules/utils/errors.js +31 -0
  199. package/dist/modules/utils/errors.js.map +1 -1
  200. package/dist/pm2.js +7 -7
  201. package/dist/scripts/analyze-credit.d.ts.map +1 -1
  202. package/dist/scripts/analyze-credit.js +13 -3
  203. package/dist/scripts/analyze-credit.js.map +1 -1
  204. package/dist/scripts/analyze-orders.js +2 -2
  205. package/dist/scripts/analyze-orders.js.map +1 -1
  206. package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
  207. package/dist/scripts/diagnose-kibana-candles.js +0 -1
  208. package/dist/scripts/diagnose-kibana-candles.js.map +1 -1
  209. package/dist/scripts/test-credit-renewal.js +1 -1
  210. package/dist/scripts/test-credit-renewal.js.map +1 -1
  211. package/dist/scripts/tv.d.ts +24 -2
  212. package/dist/scripts/tv.d.ts.map +1 -1
  213. package/dist/scripts/tv.js +190 -65
  214. package/dist/scripts/tv.js.map +1 -1
  215. package/docs/BITSHARES_ONBOARDING.md +9 -2
  216. package/docs/COW_INVARIANTS.md +18 -1
  217. package/docs/DEXBOT_COMPARISON.md +3 -3
  218. package/docs/EVOLUTION.md +8 -7
  219. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
  220. package/docs/GRID_RECALCULATION.md +5 -6
  221. package/docs/GRID_RECONCILE.md +12 -2
  222. package/docs/MPA_CREDIT_USAGE.md +2 -2
  223. package/docs/README.md +5 -6
  224. package/docs/architecture.md +0 -58
  225. package/docs/developer_guide.md +2 -1
  226. package/docs/docker.md +1 -1
  227. package/market_adapter/README.md +4 -0
  228. package/modules/README.md +1 -1
  229. package/package.json +1 -1
  230. package/scripts/README.md +5 -3
  231. package/tests/README.md +2 -2
@@ -0,0 +1,87 @@
1
+ declare const OP_TYPE_FEED = 19;
2
+ declare const DEFAULT_CONFIG: any;
3
+ /**
4
+ * Backing-per-MPA from a settlement_price { base, quote } object, handling
5
+ * either base/quote orientation. Returns null when the price does not span
6
+ * exactly the mpa/backing pair.
7
+ */
8
+ declare function backingPerMpa(settlement: any, mpaAsset: any, backingAsset: any): number | null;
9
+ /**
10
+ * Kibana hit → { tsMs, price, sequence, kibanaSortKey } in backing-per-MPA,
11
+ * or null when the hit carries no usable settlement price for the pair.
12
+ */
13
+ declare function hitToFeedPrice(hit: any, { mpaAsset, backingAsset }: any): {
14
+ tsMs: number;
15
+ price: number;
16
+ sequence: number;
17
+ kibanaSortKey: any;
18
+ } | null;
19
+ declare function buildFeedDocumentQuery({ mpaAssetId, lookbackHours, timeRange, size, searchAfter }: any): any;
20
+ /**
21
+ * All feed price points for the MPA in the requested window, time-ascending.
22
+ */
23
+ declare function fetchFeedPricePoints({ mpaAsset, backingAsset, config }: any): Promise<any[]>;
24
+ /**
25
+ * Price points → OHLC candles per interval bucket.
26
+ * Pure function (no I/O) for unit testing.
27
+ *
28
+ * @param {Array} points – [{ tsMs, price }] time-ascending preferred
29
+ * @param {number} intervalSeconds – bucket size (1h default)
30
+ * @returns {Array} [[timestamp_ms, open, high, low, close, publishCount], ...]
31
+ */
32
+ declare function bucketPricesToCandles(points: any, intervalSeconds?: number): any[][];
33
+ /**
34
+ * Invert candles (1/price, high/low swapped). Pure function for pair
35
+ * orientation when the MPA is the B leg (B-per-A = MPA-per-backing).
36
+ */
37
+ declare function invertCandles(candles: any): any;
38
+ /**
39
+ * OHLC feed candles for an MPA in backing-per-MPA units
40
+ * (e.g. BTS per HONEST.USD).
41
+ *
42
+ * @param {Object} mpaAsset – { id, precision, symbol }
43
+ * @param {Object} backingAsset – { id, precision, symbol }
44
+ * @param {Object} [config] – { intervalSeconds, timeRange | lookbackHours, fillGaps, fillGapsToRequestedRange, kibanaSearch, ... }
45
+ */
46
+ declare function getFeedCandlesForMpa(mpaAsset: any, backingAsset: any, config?: any): Promise<any>;
47
+ /**
48
+ * Cross-rate price points for two MPAs sharing one backing asset.
49
+ *
50
+ * Both feeds are fetched in backing-per-MPA units, then each numerator-side
51
+ * publish is paired with the latest denominator-side publish at or before
52
+ * its timestamp (forward-fill). Ratios are therefore B-per-A directly: the
53
+ * backing leg cancels ((backing/A) / (backing/B) = B/A).
54
+ *
55
+ * Numerator publishes predating the first denominator publish are dropped
56
+ * (no reference price yet). Pure function of two point lists otherwise.
57
+ */
58
+ declare function crossPointsToRatios(numeratorPoints: any, denominatorPoints: any, intervalSeconds?: any): any[];
59
+ declare function fetchFeedCrossPoints({ mpaA, mpaB, backing, config }: any): Promise<any[]>;
60
+ /**
61
+ * Feed candles oriented for a tv pair (B-per-A units).
62
+ *
63
+ * - pair (backing, MPA): B-per-A = MPA-per-backing → inverted feed.
64
+ * - pair (MPA, backing): B-per-A = backing-per-MPA → feed as-is.
65
+ * - any other pair (MPA vs non-backing asset): throws — a single MPA feed
66
+ * cannot price that pair; use pool/orderbook candles instead, or
67
+ * getFeedCandlesForMpaCross() when both legs are MPAs.
68
+ */
69
+ declare function getFeedCandlesForPair(assetA: any, assetB: any, mpaAsset: any, backingAsset: any, config?: any): Promise<any>;
70
+ /**
71
+ * Cross-rate feed candles for an MPA/MPA pair sharing one backing asset
72
+ * (e.g. HONEST.USD/HONEST.EUR, both BTS-backed), in B-per-A units.
73
+ *
74
+ * Both feeds are queried and the quote of both is calculated per bucket via
75
+ * forward-fill (see crossPointsToRatios). Legs may be passed in either
76
+ * order; orientation follows assetA/assetB.
77
+ *
78
+ * @param {Object} assetA – tv pair leg A { id, precision, symbol }
79
+ * @param {Object} assetB – tv pair leg B { id, precision, symbol }
80
+ * @param {Object} legA – { mpa, backing } for one MPA leg
81
+ * @param {Object} legB – { mpa, backing } for the other MPA leg
82
+ */
83
+ declare function getFeedCandlesForMpaCross(assetA: any, assetB: any, legA: any, legB: any, config?: any): Promise<any>;
84
+ declare function feedOutputPath(feedKey: any, intervalSeconds: any, assetA: any, assetB: any): string;
85
+ declare function fetchFeedCandlesSequentially(feedCtx: any, assetA: any, assetB: any, opts?: any): Promise<any[]>;
86
+ export { OP_TYPE_FEED, DEFAULT_CONFIG, backingPerMpa, hitToFeedPrice, buildFeedDocumentQuery, fetchFeedPricePoints, fetchFeedCrossPoints, crossPointsToRatios, bucketPricesToCandles, invertCandles, getFeedCandlesForMpa, getFeedCandlesForPair, getFeedCandlesForMpaCross, feedOutputPath, fetchFeedCandlesSequentially, };
87
+ //# sourceMappingURL=kibana_feed_source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kibana_feed_source.d.ts","sourceRoot":"","sources":["../../../market_adapter/inputs/kibana_feed_source.ts"],"names":[],"mappings":"AA2CA,QAAA,MAAM,YAAY,KAAK,CAAC;AAkBxB,QAAA,MAAM,cAAc,EAAE,GAWrB,CAAC;AAeF;;;;GAIG;AACH,iBAAS,aAAa,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,iBAoBvE;AA6BD;;;GAGG;AACH,iBAAS,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,GAAG;;WASzC,MAAM;;;SAI7B;AAID,iBAAS,sBAAsB,CAAC,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,GAAG,OA0B/F;AAED;;GAEG;AACH,iBAAe,oBAAoB,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAW,EAAE,EAAE,GAAG,kBAqF/E;AAID;;;;;;;GAOG;AACH,iBAAS,qBAAqB,CAAC,MAAM,EAAE,GAAG,EAAE,eAAe,SAAO,WAqCjE;AAED;;;GAGG;AACH,iBAAS,aAAa,CAAC,OAAO,EAAE,GAAG,OAIlC;AAID;;;;;;;GAOG;AACH,iBAAe,oBAAoB,CAAC,QAAQ,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAE,GAAQ,gBAIrF;AAeD;;;;;;;;;;GAUG;AACH,iBAAS,mBAAmB,CAAC,eAAe,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,eAAe,GAAE,GAAO,SAiDlG;AAED,iBAAe,oBAAoB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAW,EAAE,EAAE,GAAG,kBAS5E;AAED;;;;;;;;GAQG;AACH,iBAAe,qBAAqB,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAE,GAAQ,gBAahH;AAED;;;;;;;;;;;;GAYG;AACH,iBAAe,yBAAyB,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,GAAE,GAAQ,gBA4BxG;AAuBD,iBAAS,cAAc,CAAC,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,UAInF;AAUD,iBAAe,4BAA4B,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAE,GAAQ,kBAwDjG;AAED,OAAO,EACH,YAAY,EACZ,cAAc,EACd,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,4BAA4B,GAC/B,CAAA"}
@@ -0,0 +1,570 @@
1
+ 'use strict';
2
+ /**
3
+ * KIBANA FEED SOURCE — MPA settlement-price history
4
+ *
5
+ * Fetches historical price-feed publications (op_type 19, asset_publish_feed)
6
+ * for a Market Pegged Asset and buckets them into OHLCV candles. Unlike
7
+ * kibana_source.ts (LP pool swaps, op_type 63) and kibana_market_candles.ts
8
+ * (order book fills, op_type 4), this module tracks the on-chain feed itself:
9
+ * the settlement_price the chain uses for margin calls, force settlement,
10
+ * and collateral-ratio math.
11
+ *
12
+ * Data source:
13
+ * Kibana: https://kibana.bitshares.dev
14
+ * Index: bitshares-*
15
+ * Operation type: 19 (asset_publish_feed)
16
+ *
17
+ * ES field paths for asset_publish_feed:
18
+ * operation_history.op_object.asset_id – published MPA asset ID
19
+ * operation_history.op_object.feed.settlement_price.base.amount / .asset_id
20
+ * operation_history.op_object.feed.settlement_price.quote.amount / .asset_id
21
+ *
22
+ * Output: [[timestamp_ms, open, high, low, close, feed_publish_count], ...]
23
+ * Prices are in backing-per-MPA units by default (e.g. BTS per HONEST.USD),
24
+ * matching the live feed_price_source convention. Use
25
+ * getFeedCandlesForPair() for tv-style B-per-A orientation.
26
+ *
27
+ * Notes:
28
+ * - Every publisher's feed is kept (no publisher filter); each 1h bucket
29
+ * aggregates all publishes in that hour (open = first, close = last).
30
+ * - Volume is the feed publish count per bucket, NOT traded asset volume.
31
+ */
32
+ import { fillCandleGaps } from '../candle_utils.js';
33
+ import { resolveRequestedFillRange } from '../core/kibana_candles.js';
34
+ import { kibanaSearch, DEFAULT_CONFIG as BASE_CONFIG } from '../core/kibana_client.js';
35
+ import { path } from '../../modules/path_api.js';
36
+ import { PATHS } from '../../modules/paths.js';
37
+ import { toIntervalLabel, slugPart } from '../interval_utils.js';
38
+ import { chunkPathFor, buildFetchWindowsFromRange, runCachedWindows } from './window_cache.js';
39
+ import { isTransientNetworkError, sleepMs } from '../../modules/utils/errors.js';
40
+ // ─── Constants ────────────────────────────────────────────────────────────────
41
+ const OP_TYPE_FEED = 19; // asset_publish_feed
42
+ const FEED_ASSET_ID_FIELD = 'operation_history.op_object.asset_id.keyword';
43
+ // Minimal _source projection: timestamp + ordering + the settlement price.
44
+ // Amounts sometimes serialize as strings (observed on core_exchange_rate
45
+ // siblings), so the whole settlement_price branch is fetched and coerced.
46
+ const FEED_SOURCE_FIELDS = [
47
+ 'block_data.block_time',
48
+ 'operation_id_num',
49
+ 'account_history.operation_id',
50
+ 'account_history.sequence',
51
+ 'operation_history.op_object.asset_id',
52
+ 'operation_history.op_object.feed.settlement_price',
53
+ ];
54
+ // ─── Default Config ───────────────────────────────────────────────────────────
55
+ const DEFAULT_CONFIG = {
56
+ ...BASE_CONFIG,
57
+ intervalSeconds: 3600,
58
+ lookbackHours: 500,
59
+ // Same paging discipline as the trade candle sources: the Kibana console
60
+ // proxy resets connections when a single page streams too much data.
61
+ kibanaPageSize: 2000,
62
+ kibanaPageRetries: 4,
63
+ kibanaRetryDelayMs: 1000,
64
+ // Runaway guard for search_after pagination (see kibana_candles.ts).
65
+ kibanaMaxPages: 500,
66
+ };
67
+ // ─── Price math ───────────────────────────────────────────────────────────────
68
+ /**
69
+ * Integer blockchain amount → float. Amounts may arrive as numbers or
70
+ * numeric strings; anything else yields NaN.
71
+ */
72
+ function floatAmount(raw, precision) {
73
+ const n = Number(raw);
74
+ const p = Number(precision);
75
+ if (!Number.isFinite(n) || !Number.isFinite(p) || p < 0)
76
+ return Number.NaN;
77
+ return n / Math.pow(10, p);
78
+ }
79
+ /**
80
+ * Backing-per-MPA from a settlement_price { base, quote } object, handling
81
+ * either base/quote orientation. Returns null when the price does not span
82
+ * exactly the mpa/backing pair.
83
+ */
84
+ function backingPerMpa(settlement, mpaAsset, backingAsset) {
85
+ const base = settlement?.base;
86
+ const quote = settlement?.quote;
87
+ if (!base || !quote)
88
+ return null;
89
+ const baseId = String(base.asset_id || '');
90
+ const quoteId = String(quote.asset_id || '');
91
+ const mpaId = String(mpaAsset?.id || '');
92
+ const backingId = String(backingAsset?.id || '');
93
+ if (!baseId || !quoteId || !mpaId || !backingId)
94
+ return null;
95
+ const ids = new Set([baseId, quoteId]);
96
+ if (!ids.has(mpaId) || !ids.has(backingId))
97
+ return null;
98
+ const baseFloat = floatAmount(base.amount, baseId === mpaId ? mpaAsset.precision : backingAsset.precision);
99
+ const quoteFloat = floatAmount(quote.amount, quoteId === mpaId ? mpaAsset.precision : backingAsset.precision);
100
+ if (!Number.isFinite(baseFloat) || !Number.isFinite(quoteFloat) || baseFloat <= 0 || quoteFloat <= 0)
101
+ return null;
102
+ // backing-per-MPA: divide the backing side by the MPA side.
103
+ if (baseId === backingId && quoteId === mpaId)
104
+ return baseFloat / quoteFloat;
105
+ if (baseId === mpaId && quoteId === backingId)
106
+ return quoteFloat / baseFloat;
107
+ return null;
108
+ }
109
+ function parseFeedTimestamp(source) {
110
+ const raw = source?.block_data?.block_time;
111
+ if (raw == null)
112
+ return null;
113
+ const text = String(raw);
114
+ const tsMs = Date.parse(text.endsWith('Z') ? text : `${text}Z`);
115
+ return Number.isFinite(tsMs) ? tsMs : null;
116
+ }
117
+ function hitSortKey(hit) {
118
+ const sort = Array.isArray(hit?.sort) ? hit.sort : [];
119
+ return sort.map((v) => String(v)).join('|') || String(hit?._id || '');
120
+ }
121
+ function hitSequence(source) {
122
+ const candidates = [
123
+ source?.operation_id_num,
124
+ source?.account_history?.operation_id,
125
+ source?.account_history?.sequence,
126
+ ];
127
+ for (const value of candidates) {
128
+ if (typeof value === 'number' && Number.isFinite(value))
129
+ return value;
130
+ const m = String(value || '').match(/(\d+)$/);
131
+ if (m)
132
+ return Number(m[1]);
133
+ }
134
+ return Number.NaN;
135
+ }
136
+ /**
137
+ * Kibana hit → { tsMs, price, sequence, kibanaSortKey } in backing-per-MPA,
138
+ * or null when the hit carries no usable settlement price for the pair.
139
+ */
140
+ function hitToFeedPrice(hit, { mpaAsset, backingAsset }) {
141
+ const source = hit?._source || {};
142
+ const tsMs = parseFeedTimestamp(source);
143
+ if (tsMs == null)
144
+ return null;
145
+ const settlement = source?.operation_history?.op_object?.feed?.settlement_price;
146
+ const price = backingPerMpa(settlement, mpaAsset, backingAsset);
147
+ if (!Number.isFinite(price) || price <= 0)
148
+ return null;
149
+ return {
150
+ tsMs,
151
+ price: price,
152
+ sequence: hitSequence(source),
153
+ kibanaSortKey: hitSortKey(hit),
154
+ };
155
+ }
156
+ // ─── Query ────────────────────────────────────────────────────────────────────
157
+ function buildFeedDocumentQuery({ mpaAssetId, lookbackHours, timeRange, size, searchAfter }) {
158
+ const rangeValue = timeRange
159
+ ? { gte: timeRange.gte, lte: timeRange.lte }
160
+ : { gte: `now-${lookbackHours}h`, lte: 'now' };
161
+ const query = {
162
+ size,
163
+ track_total_hits: false,
164
+ _source: FEED_SOURCE_FIELDS,
165
+ query: {
166
+ bool: {
167
+ filter: [
168
+ { term: { operation_type: OP_TYPE_FEED } },
169
+ { term: { [FEED_ASSET_ID_FIELD]: String(mpaAssetId) } },
170
+ { range: { 'block_data.block_time': rangeValue } },
171
+ ],
172
+ },
173
+ },
174
+ sort: [
175
+ { 'block_data.block_time': { order: 'asc' } },
176
+ { operation_id_num: { order: 'asc' } },
177
+ ],
178
+ };
179
+ if (Array.isArray(searchAfter))
180
+ query.search_after = searchAfter;
181
+ return query;
182
+ }
183
+ /**
184
+ * All feed price points for the MPA in the requested window, time-ascending.
185
+ */
186
+ async function fetchFeedPricePoints({ mpaAsset, backingAsset, config = {} }) {
187
+ const cfg = { ...DEFAULT_CONFIG, ...config };
188
+ const search = typeof cfg.kibanaSearch === 'function' ? cfg.kibanaSearch : kibanaSearch;
189
+ const size = Math.min(Math.max(1, Number(cfg.kibanaPageSize) || DEFAULT_CONFIG.kibanaPageSize), 10000);
190
+ const retriesRaw = Number(cfg.kibanaPageRetries);
191
+ const retries = Number.isFinite(retriesRaw) && retriesRaw >= 1 ? Math.floor(retriesRaw) : DEFAULT_CONFIG.kibanaPageRetries;
192
+ const delayRaw = Number(cfg.kibanaRetryDelayMs);
193
+ const retryDelayMs = Number.isFinite(delayRaw) && delayRaw >= 0 ? delayRaw : DEFAULT_CONFIG.kibanaRetryDelayMs;
194
+ const maxPagesRaw = Number(cfg.kibanaMaxPages);
195
+ const maxPages = Number.isFinite(maxPagesRaw) && maxPagesRaw >= 1 ? Math.floor(maxPagesRaw) : DEFAULT_CONFIG.kibanaMaxPages;
196
+ // The page loop owns the retry budget here (see kibana_candles.ts).
197
+ const pageCfg = { ...cfg, kibanaSearchRetries: 1 };
198
+ const points = [];
199
+ let searchAfter = null;
200
+ let page = 0;
201
+ let droppedTotal = 0;
202
+ while (true) {
203
+ page += 1;
204
+ if (page > maxPages) {
205
+ throw new Error(`Kibana feed pagination exceeded kibanaMaxPages=${maxPages} for ${mpaAsset?.symbol || mpaAsset?.id} ` +
206
+ `— stuck search_after cursor or range too deep for one fetch. ` +
207
+ `Narrow the timeRange or raise kibanaMaxPages.`);
208
+ }
209
+ const query = buildFeedDocumentQuery({
210
+ mpaAssetId: mpaAsset.id,
211
+ lookbackHours: cfg.lookbackHours,
212
+ timeRange: cfg.timeRange ?? null,
213
+ size,
214
+ searchAfter,
215
+ });
216
+ // A failed page is safe to retry: search_after pagination is
217
+ // stateless on the server, so replaying the same page yields the
218
+ // same documents.
219
+ let result = null;
220
+ let lastErr = null;
221
+ for (let attempt = 1; attempt <= retries; attempt++) {
222
+ try {
223
+ result = await search(pageCfg, query);
224
+ lastErr = null;
225
+ break;
226
+ }
227
+ catch (err) {
228
+ lastErr = err;
229
+ if (attempt >= retries || !isTransientNetworkError(err))
230
+ throw err;
231
+ if (retryDelayMs > 0)
232
+ await sleepMs(retryDelayMs * attempt);
233
+ }
234
+ }
235
+ if (lastErr)
236
+ throw lastErr;
237
+ const hits = result?.hits?.hits || [];
238
+ if (!Array.isArray(hits) || hits.length === 0)
239
+ break;
240
+ for (const hit of hits) {
241
+ const point = hitToFeedPrice(hit, { mpaAsset, backingAsset });
242
+ if (point)
243
+ points.push(point);
244
+ else
245
+ droppedTotal += 1;
246
+ }
247
+ if (hits.length < size)
248
+ break;
249
+ const lastSort = hits[hits.length - 1]?.sort;
250
+ if (!Array.isArray(lastSort)) {
251
+ throw new Error('Kibana document pagination requires sort values on hits');
252
+ }
253
+ searchAfter = lastSort;
254
+ }
255
+ if (droppedTotal > 0) {
256
+ console.warn(`[kibana] feed ${mpaAsset?.symbol || mpaAsset?.id}: skipped ${droppedTotal} unparseable document(s) ` +
257
+ `across ${page} page(s) — kept ${points.length} price point(s)`);
258
+ }
259
+ points.sort((a, b) => {
260
+ const tsDelta = a.tsMs - b.tsMs;
261
+ if (tsDelta !== 0)
262
+ return tsDelta;
263
+ const aSeq = Number(a.sequence);
264
+ const bSeq = Number(b.sequence);
265
+ if (Number.isFinite(aSeq) && Number.isFinite(bSeq) && aSeq !== bSeq)
266
+ return aSeq - bSeq;
267
+ return String(a.kibanaSortKey || '').localeCompare(String(b.kibanaSortKey || ''));
268
+ });
269
+ return points;
270
+ }
271
+ // ─── Bucketing ────────────────────────────────────────────────────────────────
272
+ /**
273
+ * Price points → OHLC candles per interval bucket.
274
+ * Pure function (no I/O) for unit testing.
275
+ *
276
+ * @param {Array} points – [{ tsMs, price }] time-ascending preferred
277
+ * @param {number} intervalSeconds – bucket size (1h default)
278
+ * @returns {Array} [[timestamp_ms, open, high, low, close, publishCount], ...]
279
+ */
280
+ function bucketPricesToCandles(points, intervalSeconds = 3600) {
281
+ const bucketMs = Number(intervalSeconds) * 1000;
282
+ if (!Array.isArray(points) || points.length === 0)
283
+ return [];
284
+ if (!Number.isFinite(bucketMs) || bucketMs <= 0)
285
+ return [];
286
+ const buckets = new Map();
287
+ for (const point of points || []) {
288
+ const tsMs = Number(point?.tsMs);
289
+ const price = Number(point?.price);
290
+ if (!Number.isFinite(tsMs) || !Number.isFinite(price) || price <= 0)
291
+ continue;
292
+ const bucketTs = Math.floor(tsMs / bucketMs) * bucketMs;
293
+ let bucket = buckets.get(bucketTs);
294
+ if (!bucket) {
295
+ bucket = { ts: bucketTs, open: price, high: price, low: price, close: price, count: 0, firstTs: tsMs, lastTs: tsMs };
296
+ buckets.set(bucketTs, bucket);
297
+ }
298
+ // Points may arrive unordered; open tracks the earliest timestamp,
299
+ // close the latest, while high/low span the whole bucket.
300
+ if (tsMs < bucket.firstTs) {
301
+ bucket.open = price;
302
+ bucket.firstTs = tsMs;
303
+ }
304
+ if (tsMs >= bucket.lastTs) {
305
+ bucket.close = price;
306
+ bucket.lastTs = tsMs;
307
+ }
308
+ if (price > bucket.high)
309
+ bucket.high = price;
310
+ if (price < bucket.low)
311
+ bucket.low = price;
312
+ // Cross-feed points may carry an averaged publication count for the
313
+ // bucket; ordinary feed points count one publication each.
314
+ const pointCount = Number(point?.feedPublishCount);
315
+ bucket.count += Number.isFinite(pointCount) && pointCount >= 0 ? pointCount : 1;
316
+ }
317
+ return [...buckets.entries()]
318
+ .sort((a, b) => a[0] - b[0])
319
+ .map(([, b]) => [b.ts, b.open, b.high, b.low, b.close, b.count]);
320
+ }
321
+ /**
322
+ * Invert candles (1/price, high/low swapped). Pure function for pair
323
+ * orientation when the MPA is the B leg (B-per-A = MPA-per-backing).
324
+ */
325
+ function invertCandles(candles) {
326
+ return (candles || [])
327
+ .filter((c) => Array.isArray(c) && c.slice(1, 5).every((v) => Number.isFinite(Number(v)) && Number(v) > 0))
328
+ .map((c) => [c[0], 1 / c[4], 1 / c[3], 1 / c[2], 1 / c[1], c[5]]);
329
+ }
330
+ // ─── Public API ───────────────────────────────────────────────────────────────
331
+ /**
332
+ * OHLC feed candles for an MPA in backing-per-MPA units
333
+ * (e.g. BTS per HONEST.USD).
334
+ *
335
+ * @param {Object} mpaAsset – { id, precision, symbol }
336
+ * @param {Object} backingAsset – { id, precision, symbol }
337
+ * @param {Object} [config] – { intervalSeconds, timeRange | lookbackHours, fillGaps, fillGapsToRequestedRange, kibanaSearch, ... }
338
+ */
339
+ async function getFeedCandlesForMpa(mpaAsset, backingAsset, config = {}) {
340
+ const cfg = { ...DEFAULT_CONFIG, ...config };
341
+ const points = await fetchFeedPricePoints({ mpaAsset, backingAsset, config: cfg });
342
+ return applyGapFill(bucketPricesToCandles(points, cfg.intervalSeconds), cfg);
343
+ }
344
+ function applyGapFill(consolidated, cfg) {
345
+ if (cfg.fillGaps === false) {
346
+ return consolidated;
347
+ }
348
+ if (cfg.fillGapsToRequestedRange === false) {
349
+ return fillCandleGaps(consolidated, cfg.intervalSeconds);
350
+ }
351
+ const { startTs, endTs } = resolveRequestedFillRange(cfg);
352
+ return fillCandleGaps(consolidated, cfg.intervalSeconds, startTs, endTs);
353
+ }
354
+ /**
355
+ * Cross-rate price points for two MPAs sharing one backing asset.
356
+ *
357
+ * Both feeds are fetched in backing-per-MPA units, then each numerator-side
358
+ * publish is paired with the latest denominator-side publish at or before
359
+ * its timestamp (forward-fill). Ratios are therefore B-per-A directly: the
360
+ * backing leg cancels ((backing/A) / (backing/B) = B/A).
361
+ *
362
+ * Numerator publishes predating the first denominator publish are dropped
363
+ * (no reference price yet). Pure function of two point lists otherwise.
364
+ */
365
+ function crossPointsToRatios(numeratorPoints, denominatorPoints, intervalSeconds = 0) {
366
+ const ratios = [];
367
+ let j = 0;
368
+ let lastDenominator = null;
369
+ const bucketMs = Number(intervalSeconds) * 1000;
370
+ const numeratorCounts = new Map();
371
+ const denominatorCounts = new Map();
372
+ const countedBuckets = new Set();
373
+ if (Number.isFinite(bucketMs) && bucketMs > 0) {
374
+ for (const point of numeratorPoints || []) {
375
+ const tsMs = Number(point?.tsMs);
376
+ if (Number.isFinite(tsMs)) {
377
+ const bucket = Math.floor(tsMs / bucketMs) * bucketMs;
378
+ numeratorCounts.set(bucket, (numeratorCounts.get(bucket) || 0) + 1);
379
+ }
380
+ }
381
+ for (const point of denominatorPoints || []) {
382
+ const tsMs = Number(point?.tsMs);
383
+ if (Number.isFinite(tsMs)) {
384
+ const bucket = Math.floor(tsMs / bucketMs) * bucketMs;
385
+ denominatorCounts.set(bucket, (denominatorCounts.get(bucket) || 0) + 1);
386
+ }
387
+ }
388
+ }
389
+ for (const point of numeratorPoints || []) {
390
+ const tsMs = Number(point?.tsMs);
391
+ const price = Number(point?.price);
392
+ if (!Number.isFinite(tsMs) || !Number.isFinite(price) || price <= 0)
393
+ continue;
394
+ while (j < (denominatorPoints || []).length && Number(denominatorPoints[j]?.tsMs) <= tsMs) {
395
+ lastDenominator = denominatorPoints[j];
396
+ j++;
397
+ }
398
+ const refPrice = Number(lastDenominator?.price);
399
+ if (!Number.isFinite(refPrice) || refPrice <= 0)
400
+ continue;
401
+ const ratio = { tsMs, price: price / refPrice };
402
+ if (Number.isFinite(bucketMs) && bucketMs > 0) {
403
+ const bucket = Math.floor(tsMs / bucketMs) * bucketMs;
404
+ if (!countedBuckets.has(bucket)) {
405
+ ratio.feedPublishCount = ((numeratorCounts.get(bucket) || 0) + (denominatorCounts.get(bucket) || 0)) / 2;
406
+ countedBuckets.add(bucket);
407
+ }
408
+ else {
409
+ ratio.feedPublishCount = 0;
410
+ }
411
+ }
412
+ ratios.push(ratio);
413
+ }
414
+ return ratios;
415
+ }
416
+ async function fetchFeedCrossPoints({ mpaA, mpaB, backing, config = {} }) {
417
+ if (String(mpaA?.id || '') === String(mpaB?.id || '')) {
418
+ throw new Error('Feed cross requires two distinct MPAs');
419
+ }
420
+ const [pointsA, pointsB] = await Promise.all([
421
+ fetchFeedPricePoints({ mpaAsset: mpaA, backingAsset: backing, config }),
422
+ fetchFeedPricePoints({ mpaAsset: mpaB, backingAsset: backing, config }),
423
+ ]);
424
+ return crossPointsToRatios(pointsA, pointsB, config.intervalSeconds || 3600);
425
+ }
426
+ /**
427
+ * Feed candles oriented for a tv pair (B-per-A units).
428
+ *
429
+ * - pair (backing, MPA): B-per-A = MPA-per-backing → inverted feed.
430
+ * - pair (MPA, backing): B-per-A = backing-per-MPA → feed as-is.
431
+ * - any other pair (MPA vs non-backing asset): throws — a single MPA feed
432
+ * cannot price that pair; use pool/orderbook candles instead, or
433
+ * getFeedCandlesForMpaCross() when both legs are MPAs.
434
+ */
435
+ async function getFeedCandlesForPair(assetA, assetB, mpaAsset, backingAsset, config = {}) {
436
+ const aId = String(assetA?.id || '');
437
+ const bId = String(assetB?.id || '');
438
+ const mpaId = String(mpaAsset?.id || '');
439
+ const backingId = String(backingAsset?.id || '');
440
+ const candles = await getFeedCandlesForMpa(mpaAsset, backingAsset, config);
441
+ if (aId === backingId && bId === mpaId)
442
+ return invertCandles(candles);
443
+ if (aId === mpaId && bId === backingId)
444
+ return candles;
445
+ throw new Error(`Feed candles cover ${backingAsset?.symbol || backingId}/${mpaAsset?.symbol || mpaId} only; ` +
446
+ `cannot price ${assetA?.symbol || aId}/${assetB?.symbol || bId} from the feed`);
447
+ }
448
+ /**
449
+ * Cross-rate feed candles for an MPA/MPA pair sharing one backing asset
450
+ * (e.g. HONEST.USD/HONEST.EUR, both BTS-backed), in B-per-A units.
451
+ *
452
+ * Both feeds are queried and the quote of both is calculated per bucket via
453
+ * forward-fill (see crossPointsToRatios). Legs may be passed in either
454
+ * order; orientation follows assetA/assetB.
455
+ *
456
+ * @param {Object} assetA – tv pair leg A { id, precision, symbol }
457
+ * @param {Object} assetB – tv pair leg B { id, precision, symbol }
458
+ * @param {Object} legA – { mpa, backing } for one MPA leg
459
+ * @param {Object} legB – { mpa, backing } for the other MPA leg
460
+ */
461
+ async function getFeedCandlesForMpaCross(assetA, assetB, legA, legB, config = {}) {
462
+ const aId = String(assetA?.id || '');
463
+ const bId = String(assetB?.id || '');
464
+ const mpaAId = String(legA?.mpa?.id || '');
465
+ const mpaBId = String(legB?.mpa?.id || '');
466
+ const backingAId = String(legA?.backing?.id || '');
467
+ const backingBId = String(legB?.backing?.id || '');
468
+ if (!mpaAId || !mpaBId || mpaAId === mpaBId) {
469
+ throw new Error('Feed cross requires two distinct MPA legs');
470
+ }
471
+ if (!backingAId || backingAId !== backingBId) {
472
+ throw new Error(`Feed cross requires one shared backing asset, got ${legA?.backing?.symbol || backingAId} vs ${legB?.backing?.symbol || backingBId}`);
473
+ }
474
+ // Numerator leg is the A leg so ratios come out as B-per-A directly.
475
+ const flip = aId === mpaBId && bId === mpaAId;
476
+ if (!flip && !(aId === mpaAId && bId === mpaBId)) {
477
+ throw new Error(`Feed cross covers ${legA?.mpa?.symbol || mpaAId}/${legB?.mpa?.symbol || mpaBId} only; ` +
478
+ `cannot price ${assetA?.symbol || aId}/${assetB?.symbol || bId} from the feed`);
479
+ }
480
+ const cfg = { ...DEFAULT_CONFIG, ...config };
481
+ const points = flip
482
+ ? await fetchFeedCrossPoints({ mpaA: legB.mpa, mpaB: legA.mpa, backing: legA.backing, config: cfg })
483
+ : await fetchFeedCrossPoints({ mpaA: legA.mpa, mpaB: legB.mpa, backing: legA.backing, config: cfg });
484
+ return applyGapFill(bucketPricesToCandles(points, cfg.intervalSeconds), cfg);
485
+ }
486
+ // ─── Sequential cached fetch (tv --feed) ───────────────────────────────────────
487
+ // Without this, every `dexbot tv --feed` re-queries all windows from Kibana.
488
+ // Chunk files live under MARKET_ADAPTER.FEED_DATA_DIR and reuse the shared
489
+ // bucket-cache machinery (see window_cache.js): shifted reruns reuse local
490
+ // buckets and query only what is missing.
491
+ //
492
+ // Correctness note: single-MPA pairs allow sub-range fetches (each bucket is
493
+ // built from its own publishes). MPA/MPA crosses forward-fill the denominator
494
+ // leg, which needs history before the query start — so crosses reuse exact
495
+ // ranges but always take full-window fetches otherwise (same boundary
496
+ // semantics as the uncached path: only the window-start edge is affected).
497
+ function feedCacheKey(feedCtx) {
498
+ if (feedCtx?.kind === 'cross') {
499
+ const a = feedCtx.legs[0]?.mpa?.symbol || feedCtx.legs[0]?.mpa?.id || 'legA';
500
+ const b = feedCtx.legs[1]?.mpa?.symbol || feedCtx.legs[1]?.mpa?.id || 'legB';
501
+ return `cross_${slugPart(a)}_${slugPart(b)}`;
502
+ }
503
+ return slugPart(feedCtx?.legs[0]?.mpa?.symbol || feedCtx?.legs[0]?.mpa?.id || 'feed');
504
+ }
505
+ function feedOutputPath(feedKey, intervalSeconds, assetA, assetB) {
506
+ const label = toIntervalLabel(intervalSeconds);
507
+ const folder = `${slugPart(assetA?.symbol)}_${slugPart(assetB?.symbol)}`;
508
+ return path.join(PATHS.MARKET_ADAPTER.FEED_DATA_DIR, folder, `feed_${slugPart(feedKey)}_${label}.json`);
509
+ }
510
+ function isFeedChunkMatch(meta, requestKey) {
511
+ if (meta.feed !== requestKey.feed)
512
+ return false;
513
+ if (meta.intervalSeconds !== requestKey.intervalSeconds)
514
+ return false;
515
+ if (meta.assetA?.id !== requestKey.assetA.id || meta.assetB?.id !== requestKey.assetB.id)
516
+ return false;
517
+ if (meta.assetA?.precision !== requestKey.assetA.precision || meta.assetB?.precision !== requestKey.assetB.precision)
518
+ return false;
519
+ return true;
520
+ }
521
+ async function fetchFeedCandlesSequentially(feedCtx, assetA, assetB, opts = {}) {
522
+ const intervalSeconds = Number(opts.intervalSeconds) || 3600;
523
+ const chunkMonths = Number(opts.chunkMonths) || 1;
524
+ const timeRange = opts.timeRange;
525
+ if (!timeRange?.gte || !timeRange?.lte) {
526
+ throw new Error('fetchFeedCandlesSequentially requires opts.timeRange { gte, lte }');
527
+ }
528
+ const feedKey = feedCacheKey(feedCtx);
529
+ const outPath = opts.outPath || feedOutputPath(feedKey, intervalSeconds, assetA, assetB);
530
+ const requestKey = {
531
+ feed: feedKey,
532
+ assetA: { id: assetA.id, precision: assetA.precision, symbol: assetA.symbol },
533
+ assetB: { id: assetB.id, precision: assetB.precision, symbol: assetB.symbol },
534
+ intervalSeconds,
535
+ };
536
+ const isCross = feedCtx?.kind === 'cross';
537
+ const plainWindows = buildFetchWindowsFromRange(timeRange, chunkMonths);
538
+ const windows = plainWindows.map((w, idx) => ({
539
+ index: idx + 1,
540
+ gte: w.gte,
541
+ lte: w.lte,
542
+ file: chunkPathFor(outPath, idx + 1, w),
543
+ }));
544
+ const fetchRange = isCross
545
+ ? (gte, lte) => getFeedCandlesForMpaCross(assetA, assetB, feedCtx.legs[0], feedCtx.legs[1], { intervalSeconds, timeRange: { gte, lte } })
546
+ : (gte, lte) => getFeedCandlesForPair(assetA, assetB, feedCtx.legs[0].mpa, feedCtx.legs[0].backing, { intervalSeconds, timeRange: { gte, lte } });
547
+ return runCachedWindows({
548
+ windows,
549
+ outPath,
550
+ requestKey,
551
+ isMatch: isFeedChunkMatch,
552
+ metaForWindow: (window) => ({
553
+ source: `https://kibana.bitshares.dev (bitshares-*, op_type 19, feed ${feedKey})`,
554
+ feed: feedKey,
555
+ assetA: requestKey.assetA,
556
+ assetB: requestKey.assetB,
557
+ intervalSeconds,
558
+ chunkIndex: window.index,
559
+ timeRange: { gte: window.gte, lte: window.lte },
560
+ format: '[timestamp_ms, open, high, low, close, feed_publish_count]',
561
+ }),
562
+ fetchRange,
563
+ bucketMs: intervalSeconds * 1000,
564
+ // Crosses forward-fill across the query start: sub-range fetches
565
+ // would drop leading ratios, so only exact reuse applies to them.
566
+ allowSubFetch: !isCross,
567
+ });
568
+ }
569
+ export { OP_TYPE_FEED, DEFAULT_CONFIG, backingPerMpa, hitToFeedPrice, buildFeedDocumentQuery, fetchFeedPricePoints, fetchFeedCrossPoints, crossPointsToRatios, bucketPricesToCandles, invertCandles, getFeedCandlesForMpa, getFeedCandlesForPair, getFeedCandlesForMpaCross, feedOutputPath, fetchFeedCandlesSequentially, };
570
+ //# sourceMappingURL=kibana_feed_source.js.map