@tradelayerprotocol/tradelayer 1.9.1

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 (249) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/.claude/skills/tl-algo/SKILL.md +255 -0
  3. package/.gitattributes +2 -0
  4. package/.github/workflows/publish.yaml +26 -0
  5. package/4mm.js +163 -0
  6. package/LICENSE +21 -0
  7. package/NPMSwapRefactor.zip +0 -0
  8. package/README.md +217 -0
  9. package/address.sh +26 -0
  10. package/algoAPI.js +581 -0
  11. package/analyzepsbt.js +92 -0
  12. package/apiEx.js +99 -0
  13. package/bb_hyperscalper.js +290 -0
  14. package/bbo_demo.js +111 -0
  15. package/buyer.js +622 -0
  16. package/client.js +50 -0
  17. package/createTxTest.js +26 -0
  18. package/createWallet.js +75 -0
  19. package/daytrader.js +531 -0
  20. package/decodeTest.js +69 -0
  21. package/fundingManager.js +144 -0
  22. package/index.js +4 -0
  23. package/listener.js +27 -0
  24. package/litecoreTxBuilder.js +1128 -0
  25. package/mmEx.js +356 -0
  26. package/networks.js +51 -0
  27. package/orderbook.js +200 -0
  28. package/package.json +34 -0
  29. package/perTradeQueue.js +36 -0
  30. package/projectsTLNPMTLNPM/package-lock.json +162 -0
  31. package/projectsTLNPMTLNPM/package.json +5 -0
  32. package/quick.js +32 -0
  33. package/quickFut.js +37 -0
  34. package/quickSell.js +37 -0
  35. package/relayerClient.js +117 -0
  36. package/run4mm.js +80 -0
  37. package/run_bbo_tracker.js +241 -0
  38. package/seller.js +443 -0
  39. package/session.js +45 -0
  40. package/setup-lin-ltc.sh +139 -0
  41. package/setup-lin.sh +203 -0
  42. package/setup-win-ltc.bat +108 -0
  43. package/setup-win.bat +167 -0
  44. package/spam_screamer_futures.js +222 -0
  45. package/tradelayer.js/.gitattributes +2 -0
  46. package/tradelayer.js/README.md +2 -0
  47. package/tradelayer.js/oldTests/activationTest.js +6 -0
  48. package/tradelayer.js/oldTests/base58.test.js +23 -0
  49. package/tradelayer.js/oldTests/base64Decode.test.js +16 -0
  50. package/tradelayer.js/oldTests/blocksRefactor.js +140 -0
  51. package/tradelayer.js/oldTests/checkVestBalance.js +25 -0
  52. package/tradelayer.js/oldTests/consensusHashProto.js +151 -0
  53. package/tradelayer.js/oldTests/contractOrderbook.js +243 -0
  54. package/tradelayer.js/oldTests/createPayload.js +0 -0
  55. package/tradelayer.js/oldTests/createTestnetAddr.js +43 -0
  56. package/tradelayer.js/oldTests/decode.js +205 -0
  57. package/tradelayer.js/oldTests/decodeTest.js +50 -0
  58. package/tradelayer.js/oldTests/displayTallyMap.js +19 -0
  59. package/tradelayer.js/oldTests/encodeDecode.js +340 -0
  60. package/tradelayer.js/oldTests/expressTest.js +29 -0
  61. package/tradelayer.js/oldTests/extractBlocksVanilla.js +214 -0
  62. package/tradelayer.js/oldTests/extractBlocksVanillaa.js +179 -0
  63. package/tradelayer.js/oldTests/extractPubkeyTest.js +60 -0
  64. package/tradelayer.js/oldTests/fillInputCacheProto.js +111 -0
  65. package/tradelayer.js/oldTests/getRawTxTest.js +22 -0
  66. package/tradelayer.js/oldTests/indexTest.js +26 -0
  67. package/tradelayer.js/oldTests/initTokensTest.js +32 -0
  68. package/tradelayer.js/oldTests/interfaceChild.js +129 -0
  69. package/tradelayer.js/oldTests/listenerChild.js +112 -0
  70. package/tradelayer.js/oldTests/opdecode.js +26 -0
  71. package/tradelayer.js/oldTests/options.js +79 -0
  72. package/tradelayer.js/oldTests/optxtest.js +116 -0
  73. package/tradelayer.js/oldTests/optxtest1.js +64 -0
  74. package/tradelayer.js/oldTests/oracle.test.js +32 -0
  75. package/tradelayer.js/oldTests/orderbook.test.js +36 -0
  76. package/tradelayer.js/oldTests/parsing.js +93 -0
  77. package/tradelayer.js/oldTests/payload.js +13 -0
  78. package/tradelayer.js/oldTests/persistenceUnitTest.js +23 -0
  79. package/tradelayer.js/oldTests/property.test.js +53 -0
  80. package/tradelayer.js/oldTests/propertyLevel.js +75 -0
  81. package/tradelayer.js/oldTests/propertyTest.js +32 -0
  82. package/tradelayer.js/oldTests/queryAddressTest.js +17 -0
  83. package/tradelayer.js/oldTests/salter.js +14 -0
  84. package/tradelayer.js/oldTests/tally.js +81 -0
  85. package/tradelayer.js/oldTests/tally.test.js +48 -0
  86. package/tradelayer.js/oldTests/tally2.js +124 -0
  87. package/tradelayer.js/oldTests/tally3.js +142 -0
  88. package/tradelayer.js/oldTests/tallyDiag.js +38 -0
  89. package/tradelayer.js/oldTests/testGetRaw.js +40 -0
  90. package/tradelayer.js/oldTests/testHexConvert.js +47 -0
  91. package/tradelayer.js/oldTests/testNewEncoding.js +96 -0
  92. package/tradelayer.js/oldTests/testNewEncoding2.js +113 -0
  93. package/tradelayer.js/oldTests/testNewEncoding3 +112 -0
  94. package/tradelayer.js/oldTests/testNewEncoding3.js +168 -0
  95. package/tradelayer.js/oldTests/testOPReturn.js +102 -0
  96. package/tradelayer.js/oldTests/testPayload.js +23 -0
  97. package/tradelayer.js/oldTests/testRaw.js +50 -0
  98. package/tradelayer.js/oldTests/testSendTooMuch.js +20 -0
  99. package/tradelayer.js/oldTests/testTxBuild +28 -0
  100. package/tradelayer.js/oldTests/testTxBuild.js +42 -0
  101. package/tradelayer.js/oldTests/tokenOrderbook.js +243 -0
  102. package/tradelayer.js/oldTests/txUtilsA.js +515 -0
  103. package/tradelayer.js/oldTests/validityUnitTest.js +53 -0
  104. package/tradelayer.js/oldTests/vaults.js +72 -0
  105. package/tradelayer.js/oldTests/volumeIndex.js +117 -0
  106. package/tradelayer.js/oldTests/volumeIndex2.js +88 -0
  107. package/tradelayer.js/output_base64.txt +1 -0
  108. package/tradelayer.js/package-lock.json +9967 -0
  109. package/tradelayer.js/package.json +61 -0
  110. package/tradelayer.js/server/index.js +88 -0
  111. package/tradelayer.js/server/litecoind.exe +0 -0
  112. package/tradelayer.js/src/activation.js +303 -0
  113. package/tradelayer.js/src/adjuster.js +77 -0
  114. package/tradelayer.js/src/amm.js +400 -0
  115. package/tradelayer.js/src/base256.js +55 -0
  116. package/tradelayer.js/src/base94.js +79 -0
  117. package/tradelayer.js/src/channels.js +1163 -0
  118. package/tradelayer.js/src/clearing.js +3109 -0
  119. package/tradelayer.js/src/clearlist.js +364 -0
  120. package/tradelayer.js/src/client.js +295 -0
  121. package/tradelayer.js/src/consensus.js +613 -0
  122. package/tradelayer.js/src/contractRegistry.js +964 -0
  123. package/tradelayer.js/src/db.js +89 -0
  124. package/tradelayer.js/src/init.js +24 -0
  125. package/tradelayer.js/src/insurance.js +347 -0
  126. package/tradelayer.js/src/interface.js +218 -0
  127. package/tradelayer.js/src/interfaceExpress.js +178 -0
  128. package/tradelayer.js/src/iou.js +509 -0
  129. package/tradelayer.js/src/listener.js +226 -0
  130. package/tradelayer.js/src/logic.js +1702 -0
  131. package/tradelayer.js/src/main.js +927 -0
  132. package/tradelayer.js/src/marginMap.js +2165 -0
  133. package/tradelayer.js/src/options.js +126 -0
  134. package/tradelayer.js/src/oracle.js +394 -0
  135. package/tradelayer.js/src/orderbook.js +4123 -0
  136. package/tradelayer.js/src/persistence.js +554 -0
  137. package/tradelayer.js/src/property.js +411 -0
  138. package/tradelayer.js/src/reOrg.js +41 -0
  139. package/tradelayer.js/src/scaling.js +145 -0
  140. package/tradelayer.js/src/tally.js +1275 -0
  141. package/tradelayer.js/src/tradeHistoryManager.js +552 -0
  142. package/tradelayer.js/src/txDecoder.js +584 -0
  143. package/tradelayer.js/src/txEncoder.js +610 -0
  144. package/tradelayer.js/src/txIndex.js +502 -0
  145. package/tradelayer.js/src/txUtils.js +1392 -0
  146. package/tradelayer.js/src/types.js +429 -0
  147. package/tradelayer.js/src/validity.js +3077 -0
  148. package/tradelayer.js/src/vaults.js +430 -0
  149. package/tradelayer.js/src/vesting.js +491 -0
  150. package/tradelayer.js/src/volumeIndex.js +618 -0
  151. package/tradelayer.js/src/walletInterface.js +220 -0
  152. package/tradelayer.js/src/walletListener.js +665 -0
  153. package/tradelayer.js/tests/256decode.js +82 -0
  154. package/tradelayer.js/tests/UTXOracle.js +205 -0
  155. package/tradelayer.js/tests/base94test.js +23 -0
  156. package/tradelayer.js/tests/cancelTxTest.js +62 -0
  157. package/tradelayer.js/tests/contractInterfaceTest.js +48 -0
  158. package/tradelayer.js/tests/decimalTest.js +65 -0
  159. package/tradelayer.js/tests/decoderTest.js +100 -0
  160. package/tradelayer.js/tests/deltaCount.js +47 -0
  161. package/tradelayer.js/tests/deltaCount2.js +60 -0
  162. package/tradelayer.js/tests/interfaceTest.js +37 -0
  163. package/tradelayer.js/tests/mainTest.js +53 -0
  164. package/tradelayer.js/tests/makeActivationTest.js +24 -0
  165. package/tradelayer.js/tests/maxHeightTest.js +49 -0
  166. package/tradelayer.js/tests/reverseHash.js +72 -0
  167. package/tradelayer.js/tests/sensitiveConsoleOutput.txt +267 -0
  168. package/tradelayer.js/tests/tallyTest.js +40 -0
  169. package/tradelayer.js/tests/testBuybacks.js +46 -0
  170. package/tradelayer.js/tests/testCodeHash.js +49 -0
  171. package/tradelayer.js/tests/testConsensusHash.js +91 -0
  172. package/tradelayer.js/tests/testDecode.js +30 -0
  173. package/tradelayer.js/tests/testEncodingLengths.js +129 -0
  174. package/tradelayer.js/tests/testGetTx +32 -0
  175. package/tradelayer.js/tests/testGetTx.js +32 -0
  176. package/tradelayer.js/tests/testHexHash.js +32 -0
  177. package/tradelayer.js/tests/testIndexHash.js +35 -0
  178. package/tradelayer.js/tests/testInitContracts.js +38 -0
  179. package/tradelayer.js/tests/testMaxConsensus.js +12 -0
  180. package/tradelayer.js/tests/testMaxSynth.js +44 -0
  181. package/tradelayer.js/tests/testMint.js +21 -0
  182. package/tradelayer.js/tests/testNetwork.js +33 -0
  183. package/tradelayer.js/tests/testOrderbookLoad.js +62 -0
  184. package/tradelayer.js/tests/testRebates.js +32 -0
  185. package/tradelayer.js/tests/testRedeem.js +22 -0
  186. package/tradelayer.js/tests/testTokenTrade.js +39 -0
  187. package/tradelayer.js/tests/testTxBuild.js +42 -0
  188. package/tradelayer.js/tests/testUTXOTrade.js +27 -0
  189. package/tradelayer.js/tests/tokenTradeHistory.js +27 -0
  190. package/tradelayer.js/tests/tradeFutures.js +40 -0
  191. package/tradelayer.js/tests/tradeHistoryExample.js +35 -0
  192. package/tradelayer.js/tests/tradeHistoryLoad.js +15 -0
  193. package/tradelayer.js/tests/txScanTest.js +134 -0
  194. package/tradelayer.js/tests/validateTest.js +136 -0
  195. package/tradelayer.js/tests/vestingTest.js +37 -0
  196. package/tradelayer.js/utils/activateMainnet.js +59 -0
  197. package/tradelayer.js/utils/activateMainnetDoge.js +63 -0
  198. package/tradelayer.js/utils/autocompactdb.js +23 -0
  199. package/tradelayer.js/utils/base64toHex.js +32 -0
  200. package/tradelayer.js/utils/broadcastDoge.js +38 -0
  201. package/tradelayer.js/utils/calcRedeem.js +19 -0
  202. package/tradelayer.js/utils/checkNetwork.js +27 -0
  203. package/tradelayer.js/utils/createAddress.js +48 -0
  204. package/tradelayer.js/utils/createAttestation.js +133 -0
  205. package/tradelayer.js/utils/createContract.js +118 -0
  206. package/tradelayer.js/utils/createOracle.js +94 -0
  207. package/tradelayer.js/utils/createwallet.js +20 -0
  208. package/tradelayer.js/utils/crossFuturesTrades.js +57 -0
  209. package/tradelayer.js/utils/crossTokenTrades.js +62 -0
  210. package/tradelayer.js/utils/dumpPriv.js +29 -0
  211. package/tradelayer.js/utils/generateChannel.js +34 -0
  212. package/tradelayer.js/utils/getInfo.js +21 -0
  213. package/tradelayer.js/utils/hardWipe.js +20 -0
  214. package/tradelayer.js/utils/hexTo64.js +16 -0
  215. package/tradelayer.js/utils/importAddress.js +28 -0
  216. package/tradelayer.js/utils/importpriv.js +20 -0
  217. package/tradelayer.js/utils/issueOracleContract.js +67 -0
  218. package/tradelayer.js/utils/issueTokens.js +41 -0
  219. package/tradelayer.js/utils/listunspent.js +66 -0
  220. package/tradelayer.js/utils/litecoinClient.js +30 -0
  221. package/tradelayer.js/utils/loadwallet.js +20 -0
  222. package/tradelayer.js/utils/publishOracle.js +113 -0
  223. package/tradelayer.js/utils/sendActivation.js +21 -0
  224. package/tradelayer.js/utils/sendChannelContractTrade.js +34 -0
  225. package/tradelayer.js/utils/sendChannelTokenTrade.js +34 -0
  226. package/tradelayer.js/utils/sendCommit.js +24 -0
  227. package/tradelayer.js/utils/sendDoge.js +62 -0
  228. package/tradelayer.js/utils/sendDogeMain.js +67 -0
  229. package/tradelayer.js/utils/sendDogeTx.js +46 -0
  230. package/tradelayer.js/utils/sendLTC.js +63 -0
  231. package/tradelayer.js/utils/sendMainnet.js +62 -0
  232. package/tradelayer.js/utils/sendTransfer.js +19 -0
  233. package/tradelayer.js/utils/sendVestTest.js +88 -0
  234. package/tradelayer.js/utils/sendWithdrawal.js +26 -0
  235. package/tradelayer.js/utils/simpleStart.js +8 -0
  236. package/tradelayer.js/utils/startStop.js +27 -0
  237. package/tradelayer.js/utils/structuredTrades.js +136 -0
  238. package/tradelayer.js/utils/verifySignature.js +90 -0
  239. package/tradelayer.js/utils/verifyWitnessAndScriptPubkey.js +41 -0
  240. package/tradelayer.js/utils/walletCache.js +172 -0
  241. package/tradelayer.js/utils/walletContractInterface.js +48 -0
  242. package/tradelayer.js/utils/walletFetchTxs.js +66 -0
  243. package/tradelayer.js/utils/walletUtils.js +97 -0
  244. package/tradelayer.js/utils/wipeDB.js +55 -0
  245. package/tradelayer.js/utils/wipeDBNotTx.js +50 -0
  246. package/txEncoder.js +529 -0
  247. package/utility.js +28 -0
  248. package/verifymessage.js +38 -0
  249. package/ws-transport.js +311 -0
@@ -0,0 +1,3109 @@
1
+ const TallyMap = require('./tally.js')
2
+ const ContractRegistry = require('./contractRegistry.js');
3
+ const db = require('./db.js')
4
+ const BigNumber = require('bignumber.js');
5
+ // Access the database where oracle data is stored
6
+ const Options = require('./options.js');
7
+ const MarginMap = require('./marginMap.js')
8
+ const Insurance = require('./insurance.js')
9
+ const Orderbooks = require('./orderbook.js')
10
+ const Channels = require('./channels.js')
11
+ const PropertyManager = require('./property.js')
12
+ const VolumeIndex = require('./volumeIndex.js')
13
+ const Oracles = require('./oracle.js')
14
+ const PnlIou = require('./iou.js')
15
+ const TradeHistory = require('./tradeHistoryManager.js')
16
+
17
+ const _positionCache = new Map();
18
+
19
+ class Clearing {
20
+ // ... other methods ...
21
+ constructor() {
22
+ // Access the singleton instance of TallyMap
23
+ //this.tallyMap = TallyMap.getSingletonInstance();
24
+ this.balanceChanges = []; // Initialize an array to track balance changes
25
+ }
26
+
27
+ static blockTrades = new Map(); // Pre-clearing trades: `${contractId}:${address}` → [{delta, opened}]
28
+ static deleverageTrades = new Map(); // Deleverage events: `${contractId}:${address}` → [{matchSize, fromOld, fromNew, ...}]
29
+ static liquidationRecords = new Map(); // Liquidation records: `${contractId}:${address}` → {pool, contracts, ...}
30
+ // ---------------------------------------------------------------------------
31
+ // PRICE CACHE
32
+ // ---------------------------------------------------------------------------
33
+ // oracleId -> { price, blockHeight }
34
+ static latestOracleMarkById = new Map();
35
+ // native contractId -> { price, blockHeight }
36
+ static latestNativeMarkById = new Map();
37
+ // contractId -> { price: number, blockHeight: number }
38
+
39
+ // =========================================
40
+ // TRADE TRACKING
41
+ // =========================================
42
+ // clearing.js
43
+ static _ensureBlockTradeEntry(key) {
44
+ if (!this.blockTrades.has(key)) {
45
+ this.blockTrades.set(key, { openedSoFar: 0, trades: [] });
46
+ }
47
+ const entry = this.blockTrades.get(key);
48
+
49
+ // add pools lazily (only used by "new" path)
50
+ if (!entry.pools) {
51
+ const BigNumber = require('bignumber.js');
52
+ entry.pools = {
53
+ long: { qty: new BigNumber(0), cost: new BigNumber(0) },
54
+ short: { qty: new BigNumber(0), cost: new BigNumber(0) },
55
+ };
56
+ }
57
+
58
+ return entry;
59
+ }
60
+
61
+ /**
62
+ * recordTrade(contractId, address, opened, closed, price, sideHint?)
63
+ *
64
+ * - Legacy mode (sideHint == null): behaves exactly like old openedSoFar stack.
65
+ * - Pool mode (sideHint provided): tracks same-block opens in long/short pools and
66
+ * computes consumedFromOpened + consumedAvgPrice for same-block closes.
67
+ *
68
+ * sideHint: true/"buy" => incoming BUY leg for this address
69
+ * false/"sell" => incoming SELL leg for this address
70
+ */
71
+ static recordTrade(contractId, address, opened, closed, price, txid, isBuyer) {
72
+ const key = `${contractId}:${address}`;
73
+ const entry = this._ensureBlockTradeEntry(key);
74
+
75
+ const BigNumber = require('bignumber.js');
76
+
77
+ const px = new BigNumber(price || 0);
78
+ const openedAbs = new BigNumber(Math.abs(opened || 0));
79
+ const closedAbs = new BigNumber(Math.abs(closed || 0));
80
+
81
+ // ------------------------------------------------------------
82
+ // Signed opened quantity (CRITICAL)
83
+ // Buyer => +opened
84
+ // Seller => -opened
85
+ // ------------------------------------------------------------
86
+ const signedOpened = isBuyer
87
+ ? openedAbs
88
+ : openedAbs.negated();
89
+
90
+ // ------------------------------------------------------------
91
+ // Same-block pools (avg-cost accounting)
92
+ // ------------------------------------------------------------
93
+ const openPool = isBuyer ? entry.pools.long : entry.pools.short;
94
+ const closePool = isBuyer ? entry.pools.short : entry.pools.long;
95
+
96
+ const openedBefore = openPool.qty.toNumber();
97
+
98
+ // ------------------------------------------------------------
99
+ // Add opens to the correct side pool
100
+ // ------------------------------------------------------------
101
+ if (openedAbs.gt(0)) {
102
+ openPool.qty = openPool.qty.plus(openedAbs);
103
+ openPool.cost = openPool.cost.plus(openedAbs.multipliedBy(px));
104
+ }
105
+
106
+ // ------------------------------------------------------------
107
+ // Consume closes from opposite side pool (same-block closes)
108
+ // ------------------------------------------------------------
109
+ let consumedFromOpened = new BigNumber(0);
110
+ let consumedAvgPrice = null;
111
+
112
+ if (closedAbs.gt(0) && closePool.qty.gt(0)) {
113
+ consumedFromOpened = BigNumber.min(closedAbs, closePool.qty);
114
+
115
+ const avgEntry = closePool.cost.dividedBy(closePool.qty);
116
+ consumedAvgPrice = avgEntry.toNumber();
117
+
118
+ const consumedCost = avgEntry.multipliedBy(consumedFromOpened);
119
+ closePool.qty = closePool.qty.minus(consumedFromOpened);
120
+ closePool.cost = closePool.cost.minus(consumedCost);
121
+ }
122
+
123
+ // ------------------------------------------------------------
124
+ // Trade object (SIGNED opened)
125
+ // ------------------------------------------------------------
126
+ const tradeObj = {
127
+ opened: signedOpened.toNumber(), // ✅ SIGNED
128
+ closed: closedAbs.toNumber(),
129
+ consumedFromOpened: consumedFromOpened.toNumber(),
130
+ price: px.toNumber(),
131
+ openedBefore,
132
+ consumedAvgPrice,
133
+ txid
134
+ };
135
+
136
+ entry.trades.push(tradeObj);
137
+
138
+ // Keep openedSoFar meaningful for any legacy readers
139
+ entry.openedSoFar =
140
+ entry.pools.long.qty.plus(entry.pools.short.qty).toNumber();
141
+
142
+ return tradeObj;
143
+ }
144
+
145
+ static _normalizeTrades(entry) {
146
+ if (!entry) return [];
147
+ if (Array.isArray(entry)) return entry; // old format
148
+ if (Array.isArray(entry.trades)) return entry.trades; // new format
149
+ return [];
150
+ }
151
+
152
+ static _normalizeEntry(entry) {
153
+ if (!entry) return { openedSoFar: 0, trades: [] };
154
+ if (Array.isArray(entry)) {
155
+ // Construct a pseudo-entry for backwards compat
156
+ return { openedSoFar: 0, trades: entry };
157
+ }
158
+ return entry; // already in new format
159
+ }
160
+
161
+ static computeOpenedRemainderFromTrades(trades) {
162
+ const BigNumber = require('bignumber.js');
163
+
164
+ let openedSigned = new BigNumber(0); // signed remainder since mark
165
+ let openedCostAbs = new BigNumber(0); // cost basis of remainder: sum(abs(open) * openPrice)
166
+
167
+ for (const t of trades || []) {
168
+ const px = new BigNumber(t?.price || 0);
169
+ if (px.lte(0)) continue;
170
+
171
+ const openSigned = new BigNumber(t?.opened || 0); // signed
172
+ const closeAbs = new BigNumber(t?.closed || 0).abs(); // abs
173
+
174
+ // 1) consume closes against the remainder FIRST (only up to remainder)
175
+ if (!closeAbs.isZero() && !openedSigned.isZero()) {
176
+ const remAbs = openedSigned.abs();
177
+ const consumeAbs = BigNumber.minimum(closeAbs, remAbs);
178
+
179
+ if (consumeAbs.gt(0)) {
180
+ // remove cost at CURRENT avg open cost, NOT at close price
181
+ const avgOpenCost = remAbs.gt(0) ? openedCostAbs.div(remAbs) : new BigNumber(0);
182
+ openedCostAbs = openedCostAbs.minus(consumeAbs.times(avgOpenCost));
183
+
184
+ // shrink signed remainder toward 0
185
+ const sgn = openedSigned.isNegative() ? -1 : 1;
186
+ openedSigned = openedSigned.minus(consumeAbs.times(sgn));
187
+ }
188
+ }
189
+
190
+ // 2) add new opens (signed)
191
+ if (!openSigned.isZero()) {
192
+ openedSigned = openedSigned.plus(openSigned);
193
+ openedCostAbs = openedCostAbs.plus(openSigned.abs().times(px));
194
+ }
195
+ }
196
+
197
+ const remAbs = openedSigned.abs();
198
+ const avg = remAbs.gt(0) ? openedCostAbs.div(remAbs) : null;
199
+
200
+ return {
201
+ openedSigned: openedSigned.toNumber(),
202
+ openedAvg: avg ? avg.toNumber() : null
203
+ };
204
+ }
205
+
206
+
207
+ static getTrades(contractId, address) {
208
+ const key = `${contractId}:${address}`;
209
+ const entry = this.blockTrades.get(key);
210
+ return this._normalizeTrades(entry);
211
+ }
212
+
213
+ static countTrades(contractId, address) {
214
+ return this.getTrades(contractId, address).length;
215
+ }
216
+
217
+ static hadMultipleTrades(contractId, address) {
218
+ return this.countTrades(contractId, address) > 1;
219
+ }
220
+
221
+ static hadAnyTrade(contractId, address) {
222
+ return this.countTrades(contractId, address) > 0;
223
+ }
224
+
225
+ // =========================================
226
+ // DELEVERAGE TRACKING (RAM only, atomic)
227
+ // =========================================
228
+ static recordDeleverageTrade(contractId, address, details) {
229
+ const key = `${contractId}:${address}`;
230
+ if (!this.deleverageTrades.has(key)) {
231
+ this.deleverageTrades.set(key, []);
232
+ }
233
+ this.deleverageTrades.get(key).push(details);
234
+ }
235
+
236
+ static getDeleveragedThisBlock(contractId, address) {
237
+ const key = `${contractId}:${address}`;
238
+ const arr = this.deleverageTrades.get(key);
239
+ if (!arr) return 0;
240
+ return arr.reduce((sum, t) => sum + (t.matchSize || 0), 0);
241
+ }
242
+
243
+ static getDeleverageTradesThisBlock(contractId, address) {
244
+ return this.deleverageTrades.get(`${contractId}:${address}`) || [];
245
+ }
246
+
247
+ // =========================================
248
+ // LIQUIDATION TRACKING (RAM only)
249
+ // =========================================
250
+ static recordLiquidation(contractId, address, details) {
251
+ this.liquidationRecords.set(`${contractId}:${address}`, details);
252
+ }
253
+
254
+ static getLiquidation(contractId, address) {
255
+ return this.liquidationRecords.get(`${contractId}:${address}`);
256
+ }
257
+
258
+ // =========================================
259
+ // VINTAGE BREAKDOWN - combines trade + deleverage data
260
+ // =========================================
261
+ static getVintageBreakdown(contractId, address, currentContracts) {
262
+ const openedViaTrade = this.getOpenedBeforeThisTrade(contractId, address) || 0;
263
+ const closedViaDelev = this.getDeleveragedThisBlock(contractId, address) || 0;
264
+
265
+ const totalSize = Math.abs(currentContracts);
266
+ const newFromTrades = Math.abs(openedViaTrade);
267
+
268
+ // Account for new contracts already deleveraged
269
+ const effectiveNew = Math.max(0, newFromTrades - closedViaDelev);
270
+ const effectiveOld = Math.max(0, totalSize - effectiveNew);
271
+
272
+ return {
273
+ oldContracts: effectiveOld,
274
+ newContracts: effectiveNew,
275
+ totalContracts: totalSize,
276
+ openedViaTrade,
277
+ closedViaDelev
278
+ };
279
+ }
280
+
281
+ // =========================================
282
+ // RESET - clears all block-scoped tracking
283
+ // =========================================
284
+ static resetBlockTracking() {
285
+ this.blockTrades.clear();
286
+ this.deleverageTrades.clear();
287
+ this.liquidationRecords.clear();
288
+ }
289
+
290
+ static resetBlockTrades() {
291
+ this.resetBlockTracking();
292
+ }
293
+
294
+ static initPositionCache(contractId, blockHeight, positions) {
295
+ const key = `${contractId}:${blockHeight}`;
296
+
297
+ // Convert Map to Array if needed
298
+ let posArray;
299
+ if (positions instanceof Map) {
300
+ posArray = Array.from(positions.values());
301
+ } else if (Array.isArray(positions)) {
302
+ posArray = positions;
303
+ } else {
304
+ posArray = [];
305
+ }
306
+
307
+ // Deep clone so nobody mutates marginMap's internal structures
308
+ const cloned = JSON.parse(JSON.stringify(posArray));
309
+ _positionCache.set(key, { positions: cloned });
310
+ return key;
311
+ }
312
+
313
+ static getPositionsFromCache(ctxKey) {
314
+ const ctx = _positionCache.get(ctxKey);
315
+ if (!ctx) throw new Error(`No clearing context for ${ctxKey}`);
316
+ return ctx.positions;
317
+ }
318
+
319
+ static updatePositionInCache(ctxKey, address, patchFn) {
320
+ const ctx = _positionCache.get(ctxKey);
321
+ if (!ctx) throw new Error(`No clearing context for ${ctxKey}`);
322
+ const positions = ctx.positions;
323
+
324
+ const idx = positions.findIndex(p => p.address === address);
325
+ if (idx === -1) {
326
+ throw new Error(`Position for ${address} not found in cache`);
327
+ }
328
+
329
+ const updated = patchFn(positions[idx]);
330
+ positions[idx] = updated;
331
+ }
332
+
333
+ static addOrUpdatePositionInCache(ctxKey, address, position) {
334
+ const ctx = _positionCache.get(ctxKey);
335
+ if (!ctx) throw new Error(`No clearing context for ${ctxKey}`);
336
+ const positions = ctx.positions;
337
+
338
+ const idx = positions.findIndex(p => p.address === address);
339
+ if (idx === -1) {
340
+ // Add new position
341
+ positions.push({ ...position });
342
+ console.log(`[CACHE] Added new position for ${address}`);
343
+ } else {
344
+ // Update existing
345
+ positions[idx] = { ...position };
346
+ }
347
+ }
348
+
349
+ static flushPositionCache(ctxKey) {
350
+ const ctx = _positionCache.get(ctxKey);
351
+ if (!ctx) throw new Error(`No clearing context for ${ctxKey}`);
352
+ _positionCache.delete(ctxKey);
353
+ return ctx.positions;
354
+ }
355
+
356
+ static async recordClearingRun(blockHeight, isRealtime) {
357
+ try {
358
+ const base = await db.getDatabase('clearing');
359
+ const entry = {
360
+ _id: `run-${blockHeight}-${isRealtime ? 'rt' : 'sync'}`,
361
+ blockHeight,
362
+ isRealtime,
363
+ timestamp: Date.now(),
364
+ };
365
+ await base.insertAsync(entry);
366
+ console.log(`Clearing run recorded: block ${blockHeight} (realtime=${isRealtime})`);
367
+ } catch (error) {
368
+ console.error('Error recording clearing run:', error);
369
+ //throw error;
370
+ }
371
+ }
372
+
373
+
374
+ static async clearingFunction(blockHeight,realtime) {
375
+ //console.log(`Starting clearing operations for block ${blockHeight}`);
376
+
377
+ //Clearing.recordClearingRun(blockHeight,realtime)
378
+ // 1. Fee Cache Buy
379
+ //await Clearing.feeCacheBuy(blockHeight);
380
+
381
+ // 2. Set channels as closed if needed
382
+ await Channels.removeEmptyChannels(blockHeight);
383
+
384
+ // 3. Ensure correct margins, init margin and liq prices for new conditions
385
+ //await Clearing.updateAllPositions(blockHeight)
386
+ // 4. Funding Settlement
387
+ await Clearing.applyFundingRates(blockHeight)
388
+ // 5. Settle trades at block level
389
+ await Clearing.makeSettlement(blockHeight);
390
+ // Ensure Net Contracts = 0
391
+ const ContractRegistry = require('./contractRegistry.js')
392
+ if(ContractRegistry.modFlag){
393
+ const netContracts = await Clearing.verifyNetContracts();
394
+ if (netContracts !== 0) {
395
+ throw new Error(`❌ Clearing failed on block ${blockHeight}: Net contracts imbalance detected: ${netContracts}`);
396
+ }
397
+ ContractRegistry.setModFlag(false) //reset the flag to be set true next time there's a marginMap delta
398
+ }
399
+
400
+ const TallyMap = require('./tally.js')
401
+ if(TallyMap.modFlag){
402
+ await Clearing.getTotalTokenBalances(blockHeight)
403
+ TallyMap.setModFlag(false) //reset the flag to be set true next time there's a marginMap delta
404
+ }
405
+
406
+ //console.log("✅ Net contracts check passed: System is balanced.");
407
+
408
+ //console.log(`Clearing operations completed for block ${blockHeight}`);
409
+ return
410
+ }
411
+
412
+ static async verifyNetContracts() {
413
+ const ContractRegistry = require('./contractRegistry.js')
414
+ const allContracts = await ContractRegistry.getAllContracts();
415
+ let netContracts = new BigNumber(0);
416
+
417
+ for (const contract of allContracts) {
418
+ const marginMap = await MarginMap.loadMarginMap(contract.id);
419
+ const positions = await marginMap.getAllPositions();
420
+
421
+ for (const pos of positions) {
422
+ netContracts = netContracts.plus(pos.contracts);
423
+ }
424
+ }
425
+ console.log('net contracts '+netContracts.toNumber())
426
+
427
+ return netContracts.toNumber();
428
+ }
429
+
430
+ static async getTotalTokenBalances(block) {
431
+ const TallyMap = require('./tally.js');
432
+ const InsuranceFund = require('./insurance.js');
433
+ const PropertyList = require('./property.js');
434
+ const Vaults = require('./vaults.js')
435
+ // Load property list
436
+ const propertyIndex = await PropertyList.getPropertyIndex();
437
+ //console.log('📌 Parsed property index:', propertyIndex);
438
+
439
+ for (const propertyData of propertyIndex) {
440
+ const propertyId = propertyData.id;
441
+ let propertyTotal = new BigNumber(0);
442
+
443
+ // ✅ 1️⃣ Fetch total balance from TallyMap
444
+ const tallyTotal = await TallyMap.getTotalForProperty(propertyId);
445
+ console.log(`📌 Tally total for ${propertyId}: ${tallyTotal}`);
446
+ propertyTotal = propertyTotal.plus(tallyTotal);
447
+
448
+ // ✅ 2️⃣ Add feeCache balance
449
+ const feeCacheBalance = await TallyMap.loadFeeCacheForProperty(propertyId);
450
+ console.log('fee cache balance '+feeCacheBalance)
451
+ propertyTotal = propertyTotal.plus(feeCacheBalance);
452
+
453
+ // ✅ 3️⃣ Properly Aggregate Insurance Fund Balances
454
+ const insuranceBalance = await InsuranceFund.getTotalBalanceForProperty(propertyId);
455
+ propertyTotal = propertyTotal.plus(insuranceBalance);
456
+ console.log(`📌 Insurance balance for ${propertyId}: ${insuranceBalance}`);
457
+ if(typeof propertyId=="number"){
458
+ const vaultTotal = await Vaults.getTotalBalanceForProperty(propertyId)
459
+ console.log('vaultTotal '+vaultTotal)
460
+ propertyTotal = propertyTotal.plus(vaultTotal)
461
+ }
462
+
463
+ // ✅ 4️⃣ Include vesting from `TLVEST` → `TL` & `TLI` → `TLIVEST`
464
+ if (propertyId === 1) {
465
+ const vestingTLVEST = await TallyMap.getTotalTally(2); // Get vesting of TLVEST
466
+ propertyTotal = propertyTotal.plus(vestingTLVEST.vesting);
467
+ console.log(`📌 Added vesting from TLVEST to TL: ${vestingTLVEST.vesting}`);
468
+ }
469
+ if (propertyId === 4) {
470
+ const vestingTLI = await TallyMap.getTotalTally(3); // Get vesting of TLI
471
+ propertyTotal = propertyTotal.plus(vestingTLI.vesting);
472
+ //console.log(`📌 Added vesting from TLI to TLIVEST: ${vestingTLI.vesting}`);
473
+ }
474
+ const propertyInIou =await PnlIou.getTotalForProperty(propertyId)
475
+ console.log('adding Iou '+propertyTotal.toNumber()+' Iou'+propertyInIou)
476
+ propertyTotal= propertyTotal.plus(propertyInIou)
477
+
478
+ // ✅ 5️⃣ Compare Against Expected Circulating Supply
479
+ let expectedCirculation = new BigNumber(propertyData.totalInCirculation);
480
+ if(typeof propertyId =='string'&& propertyId.startsWith("s-")){
481
+
482
+ expectedCirculation = await Vaults.getTotalOutstandingForProperty(propertyId);
483
+ console.log('vault diversion ')
484
+ }
485
+ console.log('total '+propertyTotal.toNumber()+' expected '+expectedCirculation.toNumber())
486
+ if(!propertyTotal.eq(expectedCirculation)){
487
+ if(!(propertyId === 3 || propertyId === 4 || propertyData.type === 2)){
488
+ const difference = propertyTotal.minus(expectedCirculation).decimalPlaces(8).toNumber()
489
+ if(difference>0.00000001||difference<-0.00000001){
490
+ throw new Error(`❌ Supply mismatch for Property ${propertyId}, diff ${difference}: Expected ${expectedCirculation.toFixed()}, Found ${propertyTotal.toFixed()}`+' on block '+block);
491
+ }else if(difference==-0.00000001){
492
+ TallyMap.recordTallyMapDelta('system',block,propertyId,difference,0,0,0,0,0,'salvageDust','')
493
+ const fund = await InsuranceFund.getInstance(propertyId,false)
494
+ await fund.deposit(1,0.00000001,block)
495
+ }
496
+ } else {
497
+ const difference = propertyTotal.minus(expectedCirculation).decimalPlaces(8).toNumber()
498
+ console.warn(`⚠️ Property ${propertyId} supply changed, diff ${difference} (Expected: ${expectedCirculation.toFixed()}, Found: ${propertyTotal.toFixed()}), but it's allowed.`);
499
+ }
500
+ }
501
+ }
502
+
503
+ return
504
+ }
505
+
506
+ static async applyFundingRates(block) {
507
+ if (block % 24 !== 0) return; // Only run every 24 blocks (~1 hour)
508
+
509
+ //console.log(`⏳ Applying funding rates at block ${block}`);
510
+
511
+ const ContractRegistry = require('./contractRegistry.js');
512
+ const contracts = await ContractRegistry.getAllPerpContracts(); // Get all perpetual contracts
513
+
514
+ for (const contractId of contracts) {
515
+ //console.log(`📜 Processing funding for contract ${contractId}`);
516
+
517
+ // **Step 1: Calculate Funding Rate**
518
+ const fundingRate = await Clearing.calculateFundingRate(contractId, block);
519
+ if (fundingRate === 0) {
520
+ //console.log(`⚠️ Skipping contract ${contractId}, funding rate is 0`);
521
+ continue;
522
+ }
523
+
524
+ console.log(`💰 [Funding Rate] Contract=${contractId}, Rate=${fundingRate} bps`);
525
+
526
+ // **Step 2: Apply Funding to Positions**
527
+ await Clearing.applyFundingToPositions(contractId, fundingRate, block);
528
+ await Clearing.saveFundingEvent(contractId, fundingRate, block)
529
+ }
530
+ //console.log("✅ Funding rate application complete");
531
+ }
532
+
533
+ static async calculateFundingRate(contractId, blockHeight) {
534
+ try {
535
+ const ContractRegistry = require('./contractRegistry.js');
536
+ const VolumeIndex = require('./volumeIndex.js');
537
+ const contractInfo = await ContractRegistry.getContractInfo(contractId);
538
+ if (!contractInfo) {
539
+ console.warn(`⚠️ No contract found for ID ${contractId}`);
540
+ return 0;
541
+ }
542
+
543
+ let vwap;
544
+
545
+ if (contractInfo.native) {
546
+ // Native contract → Fetch VWAP from `VolumeIndex`
547
+ vwap = await VolumeIndex.getVWAP(
548
+ contractInfo.notionalPropertyId,
549
+ contractInfo.collateralPropertyId,
550
+ blockHeight,
551
+ 192 // Last 8 hours (192 blocks)
552
+ );
553
+ } else {
554
+ // Oracle-based contract → Fetch VWAP from `OracleList`
555
+ vwap = await Oracles.getTWAP(contractInfo.underlyingOracleId, blockHeight, 192);
556
+ }
557
+
558
+ if (!vwap) {
559
+ //console.warn(`⚠️ No VWAP data found for contract ${contractId} in last 8 hours.`);
560
+ return 0;
561
+ }
562
+
563
+ // Get latest index price (Oracle or VolumeIndex)
564
+ const indexPrice = await Clearing.getIndexPrice(contractId, blockHeight);
565
+ if (!indexPrice) {
566
+ //console.warn(`⚠️ No index price available for contract ${contractId}.`);
567
+ return 0;
568
+ }
569
+
570
+ // Compute basis points difference
571
+ const priceDiff = new BigNumber(indexPrice).minus(vwap);
572
+ const basisPoints = priceDiff.dividedBy(vwap).times(10000).decimalPlaces(2).toNumber(); // Convert to bps
573
+
574
+ console.log(`📊 [Funding Rate Calc] VWAP: ${vwap}, Index Price: ${indexPrice}, Diff: ${priceDiff.toFixed(2)} (${basisPoints} bps)`);
575
+
576
+ // Apply clamp function
577
+ const clampedBps = this.clampFundingRate(basisPoints);
578
+
579
+ // Compute per-hour funding rate (divided by 8)
580
+ let fundingRate = new BigNumber(clampedBps).dividedBy(8).decimalPlaces(4).toNumber();
581
+
582
+ // Cap max rate at ±100 bps per 8 hours (12.5 bps per hour)
583
+ if (Math.abs(fundingRate) > 12.5) {
584
+ fundingRate = Math.sign(fundingRate) * 12.5;
585
+ }
586
+
587
+ console.log(`📈 Final Funding Rate: ${fundingRate} bps per hour`);
588
+ return fundingRate;
589
+ } catch (error) {
590
+ console.error(`❌ Error calculating funding rate for contract ${contractId}:`, error);
591
+ return 0;
592
+ }
593
+ }
594
+
595
+
596
+ static async getIndexPrice(contractId, blockHeight) {
597
+ // Load contract info (get from memory, or from DB)
598
+ const contractInfo = await ContractRegistry.getContractInfo(contractId); // or your method
599
+
600
+ // Check for oracle-based contract
601
+ if (contractInfo.underlyingOracleId !== undefined && contractInfo.underlyingOracleId !== null && !isNaN(contractInfo.underlyingOracleId)) {
602
+ // Use the oracle price
603
+ return await Oracle.getOraclePrice(contractInfo.underlyingOracleId, blockHeight);
604
+ } else {
605
+ // Use volume index price (for most synthetic/inverse contracts)
606
+ // If your contract uses notionalPropertyId and collateralPropertyId, use those!
607
+ return await VolumeIndex.getIndexForBlock(contractId, blockHeight);
608
+ }
609
+ }
610
+
611
+ // **Clamp function for funding rate**
612
+ static clampFundingRate(basisPoints) {
613
+ if (Math.abs(basisPoints) < 5) return 0; // Ignore small deviations
614
+ return Math.sign(basisPoints) * (Math.abs(basisPoints) - 5); // Reduce deviation >5bps by 5
615
+ }
616
+
617
+
618
+ static async applyFundingToPositions(contractId, fundingRate, block) {
619
+ const margins = await MarginMap.getInstance(contractId);
620
+ const openPositions = await margins.getAllPositions(contractId);
621
+ const notionalPerContract = await ContractRegistry.getNotionalValue(contractId); // Fetch notional value
622
+
623
+ if (!openPositions.length) {
624
+ //console.log(`⚠️ No positions found for contract ${contractId}`);
625
+ return;
626
+ }
627
+
628
+ // Separate longs and shorts
629
+ let longs = openPositions.filter(pos => pos.contracts > 0);
630
+ let shorts = openPositions.filter(pos => pos.contracts < 0);
631
+
632
+ let longFunding = new BigNumber(0);
633
+ let shortFunding = new BigNumber(0);
634
+
635
+ // **Calculate total funding owed by each side**
636
+ for (let pos of openPositions) {
637
+ const contractsBN = new BigNumber(Math.abs(pos.contracts));
638
+ const fundingAmount = contractsBN.times(notionalPerContract).times(fundingRate / 10000).decimalPlaces(8);
639
+
640
+ if (fundingRate > 0 && pos.contracts > 0) {
641
+ longFunding = longFunding.plus(fundingAmount); // Longs owe shorts
642
+ } else if (fundingRate < 0 && pos.contracts < 0) {
643
+ shortFunding = shortFunding.plus(fundingAmount); // Shorts owe longs
644
+ }
645
+ }
646
+
647
+ // **Distribute funding payments**
648
+ if (fundingRate > 0) {
649
+ console.log(`💳 Longs pay shorts: ${longFunding}`);
650
+ await Clearing.processFundingPayments(longs, shorts, longFunding, contractId, block);
651
+ } else if (fundingRate < 0) {
652
+ console.log(`💳 Shorts pay longs: ${shortFunding}`);
653
+ await Clearing.processFundingPayments(shorts, longs, shortFunding, contractId, block);
654
+ }
655
+ }
656
+
657
+
658
+ static async processFundingPayments(payers, receivers, totalFunding, contractId, block) {
659
+ if (totalFunding.isZero()) return;
660
+
661
+ const collateralId = await ContractRegistry.getCollateralId(contractId);
662
+ let totalContracts = payers.reduce((sum, pos) => sum.plus(Math.abs(pos.contracts)), new BigNumber(0));
663
+
664
+ if (totalContracts.isZero()) return;
665
+
666
+ for (let pos of payers) {
667
+ let contractsBN = new BigNumber(Math.abs(pos.contracts));
668
+ let amountOwed = totalFunding.times(contractsBN.dividedBy(totalContracts)).decimalPlaces(8);
669
+
670
+ console.log(`💸 Funding Deduction: ${pos.address} pays ${amountOwed}`);
671
+
672
+ await TallyMap.updateBalance(pos.address, collateralId, -amountOwed.toNumber(), 0, 0, 0, 'fundingFee', block);
673
+ }
674
+
675
+ totalContracts = receivers.reduce((sum, pos) => sum.plus(Math.abs(pos.contracts)), new BigNumber(0));
676
+
677
+ for (let pos of receivers) {
678
+ let contractsBN = new BigNumber(Math.abs(pos.contracts));
679
+ let amountReceived = totalFunding.times(contractsBN.dividedBy(totalContracts)).decimalPlaces(8);
680
+
681
+ console.log(`💰 Funding Credit: ${pos.address} receives ${amountReceived}`);
682
+
683
+ await TallyMap.updateBalance(pos.address, collateralId, amountReceived.toNumber(), 0, 0, 0, 'fundingCredit', block);
684
+ }
685
+ }
686
+
687
+ static async getIndexPrice(contractId, blockHeight) {
688
+ try {
689
+ const ContractRegistry = require('./contractRegistry.js');
690
+ const OracleRegistry = require('./oracle.js');
691
+ const VolumeIndex = require('./volumeIndex.js');
692
+ const db = require('./db.js');
693
+
694
+ const contractInfo = await ContractRegistry.getContractInfo(contractId);
695
+ if (!contractInfo) {
696
+ console.error(`❌ Contract ${contractId} not found.`);
697
+ return null;
698
+ }
699
+
700
+ if (contractInfo.native) {
701
+ // **For native contracts, use Volume Index (DEX trade data)**
702
+ const pairKey = `${contractInfo.notionalPropertyId}-${contractInfo.collateralPropertyId}`;
703
+ const volumeIndexDB = await db.getDatabase('volumeIndex');
704
+
705
+ const volumeData = await volumeIndexDB.findAsync({ _id: pairKey });
706
+ if (!volumeData || volumeData.length === 0) {
707
+ console.warn(`⚠️ No volume data found for pair ${pairKey}.`);
708
+ return null;
709
+ }
710
+
711
+ // **Sort by blockHeight descending & get latest**
712
+ const sortedData = volumeData.sort((a, b) => b.value.blockHeight - a.value.blockHeight);
713
+ const latestEntry = sortedData.find(entry => entry.value.blockHeight <= blockHeight);
714
+
715
+ if (latestEntry) {
716
+ console.log(`📊 Latest native index price for ${pairKey}: ${latestEntry.value.price} (at block ${latestEntry.value.blockHeight})`);
717
+ return latestEntry.value.price;
718
+ }
719
+ } else {
720
+ // **For oracle contracts, get the latest oracle price**
721
+ const oracleId = contractInfo.underlyingOracleId;
722
+ const latestOracleData = await OracleRegistry.getOraclePrice(oracleId);
723
+
724
+ if (!latestOracleData || latestOracleData.blockHeight > blockHeight) {
725
+ console.warn(`⚠️ No valid oracle data found for Oracle ID ${oracleId}.`);
726
+ return null;
727
+ }
728
+
729
+ console.log(`📊 Latest oracle price for contract ${contractId}: ${latestOracleData.price} (at block ${latestOracleData.blockHeight})`);
730
+ return latestOracleData.price;
731
+ }
732
+
733
+ return null;
734
+ } catch (error) {
735
+ console.error(`❌ Error retrieving index price for contract ${contractId}:`, error.message);
736
+ return null;
737
+ }
738
+ }
739
+
740
+ // Define each of the above methods with corresponding logic based on the C++ functions provided
741
+ // ...static async feeCacheBuy(block) {
742
+
743
+ static async updateAllPositions(blockHeight, contractRegistry) {
744
+ // Fetch all valid contract IDs (adjust this function to your environment)
745
+ const ContractRegistry = require('./contractRegistry.js')
746
+ const contracts = await ContractRegistry.getAllContracts();
747
+
748
+ for (const contract of contracts) {
749
+ const contractId = contract.id; // ✅ Extract only the contract ID
750
+ //console.log(`Updating positions for contract ${contractId} at block ${blockHeight}`);
751
+
752
+ // Load the margin map for this contract.
753
+ const marginMap = await MarginMap.loadMarginMap(contractId);
754
+ // Get the current positions stored in the margin map.
755
+ const positions = await marginMap.getAllPositions();
756
+
757
+ // Get contract details used in calculations.
758
+ const contractInfo = await ContractRegistry.getContractInfo(contractId);
759
+ const collateralPropertyId = contractInfo.collateralPropertyId;
760
+ const notionalValue = contractInfo.notionalValue;
761
+ const isInverse = contractInfo.inverse;
762
+
763
+ // Loop through each position.
764
+ for (const pos of positions) {
765
+ if(blockHeight%1000){
766
+ //Clearing.reconcileReserve(pos.address,collateralPropertyId)
767
+ }
768
+ /* // 1. Recalculate bankruptcy/liquidation prices.
769
+ // Get the latest available balance and reserve from the tally.
770
+ const tally = await TallyMap.getTally(pos.address, collateralPropertyId);
771
+ const liqInfo = marginMap.calculateLiquidationPrice(
772
+ tally.available,
773
+ tally.margin,
774
+ pos.contracts,
775
+ notionalValue,
776
+ isInverse,
777
+ pos.contracts > 0, // isLong: positive means long, negative means short.
778
+ pos.avgPrice
779
+ );
780
+ pos.liquidationPrice = liqInfo.liquidationPrice;
781
+ pos.bankruptcyPrice = liqInfo.bankruptcyPrice;
782
+ console.log(`For ${pos.address}: recalculated liqPrice = ${pos.liquidationPrice}, bankruptcyPrice = ${pos.bankruptcyPrice}`);
783
+
784
+ // 2. Recalculate margin requirements.
785
+ const initialMarginPerContract = await ContractRegistry.getInitialMargin(contractId, pos.avgPrice);
786
+ const requiredMargin = new BigNumber(initialMarginPerContract)
787
+ .times(Math.abs(pos.contracts))
788
+ .toNumber();
789
+ if (pos.margin < requiredMargin) {
790
+ const marginDeficit = requiredMargin - pos.margin;
791
+ console.log(`Adjusting margin for ${pos.address}: current margin ${pos.margin} is less than required ${requiredMargin}. Deficit: ${marginDeficit}`);
792
+ // Force the margin up to the required level.
793
+ pos.margin = requiredMargin;
794
+ // Reflect this change in the tally (reserve vs. available).
795
+ await TallyMap.updateBalance(
796
+ pos.address,
797
+ collateralPropertyId,
798
+ marginDeficit, // Increase margin (or move from reserve as needed)
799
+ 0,
800
+ -marginDeficit, // Deduct from reserve (example logic)
801
+ 0,
802
+ 'marginRequirementAdjustment',
803
+ blockHeight
804
+ );
805
+ }*/
806
+
807
+ // Update the position in the margin map.
808
+ //marginMap.margins.set(pos.address, pos);
809
+ //console.log(`Final state for ${pos.address} on contract ${contractId}: contracts=${pos.contracts}, margin=${pos.margin}, liqPrice=${pos.liquidationPrice}`);
810
+ }
811
+
812
+ // Save the updated margin map for this contract.
813
+ await marginMap.saveMarginMap(blockHeight);
814
+ }
815
+ //console.log(`Finished updating positions for all contracts at block ${blockHeight}`);
816
+ }
817
+
818
+ static async reconcileReserve(address, collateralId,block) {
819
+ console.log(`🔄 Reconciling reserved balance for ${address}`);
820
+ const ContractRegistry = require("./contractRegistry.js");
821
+ const TallyMap = require("./tally.js");
822
+ const Orderbooks = require("./orderbook.js")
823
+ const tally = await TallyMap.getTally(address, collateralId);
824
+ const allContracts = await ContractRegistry.getAllContractsForCollateral(address, collateralId);
825
+
826
+ let totalReservedAcrossOrders = new BigNumber(0);
827
+
828
+ for (const contractId of allContracts) {
829
+ // Load the orderbook instance for the contract
830
+ const orderbook = await Orderbooks.getOrderbookInstance(contractId);
831
+ console.log('book for '+contractId+' '+orderbook)
832
+ if (!orderbook || !orderbook.orderBooks[contractId]) continue;
833
+ console.log('total reserved '+totalReservedAcrossOrders.toNumber())
834
+ // Add the reserve amount for this contract
835
+ totalReservedAcrossOrders = totalReservedAcrossOrders.plus(orderbook.getReserveByAddress(address,contractId));
836
+ console.log('total reserved '+totalReservedAcrossOrders.toNumber())
837
+ }
838
+ // Compare total reserved margin to tallyMap reserved balance
839
+ const excess = new BigNumber(tally.reserved).minus(totalReservedAcrossOrders);
840
+
841
+ if (excess.gt(0)) {
842
+ console.log(`📉 Returning ${excess.toFixed(8)} excess from reserved to available for ${address}`);
843
+ await TallyMap.updateBalance(address, collateralId, excess.toNumber(), -excess.toNumber(), 0, 0, "reserveReconciliation", block);
844
+ } else {
845
+ console.log(`✅ No excess reserve found for ${address}.`);
846
+ }
847
+ return excess
848
+ }
849
+
850
+ static async sourceLoss(
851
+ address,
852
+ contractId,
853
+ collateralId,
854
+ requiredLoss,
855
+ blockHeight
856
+ ) {
857
+ const Tally = require('./tally.js');
858
+ const Orderbook = require('./orderbook.js');
859
+
860
+ let remaining = new BigNumber(requiredLoss);
861
+
862
+ console.log(`🧮 BEGIN LOSS SOURCING for ${address}, need ${remaining.toFixed(8)}`);
863
+
864
+ // 1. Use available balance
865
+ const t0 = await Tally.getTally(address, collateralId);
866
+ let avail = new BigNumber(t0.available || 0);
867
+
868
+ if (avail.gt(0)) {
869
+ const useA = BigNumber.min(avail, remaining);
870
+ console.log(`➡️ Using available ${useA}`);
871
+ await Tally.updateBalance(address, collateralId, -useA, 0, 0, 0, "loss_from_available", blockHeight);
872
+ remaining = remaining.minus(useA);
873
+ }
874
+
875
+ if (remaining.lte(0)) return { remaining: 0, stage: "available" };
876
+
877
+ // 2. Use margin on THIS contract (by canceling orders and freeing reserved)
878
+ console.log(`➡️ Sweeping contract-local orders for ${contractId}`);
879
+ await Orderbook.cancelExcessOrders(address, contractId, remaining, collateralId, blockHeight);
880
+
881
+ await Clearing.reconcileReserve(address, collateralId, blockHeight);
882
+
883
+ let t1 = await Tally.getTally(address, collateralId);
884
+ let avail1 = new BigNumber(t1.available || 0);
885
+
886
+ let freedLocal = avail1.minus(avail);
887
+ if (freedLocal.gt(0)) {
888
+ const useLocal = BigNumber.min(freedLocal, remaining);
889
+ console.log(`✔ Local freed ${useLocal}, applying to loss`);
890
+ await Tally.updateBalance(address, collateralId, -useLocal, 0, 0, 0, "loss_local_reserve", blockHeight);
891
+ remaining = remaining.minus(useLocal);
892
+ avail = avail1.minus(useLocal);
893
+ }
894
+
895
+ if (remaining.lte(0)) return { remaining: 0, stage: "localReserve" };
896
+
897
+ // 3. Cross-contract reserve scavenging
898
+ console.log(`➡️ Cross-contract scavenging…`);
899
+ const x = await Clearing.sourceCrossContractReserve(
900
+ address,
901
+ collateralId,
902
+ remaining,
903
+ contractId,
904
+ blockHeight
905
+ );
906
+
907
+ remaining = x.remaining;
908
+
909
+ // 4. Reconcile after scavenging
910
+ await Clearing.reconcileReserve(address, collateralId, blockHeight);
911
+
912
+ console.log(`🏁 LOSS SOURCING END — remaining: ${remaining}`);
913
+
914
+ return {
915
+ remaining: remaining.toNumber(),
916
+ stage: remaining.gt(0) ? "residual" : "cleared"
917
+ };
918
+ }
919
+
920
+ static async updateLastExchangeBlock(blockHeight) {
921
+ console.log('Updating last exchange block in channels');
922
+
923
+ // Fetch the list of active channels
924
+ let channels = await this.getActiveChannels();
925
+
926
+ // Update the last active block for each channel
927
+ channels.forEach(channel => {
928
+ if (channel.isActive) {
929
+ channel.lastExchangeBlock = blockHeight;
930
+ }
931
+ });
932
+
933
+ // Save the updated channel information
934
+ await this.saveChannels(channels);
935
+ }
936
+
937
+ static async fetchLiquidationVolume(contractId, blockHeight) {
938
+ // Assuming you have a database method to fetch liquidation data
939
+ try {
940
+ const base = await db.getDatabase('clearing')
941
+ const liquidationData = await base.findOneAsync({ _id: `liquidation-${contractId}-${blockHeight}` });
942
+ return liquidationData ? liquidationData.volume : null; // Assuming 'volume' is the field you're interested in
943
+ } catch (error) {
944
+ if (error.name === 'NotFoundError') {
945
+ console.log(`No liquidation data found for contract ID ${contractId} at block ${blockHeight}`);
946
+ return null; // Handle case where data is not found
947
+ }
948
+ throw error; // Rethrow other types of errors
949
+ }
950
+ }
951
+
952
+ /**
953
+ * Loads clearing deltas from the clearing database for a given block height.
954
+ * @param {number} blockHeight - The block height for which to load clearing deltas.
955
+ * @returns {Promise<Array>} - A promise that resolves to an array of clearing deltas for the block.
956
+ */
957
+ static async loadClearingDeltasForBlock(blockHeight) {
958
+ try {
959
+ const clearingDeltas = [];
960
+ const query = { blockHeight: blockHeight }; // Query to match the block height
961
+
962
+ // Fetch the deltas from the database
963
+ const base = await db.getDatabase('clearing')
964
+ const results = await base.findAsync(query);
965
+ results.forEach(doc => {
966
+ clearingDeltas.push(doc.value); // Assuming each document has a 'value' field with the delta data
967
+ });
968
+
969
+ return clearingDeltas;
970
+ } catch (error) {
971
+ console.error('Error loading clearing deltas:', error);
972
+ throw error;
973
+ }
974
+ }
975
+
976
+ // ---------------------------------------------------------------------------
977
+ // isPriceUpdatedForBlockHeight (drop-in replacement)
978
+ // Returns object:
979
+ // {
980
+ // updated: boolean,
981
+ // lastPrice: number|null,
982
+ // thisPrice: number|null,
983
+ // blockHeight: number,
984
+ // contractId: number|string,
985
+ // isOracle: boolean,
986
+ // oracleId?: number|null
987
+ // }
988
+ // ---------------------------------------------------------------------------
989
+ static async isPriceUpdatedForBlockHeight(contractId, blockHeight) {
990
+ const ContractRegistry = require('./contractRegistry.js');
991
+ const base = await db.getDatabase('oracleData');
992
+ const volumeIndexDB = await db.getDatabase('volumeIndex');
993
+
994
+ try {
995
+ const isOracle = await ContractRegistry.isOracleContract(contractId);
996
+
997
+ // -------------------------
998
+ // ORACLE CONTRACT
999
+ // -------------------------
1000
+ if (isOracle) {
1001
+ const oracleId = await ContractRegistry.getOracleId(contractId);
1002
+ const cached = Clearing.latestOracleMarkById.get(oracleId);
1003
+ const lastPrice = cached ? cached.price : null;
1004
+
1005
+ // Only check THIS block for a new oracle mark
1006
+ const rows = await base.findAsync({ oracleId, blockHeight });
1007
+ const entry = Array.isArray(rows) && rows.length ? rows[0] : null;
1008
+ const thisPrice = entry?.data?.price ?? null;
1009
+
1010
+ if (thisPrice != null) {
1011
+ Clearing.latestOracleMarkById.set(oracleId, { price: thisPrice, blockHeight });
1012
+
1013
+ return {
1014
+ updated: (lastPrice == null || thisPrice !== lastPrice),
1015
+ lastPrice,
1016
+ thisPrice,
1017
+ blockHeight,
1018
+ contractId,
1019
+ isOracle: true,
1020
+ oracleId
1021
+ };
1022
+ }
1023
+
1024
+ // Prime cache once if empty (lightweight max scan, no sort)
1025
+ if (lastPrice == null) {
1026
+ const all = await base.findAsync({ oracleId });
1027
+ if (Array.isArray(all) && all.length) {
1028
+ let best = all[0];
1029
+ for (const row of all) {
1030
+ if ((row.blockHeight || 0) > (best.blockHeight || 0)) best = row;
1031
+ }
1032
+ const p = best?.data?.price ?? null;
1033
+ if (p != null) {
1034
+ Clearing.latestOracleMarkById.set(oracleId, { price: p, blockHeight: best.blockHeight });
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ return {
1040
+ updated: false,
1041
+ lastPrice: Clearing.latestOracleMarkById.get(oracleId)?.price ?? null,
1042
+ thisPrice: null,
1043
+ blockHeight,
1044
+ contractId,
1045
+ isOracle: true,
1046
+ oracleId
1047
+ };
1048
+ }
1049
+
1050
+ // -------------------------
1051
+ // NATIVE CONTRACT
1052
+ // -------------------------
1053
+ const cached = Clearing.latestNativeMarkById.get(contractId);
1054
+ const lastPrice = cached ? cached.price : null;
1055
+
1056
+ let pairKey = null;
1057
+ try {
1058
+ const info = await ContractRegistry.getContractInfo(contractId);
1059
+ if (info?.notionalPropertyId != null && info?.collateralPropertyId != null) {
1060
+ pairKey = `${info.notionalPropertyId}-${info.collateralPropertyId}`;
1061
+ }
1062
+ } catch (e) {}
1063
+
1064
+ // Try pairKey doc first, then contractId doc
1065
+ let docArr = [];
1066
+ if (pairKey) {
1067
+ docArr = await volumeIndexDB.findAsync({ _id: pairKey });
1068
+ }
1069
+ if (!Array.isArray(docArr) || docArr.length === 0) {
1070
+ docArr = await volumeIndexDB.findAsync({ _id: contractId });
1071
+ }
1072
+
1073
+ const doc = Array.isArray(docArr) && docArr.length ? docArr[0] : null;
1074
+ const docBlock = doc?.value?.blockHeight ?? doc?.blockHeight ?? null;
1075
+ const thisPrice = doc?.value?.price ?? doc?.data?.price ?? null;
1076
+
1077
+ // If there is a price entry at THIS block, update cache + return object
1078
+ if (thisPrice != null && docBlock === blockHeight) {
1079
+ Clearing.latestNativeMarkById.set(contractId, { price: thisPrice, blockHeight: docBlock });
1080
+
1081
+ return {
1082
+ updated: (lastPrice == null || thisPrice !== lastPrice),
1083
+ lastPrice,
1084
+ thisPrice,
1085
+ blockHeight,
1086
+ contractId,
1087
+ isOracle: false,
1088
+ oracleId: null
1089
+ };
1090
+ }
1091
+
1092
+ // Prime cache if empty
1093
+ if (lastPrice == null && thisPrice != null && docBlock != null) {
1094
+ Clearing.latestNativeMarkById.set(contractId, { price: thisPrice, blockHeight: docBlock });
1095
+ }
1096
+
1097
+ return {
1098
+ updated: false,
1099
+ lastPrice: Clearing.latestNativeMarkById.get(contractId)?.price ?? null,
1100
+ thisPrice: null,
1101
+ blockHeight,
1102
+ contractId,
1103
+ isOracle: false,
1104
+ oracleId: null
1105
+ };
1106
+
1107
+ } catch (error) {
1108
+ console.error(`Error checking price update for contract ID ${contractId}:`, error.message);
1109
+
1110
+ return {
1111
+ updated: false,
1112
+ lastPrice: null,
1113
+ thisPrice: null,
1114
+ blockHeight,
1115
+ contractId,
1116
+ isOracle: false,
1117
+ oracleId: null,
1118
+ error: error.message
1119
+
1120
+ };
1121
+ }
1122
+ }
1123
+
1124
+ static async settleLiqNewContractsFromDB(contractId, blockHeight, lastPrice, ctxKey, preTradePositions) {
1125
+ const BigNumber = require('bignumber.js');
1126
+ const Tally = require('./tally.js');
1127
+ const ContractRegistry = require('./contractRegistry.js');
1128
+
1129
+ const trades = await TradeHistory.getLiquidationTradesForContractAtBlock(contractId, blockHeight);
1130
+ console.log('trades in settleLiqNewContractsFromDB ' + JSON.stringify(trades));
1131
+
1132
+ const refPrice = lastPrice;
1133
+ const collateralId = await ContractRegistry.getCollateralId(contractId);
1134
+ const inverse = await ContractRegistry.isInverse(contractId);
1135
+ const notionalObj = await ContractRegistry.getNotionalValue(contractId, refPrice);
1136
+ const notional = notionalObj?.notionalPerContract ?? notionalObj ?? 1;
1137
+
1138
+ if (!trades?.length) return;
1139
+
1140
+ const Clearing = this;
1141
+ const cachedPositions = preTradePositions || Clearing.getPositionsFromCache(ctxKey);
1142
+
1143
+ const positionDeltas = new Map();
1144
+
1145
+ for (const trade of trades) {
1146
+ const entryPrice = Number(trade.price);
1147
+ if (!entryPrice || entryPrice <= 0) continue;
1148
+ const amount = Number(trade.amount);
1149
+
1150
+ // ---------- BUYER side ----------
1151
+ const buyerAddr = trade.buyerAddress;
1152
+
1153
+ let buyerContractsBefore;
1154
+ if (positionDeltas.has(buyerAddr)) {
1155
+ buyerContractsBefore = positionDeltas.get(buyerAddr);
1156
+ } else {
1157
+ const buyerCachedPos = cachedPositions.find(p => p.address === buyerAddr);
1158
+ buyerContractsBefore = buyerCachedPos?.contracts || 0;
1159
+ }
1160
+
1161
+ const buyerContractsAfter = buyerContractsBefore + amount;
1162
+ positionDeltas.set(buyerAddr, buyerContractsAfter);
1163
+
1164
+ const buyerClose = buyerContractsBefore < 0
1165
+ ? Math.min(amount, Math.abs(buyerContractsBefore))
1166
+ : 0;
1167
+ const buyerOpened = amount - buyerClose;
1168
+
1169
+ console.log(`BUYER ${buyerAddr.slice(-8)}: before=${buyerContractsBefore} after=${buyerContractsAfter} close=${buyerClose} opened=${buyerOpened}`);
1170
+
1171
+ if (buyerOpened > 0) {
1172
+ let pnl;
1173
+ if (!inverse) {
1174
+ pnl = buyerOpened * notional * (refPrice - entryPrice);
1175
+ } else {
1176
+ pnl = buyerOpened * notional * ((1 / entryPrice) - (1 / refPrice));
1177
+ }
1178
+ console.log(`BUYER ${buyerAddr.slice(-8)} pnl=${pnl}`);
1179
+ if (pnl !== 0) {
1180
+ await Tally.updateBalance(
1181
+ buyerAddr,
1182
+ collateralId,
1183
+ pnl,
1184
+ 0,
1185
+ 0,
1186
+ 0,
1187
+ 'liqNewContractTieOff',
1188
+ blockHeight
1189
+ );
1190
+ }
1191
+ }
1192
+
1193
+ // ---------- SELLER side ----------
1194
+ const sellerAddr = trade.sellerAddress;
1195
+
1196
+ let sellerContractsBefore;
1197
+ if (positionDeltas.has(sellerAddr)) {
1198
+ sellerContractsBefore = positionDeltas.get(sellerAddr);
1199
+ } else {
1200
+ const sellerCachedPos = cachedPositions.find(p => p.address === sellerAddr);
1201
+ sellerContractsBefore = sellerCachedPos?.contracts || 0;
1202
+ }
1203
+
1204
+ const sellerContractsAfter = sellerContractsBefore - amount;
1205
+ positionDeltas.set(sellerAddr, sellerContractsAfter);
1206
+
1207
+ const sellerClose = sellerContractsBefore > 0
1208
+ ? Math.min(amount, sellerContractsBefore)
1209
+ : 0;
1210
+ const sellerOpened = amount - sellerClose;
1211
+
1212
+ console.log(`SELLER ${sellerAddr.slice(-8)}: before=${sellerContractsBefore} after=${sellerContractsAfter} close=${sellerClose} opened=${sellerOpened}`);
1213
+
1214
+ if (sellerOpened > 0) {
1215
+ let pnl;
1216
+ if (!inverse) {
1217
+ pnl = -sellerOpened * notional * (refPrice - entryPrice);
1218
+ } else {
1219
+ pnl = -sellerOpened * notional * ((1 / entryPrice) - (1 / refPrice));
1220
+ }
1221
+ console.log(`SELLER ${sellerAddr.slice(-8)} pnl=${pnl}`);
1222
+ if (pnl !== 0) {
1223
+ await Tally.updateBalance(
1224
+ sellerAddr,
1225
+ collateralId,
1226
+ pnl,
1227
+ 0,
1228
+ 0,
1229
+ 0,
1230
+ 'liqNewContractTieOff',
1231
+ blockHeight
1232
+ );
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+
1238
+ static async settleNewContracts(contractId, blockHeight, priceInfo) {
1239
+ const BigNumber = require('bignumber.js');
1240
+ const Tally = require('./tally.js');
1241
+ const ContractRegistry = require('./contractRegistry.js');
1242
+ const TradeHistoryManager = require('./tradeHistoryManager.js');
1243
+
1244
+ const refPrice = priceInfo?.lastPrice ?? null;
1245
+ if (refPrice == null) return;
1246
+
1247
+ const collateralId = await ContractRegistry.getCollateralId(contractId);
1248
+ const inverse = await ContractRegistry.isInverse(contractId);
1249
+ const notionalObj = await ContractRegistry.getNotionalValue(contractId, refPrice);
1250
+ const notional = notionalObj?.notionalPerContract ?? notionalObj ?? 1;
1251
+
1252
+ // --------------------------------------------------------
1253
+ // Fetch actual trade records for this block
1254
+ // --------------------------------------------------------
1255
+ const trades = await TradeHistoryManager.getTradesForContractBetweenBlocks(contractId, blockHeight, blockHeight);
1256
+
1257
+ if (!trades || trades.length === 0) {
1258
+ console.log(`[settleNewContracts] No trades for contract ${contractId} at block ${blockHeight}`);
1259
+ return;
1260
+ }
1261
+
1262
+ console.log(`[settleNewContracts] Processing ${trades.length} trades for contract ${contractId}`);
1263
+
1264
+ // --------------------------------------------------------
1265
+ // First pass: collect opens and closes per address
1266
+ // We need to track same-block netting properly
1267
+ // --------------------------------------------------------
1268
+ // Structure: address -> {
1269
+ // longOpens: [{qty, price}], shortOpens: [{qty, price}],
1270
+ // longCloses: number, shortCloses: number
1271
+ // }
1272
+ const addressData = new Map();
1273
+
1274
+ function getOrCreate(addr) {
1275
+ if (!addressData.has(addr)) {
1276
+ addressData.set(addr, {
1277
+ longOpens: [],
1278
+ shortOpens: [],
1279
+ longCloses: 0,
1280
+ shortCloses: 0
1281
+ });
1282
+ }
1283
+ return addressData.get(addr);
1284
+ }
1285
+
1286
+ for (const trade of trades) {
1287
+ const { buyerAddress, sellerAddress, amount, price, buyerClose, sellerClose } = trade;
1288
+
1289
+ // Buyer side
1290
+ const buyerData = getOrCreate(buyerAddress);
1291
+ const buyerOpened = amount - (buyerClose || 0);
1292
+ const buyerClosed = buyerClose || 0;
1293
+
1294
+ if (buyerOpened > 0) {
1295
+ // Buyer opens LONG
1296
+ buyerData.longOpens.push({ qty: buyerOpened, price });
1297
+ }
1298
+ if (buyerClosed > 0) {
1299
+ // Buyer closing means they had SHORT before
1300
+ buyerData.shortCloses += buyerClosed;
1301
+ }
1302
+
1303
+ // Seller side
1304
+ const sellerData = getOrCreate(sellerAddress);
1305
+ const sellerOpened = amount - (sellerClose || 0);
1306
+ const sellerClosed = sellerClose || 0;
1307
+
1308
+ if (sellerOpened > 0) {
1309
+ // Seller opens SHORT
1310
+ sellerData.shortOpens.push({ qty: sellerOpened, price });
1311
+ }
1312
+ if (sellerClosed > 0) {
1313
+ // Seller closing means they had LONG before
1314
+ sellerData.longCloses += sellerClosed;
1315
+ }
1316
+ }
1317
+
1318
+ // --------------------------------------------------------
1319
+ // Second pass: calculate tie-off PnL with same-block netting
1320
+ // Opens within same block can be netted against closes
1321
+ // --------------------------------------------------------
1322
+ const pnlByAddress = new Map();
1323
+
1324
+ for (const [address, data] of addressData.entries()) {
1325
+ const { longOpens, shortOpens, longCloses, shortCloses } = data;
1326
+
1327
+ // Process LONG opens (netted against shortCloses if any same-block close of longs happened)
1328
+ // Wait - longCloses means closing longs (selling), shortCloses means closing shorts (buying)
1329
+ // If someone opens long AND closes short in same block, those don't net
1330
+ // If someone opens short AND closes that short in same block, THOSE net
1331
+
1332
+ // Actually: shortCloses = closing shorts by buying = person was short, now buying
1333
+ // longCloses = closing longs by selling = person was long, now selling
1334
+ //
1335
+ // If in same block you OPEN SHORT then CLOSE SHORT:
1336
+ // shortOpens has qty, shortCloses has qty -> they net
1337
+ // If in same block you OPEN LONG then CLOSE LONG:
1338
+ // longOpens has qty, longCloses has qty -> they net
1339
+
1340
+ // Net long opens = sum(longOpens.qty) - longCloses (can't be negative)
1341
+ let totalLongOpened = 0;
1342
+ for (const o of longOpens) totalLongOpened += o.qty;
1343
+ const netLongOpened = Math.max(0, totalLongOpened - longCloses);
1344
+
1345
+ // Net short opens = sum(shortOpens.qty) - shortCloses (can't be negative)
1346
+ let totalShortOpened = 0;
1347
+ for (const o of shortOpens) totalShortOpened += o.qty;
1348
+ const netShortOpened = Math.max(0, totalShortOpened - shortCloses);
1349
+
1350
+ console.log(`[settleNewContracts] ${address}: longOpened=${totalLongOpened} longCloses=${longCloses} -> netLong=${netLongOpened}`);
1351
+ console.log(`[settleNewContracts] ${address}: shortOpened=${totalShortOpened} shortCloses=${shortCloses} -> netShort=${netShortOpened}`);
1352
+
1353
+ let totalPnl = new BigNumber(0);
1354
+
1355
+ // Tie-off net LONG opens (FIFO: consume from earliest opens first for closes)
1356
+ if (netLongOpened > 0 && longOpens.length > 0) {
1357
+ // Skip the first `longCloses` worth of opens (they were closed same-block)
1358
+ let remaining = netLongOpened;
1359
+ let skipped = longCloses;
1360
+
1361
+ for (const o of longOpens) {
1362
+ if (skipped >= o.qty) {
1363
+ skipped -= o.qty;
1364
+ continue;
1365
+ }
1366
+ const useQty = Math.min(remaining, o.qty - skipped);
1367
+ skipped = 0;
1368
+
1369
+ if (useQty > 0) {
1370
+ let pnl;
1371
+ if (inverse) {
1372
+ const invEntry = new BigNumber(1).div(o.price);
1373
+ const invRef = new BigNumber(1).div(refPrice);
1374
+ pnl = new BigNumber(useQty).times(notional).times(invEntry.minus(invRef));
1375
+ } else {
1376
+ pnl = new BigNumber(useQty).times(notional).times(
1377
+ new BigNumber(refPrice).minus(o.price)
1378
+ );
1379
+ }
1380
+ totalPnl = totalPnl.plus(pnl);
1381
+ console.log(`[settleNewContracts] ${address} LONG ${useQty} @ ${o.price} -> pnl=${pnl.toFixed()}`);
1382
+ remaining -= useQty;
1383
+ }
1384
+ if (remaining <= 0) break;
1385
+ }
1386
+ }
1387
+
1388
+ // Tie-off net SHORT opens
1389
+ if (netShortOpened > 0 && shortOpens.length > 0) {
1390
+ let remaining = netShortOpened;
1391
+ let skipped = shortCloses;
1392
+
1393
+ for (const o of shortOpens) {
1394
+ if (skipped >= o.qty) {
1395
+ skipped -= o.qty;
1396
+ continue;
1397
+ }
1398
+ const useQty = Math.min(remaining, o.qty - skipped);
1399
+ skipped = 0;
1400
+
1401
+ if (useQty > 0) {
1402
+ let pnl;
1403
+ if (inverse) {
1404
+ const invEntry = new BigNumber(1).div(o.price);
1405
+ const invRef = new BigNumber(1).div(refPrice);
1406
+ pnl = new BigNumber(useQty).times(notional).times(invRef.minus(invEntry));
1407
+ } else {
1408
+ pnl = new BigNumber(useQty).times(notional).times(
1409
+ new BigNumber(o.price).minus(refPrice)
1410
+ );
1411
+ }
1412
+ totalPnl = totalPnl.plus(pnl);
1413
+ console.log(`[settleNewContracts] ${address} SHORT ${useQty} @ ${o.price} -> pnl=${pnl.toFixed()}`);
1414
+ remaining -= useQty;
1415
+ }
1416
+ if (remaining <= 0) break;
1417
+ }
1418
+ }
1419
+
1420
+ if (!totalPnl.isZero()) {
1421
+ pnlByAddress.set(address, totalPnl);
1422
+ }
1423
+ }
1424
+
1425
+ // --------------------------------------------------------
1426
+ // Apply PnL to each address
1427
+ // --------------------------------------------------------
1428
+ for (const [address, pnlBN] of pnlByAddress.entries()) {
1429
+ console.log(`[settleNewContracts] ${address} total PnL=${pnlBN.toFixed()}`);
1430
+
1431
+ const tally = await Tally.getTally(address, collateralId);
1432
+ const avail = new BigNumber(tally?.available ?? 0);
1433
+ const mar = new BigNumber(tally?.margin ?? 0);
1434
+
1435
+ let availCh = new BigNumber(0);
1436
+ let marCh = new BigNumber(0);
1437
+
1438
+ if (pnlBN.gt(0)) {
1439
+ availCh = pnlBN;
1440
+ } else {
1441
+ const loss = pnlBN.abs();
1442
+ if (avail.gte(loss)) {
1443
+ availCh = loss.negated();
1444
+ } else {
1445
+ const takeAvail = avail;
1446
+ const remaining = loss.minus(takeAvail);
1447
+ if (mar.gte(remaining)) {
1448
+ availCh = takeAvail.negated();
1449
+ marCh = remaining.negated();
1450
+ } else {
1451
+ availCh = takeAvail.negated();
1452
+ marCh = mar.negated();
1453
+ console.error(`[settleNewContracts] BAD DEBT: ${address} owes ${remaining.minus(mar).toFixed()}`);
1454
+ }
1455
+ }
1456
+ }
1457
+
1458
+ if (!availCh.isZero() || !marCh.isZero()) {
1459
+ await Tally.updateBalance(
1460
+ address,
1461
+ collateralId,
1462
+ availCh.toNumber(),
1463
+ 0,
1464
+ marCh.toNumber(),
1465
+ 0,
1466
+ 'newContractTieOff',
1467
+ blockHeight
1468
+ );
1469
+ }
1470
+ }
1471
+ }
1472
+ // orderbook.js
1473
+ static async pruneInstaLiqOrders(thisPrice, blockHeight,contractId) {
1474
+ const Tally = require('./tally.js');
1475
+ const ContractRegistry = require('./contractRegistry.js');
1476
+
1477
+ const inverse = await ContractRegistry.isInverse(contractId);
1478
+
1479
+ const notionalObj =
1480
+ await ContractRegistry.getNotionalValue(contractId, thisPrice);
1481
+ const notional =
1482
+ notionalObj?.notionalPerContract ?? notionalObj ?? 1;
1483
+
1484
+ // ✅ delegate after notional stuff populates
1485
+ const Orderbook = require('./orderbook.js')
1486
+ const ob = await Orderbook.getOrderbookInstance(contractId)
1487
+ return await ob._pruneInstaLiqOrdersFromFreshBook(
1488
+ thisPrice,
1489
+ blockHeight,
1490
+ contractId,
1491
+ notional,
1492
+ inverse
1493
+ );
1494
+ }
1495
+
1496
+
1497
+ static async makeSettlement(blockHeight) {
1498
+ const ContractRegistry = require('./contractRegistry.js');
1499
+ const contracts = await ContractRegistry.loadContractSeries();
1500
+ if (!contracts) return;
1501
+
1502
+ for (const contract of contracts) {
1503
+ const id = contract[1].id;
1504
+ const priceInfo = await Clearing.isPriceUpdatedForBlockHeight(id, blockHeight);
1505
+ console.log('price info '+JSON.stringify(priceInfo))
1506
+ await Clearing.pruneInstaLiqOrders(priceInfo.thisPrice, blockHeight,id)
1507
+ //await Clearing.settleNewContracts(id,blockHeight,priceInfo)
1508
+ const collateralId = await ContractRegistry.getCollateralId(id);
1509
+ await Clearing.settleIousForBlock(
1510
+ id,
1511
+ collateralId,
1512
+ blockHeight
1513
+ );
1514
+
1515
+ if (!priceInfo || !priceInfo.updated) continue;
1516
+
1517
+ const newPrice = priceInfo.thisPrice;
1518
+ console.log('new price ' + newPrice);
1519
+ console.log('Making settlement for positions at block height:', JSON.stringify(contract) + ' ' + blockHeight);
1520
+
1521
+ const inverse = await ContractRegistry.isInverse(id);
1522
+
1523
+ const notionalValue = await ContractRegistry.getNotionalValue(id, newPrice);
1524
+ console.log('notional obj ' + JSON.stringify(notionalValue));
1525
+
1526
+ let { positions, liqEvents, systemicLoss, pnlDelta } =
1527
+ await Clearing.updateMarginMaps(
1528
+ blockHeight,
1529
+ id,
1530
+ collateralId,
1531
+ inverse,
1532
+ notionalValue.notionalPerContract,
1533
+ priceInfo // ✅ pass the object
1534
+ );
1535
+
1536
+ console.log('is liq ' + JSON.stringify(liqEvents));
1537
+ console.log('length ' + liqEvents.length + ' ' + Boolean(liqEvents.length > 0));
1538
+
1539
+ await Clearing.performAdditionalSettlementTasks(
1540
+ blockHeight,
1541
+ positions,
1542
+ id,
1543
+ newPrice,
1544
+ systemicLoss,
1545
+ collateralId,
1546
+ pnlDelta
1547
+ );
1548
+ }
1549
+
1550
+ await Clearing.resetBlockTrades();
1551
+ return;
1552
+ }
1553
+
1554
+
1555
+ /**
1556
+ * Normalize all position lastMark values to match the canonical previous mark
1557
+ * from the oracle/price blob for this block.
1558
+ *
1559
+ * Ensures consistent mark-to-market accounting and prevents asymmetric PNL.
1560
+ *
1561
+ * @param {Array} positions - array of position objects from marginMap.getAllPositions()
1562
+ * @param {Number} canonicalLastMark - blob.lastPrice (true previous mark)
1563
+ * @param {Object} marginMap - reference to marginMap object (must provide savePosition)
1564
+ * @param {Number} contractId
1565
+ */
1566
+ static async normalizePositionMarks(positions, canonicalLastMark, marginMap, contractId,block){
1567
+ for (let pos of positions) {
1568
+ if (pos.lastMark !== canonicalLastMark) {
1569
+ console.log(
1570
+ `🔧 [normalize] Updating lastMark for ${pos.address}: ` +
1571
+ `${pos.lastMark} → ${canonicalLastMark}`
1572
+ );
1573
+
1574
+ pos.lastMark = canonicalLastMark;
1575
+ //marginMap.margins.set(pos.address, pos);
1576
+ }
1577
+ }
1578
+ //await marginMap.saveMarginMap(block)
1579
+ }
1580
+
1581
+ // clearing.js
1582
+ static applyTradeToOpenStats(openedByAddress, openedCostByAddress, trade) {
1583
+ const BigNumber = require('bignumber.js');
1584
+
1585
+ const amount = new BigNumber(trade?.amount || 0);
1586
+ if (amount.lte(0)) return;
1587
+
1588
+ const price = new BigNumber(trade?.price || 0);
1589
+ if (price.lte(0)) return; // can't compute avg without price
1590
+
1591
+ const buyer = trade?.buyerAddress;
1592
+ const seller = trade?.sellerAddress;
1593
+
1594
+ const buyerClose = new BigNumber(trade?.buyerClose || 0);
1595
+ const sellerClose = new BigNumber(trade?.sellerClose || 0);
1596
+
1597
+ // ✅ closes do NOT count as new exposure
1598
+ const buyerOpenedAbs = BigNumber.max(new BigNumber(0), amount.minus(buyerClose));
1599
+ const sellerOpenedAbs = BigNumber.max(new BigNumber(0), amount.minus(sellerClose));
1600
+
1601
+ // buyer opens long
1602
+ if (buyer && buyerOpenedAbs.gt(0)) {
1603
+ const prevOpen = new BigNumber(openedByAddress.get(buyer) || 0);
1604
+ openedByAddress.set(buyer, prevOpen.plus(buyerOpenedAbs).toNumber());
1605
+
1606
+ const prevCost = new BigNumber(openedCostByAddress.get(buyer) || 0);
1607
+ openedCostByAddress.set(
1608
+ buyer,
1609
+ prevCost.plus(buyerOpenedAbs.multipliedBy(price)).toNumber()
1610
+ );
1611
+ }
1612
+
1613
+ // seller opens short
1614
+ if (seller && sellerOpenedAbs.gt(0)) {
1615
+ const prevOpen = new BigNumber(openedByAddress.get(seller) || 0);
1616
+ openedByAddress.set(seller, prevOpen.minus(sellerOpenedAbs).toNumber());
1617
+
1618
+ // cost stored as ABS cost for avg calc
1619
+ const prevCost = new BigNumber(openedCostByAddress.get(seller) || 0);
1620
+ openedCostByAddress.set(
1621
+ seller,
1622
+ prevCost.plus(sellerOpenedAbs.multipliedBy(price)).toNumber()
1623
+ );
1624
+ }
1625
+ }
1626
+
1627
+ static computeOpenedAvgByAddress(openedByAddress, openedCostByAddress) {
1628
+ const BigNumber = require('bignumber.js');
1629
+ const out = new Map();
1630
+
1631
+ for (const [addr, openedSignedNum] of openedByAddress.entries()) {
1632
+ const openedSigned = new BigNumber(openedSignedNum || 0);
1633
+ const openedAbs = openedSigned.abs();
1634
+ const costAbs = new BigNumber(openedCostByAddress.get(addr) || 0);
1635
+
1636
+ if (openedAbs.gt(0) && costAbs.gt(0)) {
1637
+ out.set(addr, costAbs.div(openedAbs).toNumber());
1638
+ } else {
1639
+ out.set(addr, null);
1640
+ }
1641
+ }
1642
+
1643
+ return out;
1644
+ }
1645
+
1646
+ static consensusAddressSort(a, b) {
1647
+ if (a === b) return 0;
1648
+ return a < b ? -1 : 1;
1649
+ }
1650
+
1651
+ static async updateMarginMaps(blockHeight, contractId, collateralId, inverse, notional, priceInfo) {
1652
+ console.log(`\n================ UPDATE MARGIN MAPS ================`);
1653
+ console.log(`contract=${contractId} block=${blockHeight}`);
1654
+ console.log(`====================================================`);
1655
+
1656
+ const MarginMap = require('./marginMap.js');
1657
+ const Orderbook = require('./orderbook.js');
1658
+ const Tally = require('./tally.js');
1659
+ const BigNumber = require('bignumber.js');
1660
+
1661
+ const marginMap = await MarginMap.getInstance(contractId);
1662
+
1663
+ // ------------------------------------------------------------
1664
+ // 1) Load positions
1665
+ // ------------------------------------------------------------
1666
+ const rawPositions = await marginMap.getAllPositions(contractId);
1667
+ console.log('JSON of positions starting clearing '+JSON.stringify(rawPositions))
1668
+ //if(blockHeight==4494797){throw new Error()}
1669
+ console.log(`[LOAD] rawPositions.size=${rawPositions?.size}`);
1670
+ const ctxKey = Clearing.initPositionCache(contractId, blockHeight, rawPositions);
1671
+ console.log(`[CACHE] initPositionCache ctxKey=${ctxKey}`);
1672
+
1673
+ let positions = Clearing.getPositionsFromCache(ctxKey);
1674
+ console.log(`[CACHE] positions.length=${Array.isArray(positions) ? positions.length : 'NOT ARRAY'}`);
1675
+ console.log('positions before final '+JSON.stringify(positions))
1676
+ if (!Array.isArray(positions) || positions.length === 0) {
1677
+
1678
+ console.log('[EXIT] no positions');
1679
+ Clearing.flushPositionCache(ctxKey);
1680
+ return { positions: [], liqEvents: [], systemicLoss: new BigNumber(0), pnlDelta: new BigNumber(0) };
1681
+ }
1682
+
1683
+
1684
+ // ------------------------------------------------------------
1685
+ // 2) Resolve mark prices (use priceInfo, not blob)
1686
+ // ------------------------------------------------------------
1687
+ console.log(`[PRICE] provided priceInfo`, priceInfo);
1688
+
1689
+ const lastPrice = new BigNumber(
1690
+ priceInfo?.lastPrice ??
1691
+ 0
1692
+ );
1693
+
1694
+ let thisPrice = new BigNumber(
1695
+ priceInfo?.thisPrice ??
1696
+ 0
1697
+ );
1698
+
1699
+ console.log(`[PRICE] last=${lastPrice.toFixed()} this=${thisPrice.toFixed()}`);
1700
+
1701
+ if (!lastPrice.gt(0)) {
1702
+ console.log('[EXIT] no lastPrice');
1703
+ const finalPositions = Clearing.flushPositionCache(ctxKey);
1704
+ console.log('final positions '+JSON.stringify(finalPositions))
1705
+ await marginMap.mergePositions(finalPositions, contractId, true);
1706
+ return { positions, liqEvents: [], systemicLoss: new BigNumber(0), pnlDelta: new BigNumber(0) };
1707
+ }
1708
+
1709
+ // If we didn't get a fresh mark for "this", settle using lastPrice for this block
1710
+ if (!thisPrice.gt(0)) {
1711
+ console.log('[WARN] thisPrice null/0, setting = lastPrice');
1712
+ thisPrice = lastPrice;
1713
+ }
1714
+
1715
+ // ------------------------------------------------------------
1716
+ // 3) Setup totals + orderbook
1717
+ // ------------------------------------------------------------
1718
+ let systemicLoss = new BigNumber(0);
1719
+ let totalPos = new BigNumber(0);
1720
+ let totalNeg = new BigNumber(0);
1721
+
1722
+ const orderbook = await Orderbook.getOrderbookInstance(contractId);
1723
+ const liqQueue = [];
1724
+
1725
+ // ------------------------------------------------------------
1726
+ // 4) FIRST PASS — PNL + solvency
1727
+ // ------------------------------------------------------------
1728
+ console.log('\n--- FIRST PASS: PNL & SOLVENCY ---');
1729
+
1730
+ for (const pos of positions) {
1731
+
1732
+ if (!pos) {
1733
+ console.warn(`[SKIP] null position`);
1734
+ continue;
1735
+ }
1736
+
1737
+ if (!pos.contracts || pos.contracts === 0) {
1738
+ console.log(`[SKIP] addr=${pos.address} contracts=0`);
1739
+ continue;
1740
+ }
1741
+
1742
+ const tally = await Tally.getTally(pos.address, collateralId);
1743
+
1744
+ const pnl = Clearing.calculateClearingPNL({
1745
+ oldContracts: pos.contracts,
1746
+ previousMarkPrice: lastPrice,
1747
+ currentMarkPrice: thisPrice,
1748
+ inverse,
1749
+ notional
1750
+ });
1751
+
1752
+ console.log(
1753
+
1754
+ `[PNL] ${pos.address} c=${pos.contracts} ` +
1755
+ `pnl=${pnl.toFixed()} avail=${tally.available} mar=${tally.margin}`
1756
+ );
1757
+
1758
+ if (pnl.isZero()) {
1759
+ console.log(' -> ZERO PNL');
1760
+ continue;
1761
+ }
1762
+
1763
+ if (pnl.gt(0)) {
1764
+ console.log(' -> PROFIT (deferred)');
1765
+ pos._wasProfitable = true;
1766
+ console.log(`[DEFER PROFIT] addr=${pos.address}`);
1767
+ continue;
1768
+ }
1769
+
1770
+ const loss = pnl.abs();
1771
+ const available = new BigNumber(tally.available || 0);
1772
+ const margin = new BigNumber(tally.margin || 0);
1773
+ const maintMargin = margin.div(2);
1774
+ const coverage = available.plus(maintMargin);
1775
+
1776
+ console.log(
1777
+ ` LOSS=${loss.toFixed()} ` +
1778
+ `coverage=${coverage.toFixed()} ` +
1779
+ `(avail=${available.toFixed()} maint=${maintMargin.toFixed()})`
1780
+ );
1781
+
1782
+ totalNeg = totalNeg.plus(loss);
1783
+
1784
+ // Fully payable: take loss from available then margin
1785
+ if (coverage.gte(loss)) {
1786
+
1787
+ console.log(' -> SOLVENT, clearingLoss');
1788
+
1789
+ const takeAvail = BigNumber.min(available, loss);
1790
+ const takeMargin = loss.minus(takeAvail);
1791
+
1792
+ console.log(
1793
+ `[CLEARING LOSS] addr=${pos.address} ` +
1794
+ `takeAvail=${takeAvail.toFixed()} takeMargin=${takeMargin.toFixed()}`
1795
+ );
1796
+
1797
+ await Tally.updateBalance(
1798
+ pos.address,
1799
+ collateralId,
1800
+ takeAvail.negated().toNumber(),
1801
+ 0,
1802
+ takeMargin.negated().toNumber(),
1803
+ 0,
1804
+ 'clearingLoss',
1805
+ blockHeight
1806
+ );
1807
+ continue;
1808
+ }
1809
+
1810
+ console.log(' -> INSOLVENT, enqueue liquidation');
1811
+
1812
+ liqQueue.push({
1813
+ address: pos.address,
1814
+ pos,
1815
+ loss,
1816
+ shortfall: loss.minus(coverage),
1817
+ coverage
1818
+ });
1819
+ }
1820
+
1821
+ console.log(`[LIQ QUEUE] size=${liqQueue.length}`);
1822
+
1823
+ // ------------------------------------------------------------
1824
+ // 5) SECOND PASS — Liquidations
1825
+ // ------------------------------------------------------------
1826
+ console.log('\n--- SECOND PASS: LIQUIDATIONS ---');
1827
+ const liqEvents = [];
1828
+
1829
+ for (const q of liqQueue) {
1830
+
1831
+ console.log(
1832
+ `[LIQ] ${q.address} loss=${q.loss.toFixed()} ` +
1833
+ `coverage=${q.coverage.toFixed()} shortfall=${q.shortfall.toFixed()}`
1834
+ );
1835
+
1836
+ const tally = await Tally.getTally(q.address, collateralId);
1837
+ console.log(`[LIQ] pre-tally avail=${tally.available} mar=${tally.margin}`);
1838
+
1839
+ const liquidationType = q.coverage.gt(0) ? 'partial' : 'total';
1840
+
1841
+ const liq = await Clearing.handleLiquidation(
1842
+ ctxKey,
1843
+ orderbook,
1844
+ Tally,
1845
+ q.pos,
1846
+ contractId,
1847
+ blockHeight,
1848
+ inverse,
1849
+ collateralId,
1850
+ liquidationType,
1851
+ q.shortfall.toNumber(),
1852
+ notional,
1853
+ lastPrice,
1854
+ true,
1855
+ q.shortfall.toNumber(),
1856
+ tally,
1857
+ priceInfo
1858
+ );
1859
+
1860
+
1861
+ console.log('[LIQ] result=', liq);
1862
+
1863
+ if (!liq) continue;
1864
+
1865
+ systemicLoss = systemicLoss.plus(liq.systemicLoss || 0);
1866
+
1867
+ liqEvents.push({
1868
+ address: q.address,
1869
+ liquidationType,
1870
+ shortfall: q.shortfall.toNumber(),
1871
+ coverage: q.coverage.toNumber(),
1872
+ loss: q.loss.toNumber(),
1873
+ systemicLoss: liq.systemicLoss
1874
+ });
1875
+
1876
+ /*if (liq.counterparties?.length > 0) {
1877
+ console.log(`[LIQ UPDATE POSITIONS] counterparties=`, liq.counterparties);
1878
+ positions = Clearing.updatePositions(positions, liq.counterparties);
1879
+ }*/
1880
+ }
1881
+
1882
+ // ------------------------------------------------------------
1883
+ // 6) THIRD PASS — Profits
1884
+ // ------------------------------------------------------------
1885
+ console.log('\n--- THIRD PASS: PROFITS ---');
1886
+
1887
+ positions = Clearing.getPositionsFromCache(ctxKey);
1888
+ console.log(`[PROFIT PASS] positions.length=${positions.length}`);
1889
+
1890
+ for (const pos of positions) {
1891
+ if (!pos?.contracts || pos.contracts === 0) {
1892
+ delete pos._wasProfitable;
1893
+ continue;
1894
+ }
1895
+ console.log('profit PNL in 3rd pass '+pos.contracts+' '+pos.address+' '+lastPrice+' '+thisPrice+' '+inverse+' '+notional)
1896
+ const profit = Clearing.calculateClearingPNL({
1897
+ oldContracts: pos.contracts,
1898
+ previousMarkPrice: lastPrice,
1899
+ currentMarkPrice: thisPrice,
1900
+ inverse,
1901
+ notional
1902
+ });
1903
+
1904
+ if (profit.gt(0)) {
1905
+ console.log(`[PROFIT] ${pos.address} +${profit.toFixed()}`);
1906
+ totalPos = totalPos.plus(profit);
1907
+
1908
+ await Tally.updateBalance(
1909
+ pos.address,
1910
+ collateralId,
1911
+ profit.toNumber(),
1912
+ 0,
1913
+ 0,
1914
+ 0,
1915
+ 'clearingProfit',
1916
+ blockHeight
1917
+ );
1918
+ }
1919
+
1920
+ delete pos._wasProfitable;
1921
+ }
1922
+
1923
+ // ------------------------------------------------------------
1924
+ // 7) Normalize marks *AFTER* clearing
1925
+ // ------------------------------------------------------------
1926
+ console.log('\n--- NORMALIZE MARKS (POST CLEARING) ---');
1927
+ await Clearing.normalizePositionMarks(
1928
+ positions,
1929
+ thisPrice,
1930
+ null,
1931
+ contractId,
1932
+ blockHeight
1933
+ );
1934
+
1935
+ // ------------------------------------------------------------
1936
+ // 8) Final accounting
1937
+ // ------------------------------------------------------------
1938
+ totalNeg = totalNeg.minus(systemicLoss);
1939
+ const pnlDelta = totalPos.minus(totalNeg);
1940
+
1941
+
1942
+ console.log(`[SUMMARY] totalPos=${totalPos.toFixed()} totalNeg=${totalNeg.toFixed()} systemicLoss=${systemicLoss.toFixed()}`);
1943
+ console.log(`[SUMMARY] pnlDelta=${pnlDelta.toFixed()}`);
1944
+
1945
+ const finalPositions = Clearing.flushPositionCache(ctxKey);
1946
+ console.log(`[WRITE] finalPositions.length=${finalPositions.length}`);
1947
+ await marginMap.mergePositions(finalPositions, contractId, true);
1948
+
1949
+ console.log(`================ END UPDATE MARGIN MAPS ================\n`);
1950
+
1951
+ return { positions, liqEvents, systemicLoss, pnlDelta };
1952
+ }
1953
+
1954
+
1955
+ static async getMarkTradeWindow(priceInfo,contractId) {
1956
+ // priceInfo is the object you now return from isPriceUpdatedForBlockHeight
1957
+ // Expected minimal fields:
1958
+ // - priceInfo.thisPrice
1959
+ // - priceInfo.lastPrice (optional)
1960
+ // - priceInfo.blockHeight (the block where the new mark lives)
1961
+ // - priceInfo.prevBlockHeight (optional but ideal)
1962
+
1963
+ const markBlock = priceInfo?.blockHeight ?? null;
1964
+ const prevBlock = priceInfo?.prevBlockHeight ?? null;
1965
+ const tradeHistoryManager = new TradeHistory()
1966
+ if (prevBlock == null && markBlock != null) {
1967
+ const firstTradeBlock =
1968
+ await tradeHistoryManager.getFirstTradeBlock(contractId);
1969
+
1970
+ return {
1971
+ isBootstrap: true,
1972
+ mustQueryHistory: true,
1973
+ startBlock: firstTradeBlock ?? thisMarkBlock,
1974
+ endBlock: markBlock,
1975
+ useBlockTrades: false
1976
+ };
1977
+ }
1978
+
1979
+
1980
+ if (!markBlock) {
1981
+ return {
1982
+ useBlockTrades: false,
1983
+ mustQueryHistory: false,
1984
+ startBlock: null,
1985
+ endBlock: null,
1986
+ reason: "No markBlock in priceInfo"
1987
+ };
1988
+ }
1989
+
1990
+ // If we don't know the previous mark block, safest assumption is:
1991
+ // same-block cache is NOT sufficient for avgPrice history reconstruction.
1992
+ if (!prevBlock) {
1993
+ return {
1994
+ useBlockTrades: false,
1995
+ mustQueryHistory: true,
1996
+ startBlock: markBlock, // conservative default
1997
+ endBlock: markBlock,
1998
+ reason: "Missing prevBlockHeight; require history"
1999
+ };
2000
+ }
2001
+
2002
+ const gap = markBlock - prevBlock;
2003
+
2004
+ // gap === 0 means a mark update that effectively references same block interval
2005
+ // but in practice markBlock >= prevBlock, and we care if there were trades
2006
+ // in blocks between these marks.
2007
+ if (gap <= 0) {
2008
+ return {
2009
+ useBlockTrades: true,
2010
+ mustQueryHistory: false,
2011
+ startBlock: markBlock,
2012
+ endBlock: markBlock,
2013
+ reason: "No inter-block gap"
2014
+ };
2015
+ }
2016
+
2017
+ // There is a discontinuity: blockTrades only holds current-block trades.
2018
+ // For avgPrice correctness you need trades from prevBlock..markBlock.
2019
+ return {
2020
+ useBlockTrades: false,
2021
+ mustQueryHistory: true,
2022
+ startBlock: prevBlock + 1,
2023
+ endBlock: markBlock,
2024
+ reason: `Gap of ${gap} blocks`
2025
+ };
2026
+ }
2027
+
2028
+
2029
+
2030
+ static applyLossPoolDrain(tally, loss) {
2031
+ const result = {
2032
+ fromAvailable: 0,
2033
+ fromMargin: 0,
2034
+ shortfall: 0
2035
+ };
2036
+
2037
+ let remaining = new BigNumber(loss);
2038
+
2039
+ // 1. Drain available
2040
+ const useAvail = BigNumber.min(remaining, tally.available);
2041
+ result.fromAvailable = useAvail;
2042
+ remaining = remaining.minus(useAvail);
2043
+
2044
+ // 2. Drain margin (but limited to actual margin)
2045
+ const useMargin = BigNumber.min(remaining, tally.margin);
2046
+ result.fromMargin = useMargin;
2047
+ remaining = remaining.minus(useMargin);
2048
+
2049
+ // 3. Whatever remains is shortfall → ADL only
2050
+ result.shortfall = remaining;
2051
+
2052
+ return result;
2053
+ }
2054
+
2055
+
2056
+ static flattenMark(positions) {
2057
+ positions.forEach(pos => {
2058
+ if (pos.contracts === 0) {
2059
+ pos.lastMark = null;
2060
+ }
2061
+ });
2062
+ return positions;
2063
+ }
2064
+
2065
+ // ============================================
2066
+ // ADD TO clearing.js - new helper function
2067
+ // ============================================
2068
+ static calculateMarkToMarkPNL({ contracts, fromPrice, toPrice, inverse, notional }) {
2069
+ const Big = BigNumber;
2070
+ const c = new Big(contracts);
2071
+ const from = new Big(fromPrice);
2072
+ const to = new Big(toPrice);
2073
+ const n = new Big(notional || 1);
2074
+
2075
+ if (from.isZero() || to.isZero()) {
2076
+ return new Big(0);
2077
+ }
2078
+
2079
+ let pnl;
2080
+ if (!inverse) {
2081
+ // Linear: PNL = (toPrice - fromPrice) * contracts * notional
2082
+ pnl = to.minus(from).times(c).times(n);
2083
+ } else {
2084
+ // Inverse: PNL = (1/fromPrice - 1/toPrice) * contracts * notional
2085
+ pnl = new Big(1).div(from).minus(new Big(1).div(to)).times(c).times(n);
2086
+ }
2087
+
2088
+ return pnl.dp(8);
2089
+ }
2090
+
2091
+ static recomputeContractBalanceSnapshot(pos, remainder, price, isLong, inverse) {
2092
+ const p = { ...pos };
2093
+
2094
+ // same logic you already used inside marginMap.updateContractBalances
2095
+ // but applied *locally* to the cloned position
2096
+
2097
+ if (remainder > 0) {
2098
+ p.contracts = remainder;
2099
+ } else {
2100
+ p.contracts = 0;
2101
+ p.avgPrice = 0;
2102
+ }
2103
+
2104
+ p.lastMark = price;
2105
+ return p;
2106
+ }
2107
+
2108
+
2109
+ // Make sure BigNumber is imported:
2110
+ // const BigNumber = require("bignumber.js");
2111
+ static computeLiquidationPriceFromLoss(
2112
+ lastPrice,
2113
+ equity,
2114
+ contracts,
2115
+ notional,
2116
+ inverse
2117
+ ) {
2118
+ const PRECISION = 30;
2119
+ console.log('last price in compute liq price '+lastPrice+' '+equity+' '+contracts)
2120
+ const BNLast = new BigNumber(lastPrice);
2121
+ const BNEq = new BigNumber(equity);
2122
+ const BNContracts = new BigNumber(contracts);
2123
+ const BNNotional = new BigNumber(notional);
2124
+
2125
+ if (BNContracts.isZero() || BNEq.lte(0)) {
2126
+ return BNLast.decimalPlaces(PRECISION);
2127
+ }
2128
+
2129
+ // -------------------------------
2130
+ // LINEAR CONTRACTS
2131
+ // PnL = contracts × notional × (price − lastPrice)
2132
+ // -------------------------------
2133
+ if (!inverse) {
2134
+ if (BNContracts.gt(0)) {
2135
+ // Long → bankruptcy below lastPrice
2136
+ return BNLast
2137
+ .minus(BNEq.div(BNContracts.multipliedBy(BNNotional)))
2138
+ .decimalPlaces(PRECISION);
2139
+ } else {
2140
+ // Short → bankruptcy above lastPrice
2141
+ return BNLast
2142
+ .plus(BNEq.div(BNContracts.absoluteValue().multipliedBy(BNNotional)))
2143
+ .decimalPlaces(PRECISION);
2144
+ }
2145
+ }
2146
+
2147
+ // -------------------------------
2148
+ // INVERSE CONTRACTS
2149
+ // PnL = contracts × notional × (1/lastPrice − 1/price)
2150
+ // -------------------------------
2151
+ const invLast = new BigNumber(1).dividedBy(BNLast);
2152
+
2153
+ if (BNContracts.gt(0)) {
2154
+ // Inverse long → bankruptcy at lower price
2155
+ const invBkr = invLast.plus(
2156
+ BNEq.div(BNContracts.multipliedBy(BNNotional))
2157
+ );
2158
+ return new BigNumber(1).dividedBy(invBkr).decimalPlaces(PRECISION);
2159
+ } else {
2160
+ // Inverse short → bankruptcy at higher price
2161
+ const invBkr = invLast.minus(
2162
+ BNEq.div(BNContracts.absoluteValue().multipliedBy(BNNotional))
2163
+ );
2164
+ return new BigNumber(1).dividedBy(invBkr).decimalPlaces(PRECISION);
2165
+ }
2166
+ }
2167
+
2168
+ static getOpenedByAddressFromTrades(relevantTrades) {
2169
+ const openedByAddress = new Map();
2170
+
2171
+ if (!Array.isArray(relevantTrades)) return openedByAddress;
2172
+
2173
+ for (const t of relevantTrades) {
2174
+ const trade = t?.trade ?? t;
2175
+ if (!trade) continue;
2176
+
2177
+ const amt = Number(trade.amount || 0);
2178
+ if (!amt) continue;
2179
+
2180
+ const buyer = trade.buyerAddress;
2181
+ const seller = trade.sellerAddress;
2182
+
2183
+ if (buyer) {
2184
+ openedByAddress.set(
2185
+ buyer,
2186
+ (openedByAddress.get(buyer) || 0) + amt
2187
+ );
2188
+ }
2189
+
2190
+ if (seller) {
2191
+ openedByAddress.set(
2192
+ seller,
2193
+ (openedByAddress.get(seller) || 0) - amt
2194
+ );
2195
+ }
2196
+ }
2197
+
2198
+ return openedByAddress;
2199
+ }
2200
+
2201
+
2202
+
2203
+ static updatePositions(positions, updatedCounterparties) {
2204
+ if (!updatedCounterparties) return positions;
2205
+
2206
+ const counterpartyMap = new Map(updatedCounterparties.map(pos => [pos.address, pos]));
2207
+ const result = positions.map(pos =>
2208
+ counterpartyMap.has(pos.address)
2209
+ ? { ...pos, ...counterpartyMap.get(pos.address) }
2210
+ : pos
2211
+ );
2212
+
2213
+ // Add any counterparties that weren't in original positions
2214
+ for (const cp of updatedCounterparties) {
2215
+ if (!positions.find(p => p.address === cp.address)) {
2216
+ result.push({ ...cp });
2217
+ }
2218
+ }
2219
+
2220
+ return result;
2221
+ }
2222
+
2223
+ static async handleLiquidation(
2224
+ ctxKey,
2225
+ orderbook,
2226
+ Tally,
2227
+ position,
2228
+ contractId,
2229
+ blockHeight,
2230
+ inverse,
2231
+ collateralId,
2232
+ liquidationType, // "partial" | "total"
2233
+ marginDent, // positive number = RESIDUAL loss to resolve (post updateMarginMaps debit)
2234
+ notional,
2235
+ markPrice,
2236
+ applyDent,
2237
+ markShortfall,
2238
+ tallySnapshot,
2239
+ priceInfo
2240
+ ) {
2241
+ const Clearing = this;
2242
+ const MarginMap = require('./marginMap.js');
2243
+ const marginMap = await MarginMap.getInstance(contractId);
2244
+
2245
+ const BigNumber = require('bignumber.js');
2246
+ const Big = BigNumber.clone();
2247
+ const liquidatingAddress = position.address;
2248
+
2249
+ console.log(`🔥 handleLiquidation(${liquidationType}) for ${liquidatingAddress}`);
2250
+
2251
+ //------------------------------------------------------------
2252
+ // 0. Load cache snapshot
2253
+ //------------------------------------------------------------
2254
+ const positionCache = Clearing.getPositionsFromCache(ctxKey);
2255
+
2256
+ //------------------------------------------------------------
2257
+ // 1. Compute liquidation size (liqAmount)
2258
+ //------------------------------------------------------------
2259
+ const tally = tallySnapshot || await Tally.getTally(liquidatingAddress, collateralId);
2260
+
2261
+ const maintReq = new Big(await marginMap.checkMarginMaintainance(
2262
+ liquidatingAddress,
2263
+ contractId,
2264
+ position
2265
+ ) || 0);
2266
+
2267
+ const equity = new Big(tally.margin || 0).plus(tally.available || 0);
2268
+ const deficit = maintReq.minus(equity);
2269
+
2270
+ let liqAmount;
2271
+ const absContracts = Math.abs(position.contracts);
2272
+
2273
+ if (deficit.gt(0) && deficit.lte(new Big(tally.margin || 0))) {
2274
+ // partial liquidation to cure margin dent
2275
+ const ContractRegistry = require('./contractRegistry.js');
2276
+ const initPerContract = new Big(
2277
+ await ContractRegistry.getInitialMargin(contractId, markPrice)
2278
+ );
2279
+
2280
+ liqAmount = Big.min(
2281
+ absContracts,
2282
+ deficit.div(initPerContract).dp(8)
2283
+ ).toNumber();
2284
+
2285
+ liquidationType = "partial";
2286
+ } else {
2287
+ liqAmount = absContracts;
2288
+ liquidationType = "total";
2289
+ }
2290
+
2291
+ if (liqAmount <= 0) {
2292
+ // If we were called, insolvency/shortfall already exists upstream.
2293
+ // Force a liquidation amount rather than returning null.
2294
+ liqAmount = Math.abs(position.contracts);
2295
+ liquidationType = "total";
2296
+ console.warn(`⚠️ liqAmount computed <=0; forcing total liquidation for ${liquidatingAddress}`);
2297
+ }
2298
+
2299
+
2300
+ //------------------------------------------------------------
2301
+ // 2. Compute bankruptcy / liquidation price
2302
+ //------------------------------------------------------------
2303
+ markShortfall ??= 0;
2304
+
2305
+ let lossBudget = new Big(markShortfall);
2306
+ if (lossBudget.lte(0)) {
2307
+ lossBudget = new Big(tally.margin || 0).plus(tally.available || 0);
2308
+ }
2309
+
2310
+ const computedLiqPrice = Clearing.computeLiquidationPriceFromLoss(
2311
+ markPrice,
2312
+ lossBudget.toNumber(),
2313
+ position.contracts,
2314
+ notional,
2315
+ inverse
2316
+ );
2317
+
2318
+ console.log('estimate bankruptcyPrice' +computedLiqPrice)
2319
+
2320
+ //------------------------------------------------------------
2321
+ // 3. Generate liquidation order object
2322
+ //------------------------------------------------------------
2323
+ let liq = await marginMap.generateLiquidationOrder(
2324
+ position,
2325
+ contractId,
2326
+ liquidationType === "total",
2327
+ blockHeight,
2328
+ markPrice,
2329
+ computedLiqPrice
2330
+ );
2331
+
2332
+ if (!liq || liq === "err:0 contracts") return null;
2333
+
2334
+ liq.amount = liqAmount;
2335
+ liq.price = liq.price || computedLiqPrice;
2336
+ liq.bankruptcyPrice = liq.bankruptcyPrice || computedLiqPrice;
2337
+
2338
+ const bankruptcyPrice = liq.bankruptcyPrice;
2339
+ // force liquidation side from position sign (do this before estimateLiquidation)
2340
+ const isSell = (position.contracts > 0); // long -> SELL into bids, short -> BUY into asks
2341
+ liq.sell = isSell;
2342
+
2343
+
2344
+ //------------------------------------------------------------
2345
+ // 4. Estimate book fill BEFORE inserting order
2346
+ //------------------------------------------------------------
2347
+ console.log('contractId before est Liq '+contractId)
2348
+ const splat = await orderbook.estimateLiquidation(liq, notional, computedLiqPrice,computedLiqPrice,inverse,contractId);
2349
+ console.log("🔎 estimateLiquidation →", JSON.stringify(splat));
2350
+ const canObFill = (splat && Number(splat.goodFilledSize || 0) > 0);
2351
+ console.log('can Ob Fill '+canObFill+' '+splat.goodFilledSize)
2352
+ // ============================================================
2353
+ // FIX B1: residual-loss semantics
2354
+ // updateMarginMaps already debited "coverage". marginDent here
2355
+ // is the RESIDUAL that must be resolved by confiscation/pool/ADL.
2356
+ // So totalLossNeeded = shortfall only.
2357
+ // ============================================================
2358
+ const shortfallBN = new Big(marginDent || 0);
2359
+ let lossBN = shortfallBN; // <-- key change (was coverage+shortfall)
2360
+
2361
+ //------------------------------------------------------------
2362
+ // 6. Attempt OB matching
2363
+ //------------------------------------------------------------
2364
+ let obFill = new Big(0);
2365
+
2366
+ let markImprovement = 0;
2367
+ const preTradePositions = positionCache.map(p => ({ ...p }));
2368
+ if (canObFill) {
2369
+ console.log('inside liquidation order drop!')
2370
+ const obKey = contractId.toString();
2371
+ let obData = orderbook.orderBooks[obKey] || { buy: [], sell: [] };
2372
+
2373
+ // ============================================================
2374
+ // FIX B2: only insert the SAFE prefix size (goodFilledSize),
2375
+ // so the matching engine can’t fill beyond the safe-at-or-better
2376
+ // amount in the same call.
2377
+ // ============================================================
2378
+ const safeSize = Number(splat.goodFilledSize || 0);
2379
+ const liqOb = { ...liq, amount: safeSize };
2380
+
2381
+ console.log('safe size!? '+safeSize)
2382
+
2383
+ obData = await orderbook.insertOrder(liqOb, obData, liqOb.sell, true);
2384
+ let trades= []
2385
+
2386
+ const matchResult = await orderbook.matchContractOrders(obData);
2387
+ if (matchResult.matches && matchResult.matches.length > 0) {
2388
+ trades= await orderbook.processContractMatches(matchResult.matches, blockHeight, false,markPrice);
2389
+ }
2390
+
2391
+ console.log('liq match result '+JSON.stringify(matchResult))
2392
+ await orderbook.saveOrderBook(matchResult.orderBook, obKey);
2393
+
2394
+ // PATCH 1: set obFill to what actually matched (best-effort from match objects)
2395
+ let filledFromMatches = 0;
2396
+ if (matchResult && Array.isArray(matchResult.matches)) {
2397
+ for (const m of matchResult.matches) {
2398
+ console.log('match '+JSON.stringify(m))
2399
+ const qty =
2400
+ Number(m.sellOrder?.amount ?? m.buyOrder?.amount ?? 0);
2401
+
2402
+ if (qty > 0) filledFromMatches += qty;
2403
+ }
2404
+ }
2405
+
2406
+ // Never exceed requested liqAmount
2407
+ obFill = new Big(Math.min(filledFromMatches, liqAmount));
2408
+ console.log('obFill after matches '+obFill.toNumber()+' '+filledFromMatches+' '+liqAmount)
2409
+
2410
+
2411
+ // ============================================================
2412
+ // ✅ CANONICAL FIX: advance positionCache from TRADE RESULTS
2413
+ // ============================================================
2414
+ // ============================================================
2415
+ // Apply ONLY the final position per address from this batch
2416
+ // ============================================================
2417
+ if (trades.length>0){
2418
+ const finalPositions = new Map(); // addr -> position
2419
+
2420
+ for (const t of trades) {
2421
+ if (t.buyerAddress && t.buyerPosition) {
2422
+ finalPositions.set(t.buyerAddress, t.buyerPosition);
2423
+ }
2424
+ if (t.sellerAddress && t.sellerPosition) {
2425
+ finalPositions.set(t.sellerAddress, t.sellerPosition);
2426
+ }
2427
+ }
2428
+
2429
+ for (const [addr, pos] of finalPositions.entries()) {
2430
+ Clearing.addOrUpdatePositionInCache(ctxKey, addr, pos);
2431
+ console.log(`[CACHE APPLY FINAL] ${addr} contracts=${pos.contracts}`
2432
+ );
2433
+ }
2434
+ }
2435
+
2436
+ }
2437
+
2438
+ //await Clearing.settleLiqNewContractsFromDB(contractId, blockHeight, priceInfo.thisPrice,ctxKey,preTradePositions)
2439
+
2440
+ //------------------------------------------------------------
2441
+ // 7. Determine ADL remainder
2442
+ //------------------------------------------------------------
2443
+ const adlSize = new Big(liqAmount).minus(obFill);
2444
+ const remainder = adlSize.gt(0) ? adlSize.toNumber() : 0;
2445
+
2446
+ let residualLossBN = new Big(0);
2447
+ if (remainder > 0) {
2448
+ //------------------------------------------------------------
2449
+ // 7.5 Recompute residual loss for the UNFILLED size
2450
+ // Loss = remainder contracts moving from lastMark to thisPrice
2451
+ //------------------------------------------------------------
2452
+ const qtyBN = new Big(remainder).dp(8);
2453
+ const lastBN = new Big(priceInfo.lastPrice || markPrice);
2454
+ const thisBN = new Big(priceInfo.thisPrice || markPrice);
2455
+ const notBN = new Big(notional || 1);
2456
+
2457
+ if (!inverse) {
2458
+ // Linear: loss = qty * |lastMark - thisPrice| * notional
2459
+ residualLossBN = qtyBN.times(lastBN.minus(thisBN).abs()).times(notBN);
2460
+ } else {
2461
+ // Inverse: loss = qty * |1/thisPrice - 1/lastMark| * notional
2462
+ if (lastBN.gt(0) && thisBN.gt(0)) {
2463
+ const invLast = new Big(1).div(lastBN);
2464
+ const invThis = new Big(1).div(thisBN);
2465
+ residualLossBN = qtyBN.times(invThis.minus(invLast).abs()).times(notBN);
2466
+ }
2467
+ }
2468
+ residualLossBN = residualLossBN.dp(8);
2469
+ }
2470
+
2471
+ console.log('residual loss for remainder ' + residualLossBN.toNumber() + ' ' + remainder + ' last=' + priceInfo.lastPrice + ' this=' + priceInfo.thisPrice);
2472
+
2473
+ //------------------------------------------------------------
2474
+ // 8. Calculate liquidation pool BEFORE confiscation
2475
+ // Pool = min(what we need, what's available)
2476
+ //------------------------------------------------------------
2477
+ const liqTally = await Tally.getTally(liquidatingAddress, collateralId);
2478
+ const fullPoolBN = new Big(liqTally.margin || 0)
2479
+ .plus(liqTally.available || 0)
2480
+ .dp(8);
2481
+
2482
+ const seizureBN = Big.min(fullPoolBN, residualLossBN).dp(8);
2483
+ const liquidationPool = seizureBN.toNumber();
2484
+ console.log('liquidation pool ' + liquidationPool + ' (needed=' + residualLossBN.toNumber() + ' available=' + fullPoolBN.toNumber() + ')');
2485
+
2486
+ //------------------------------------------------------------
2487
+ // 9. Confiscate liquidation pool (seized amount only)
2488
+ // Debit available first, then margin
2489
+ //------------------------------------------------------------
2490
+ if (liquidationPool > 0) {
2491
+ const availBN = new Big(liqTally.available || 0);
2492
+ const seizeAvailBN = Big.min(availBN, seizureBN).dp(8);
2493
+ const seizeMarginBN = seizureBN.minus(seizeAvailBN).dp(8);
2494
+
2495
+ await Tally.updateBalance(
2496
+ liquidatingAddress,
2497
+ collateralId,
2498
+ -seizeAvailBN.toNumber(),
2499
+ 0,
2500
+ -seizeMarginBN.toNumber(),
2501
+ 0,
2502
+ 'liquidationPoolDebit',
2503
+ blockHeight
2504
+ );
2505
+ }
2506
+
2507
+ //------------------------------------------------------------
2508
+ // 10. Systemic loss - use actual shortfall vs seized amount
2509
+ //------------------------------------------------------------
2510
+ let systemicLoss = new Big(0);
2511
+
2512
+ const totalLossNeeded = lossBN;
2513
+ const seizedBN = new Big(liquidationPool);
2514
+
2515
+ if (totalLossNeeded.gt(seizedBN)) {
2516
+ systemicLoss = totalLossNeeded.minus(seizedBN).dp(8);
2517
+ }
2518
+
2519
+ //------------------------------------------------------------
2520
+ // 11. Apply ADL if needed - pass actual pool amount
2521
+ //------------------------------------------------------------
2522
+ let result = { counterparties: [], poolAssignments: [] };
2523
+ if (remainder > 0) {
2524
+ result = await marginMap.simpleDeleverage(
2525
+ positionCache,
2526
+ contractId,
2527
+ remainder,
2528
+ isSell,
2529
+ bankruptcyPrice,
2530
+ liquidatingAddress,
2531
+ inverse,
2532
+ notional,
2533
+ blockHeight,
2534
+ markPrice,
2535
+ collateralId,
2536
+ liquidationPool
2537
+ );
2538
+ }
2539
+ console.log('adl result '+JSON.stringify(result));
2540
+
2541
+ //------------------------------------------------------------
2542
+ // 12. Apply pool credits from ADL - CAPPED at pool
2543
+ //------------------------------------------------------------
2544
+ let poolRemaining = new Big(liquidationPool);
2545
+
2546
+ for (const u of (result.poolAssignments || [])) {
2547
+ if (poolRemaining.lte(0)) break;
2548
+
2549
+ const share = new Big(u.poolShare || 0);
2550
+ if (share.lte(0)) continue;
2551
+
2552
+ const creditAmount = Big.min(share, poolRemaining).dp(8);
2553
+
2554
+ await Tally.updateBalance(
2555
+ u.address,
2556
+ collateralId,
2557
+ creditAmount.toNumber(),
2558
+ 0, 0, 0,
2559
+ 'deleveragePoolCredit',
2560
+ blockHeight
2561
+ );
2562
+
2563
+ poolRemaining = poolRemaining.minus(creditAmount);
2564
+ }
2565
+
2566
+ //------------------------------------------------------------
2567
+ // 13. Apply CP position updates
2568
+ //------------------------------------------------------------
2569
+ for (const cp of (result.counterparties || [])) {
2570
+ Clearing.updatePositionInCache(ctxKey, cp.address, () => ({ ...cp.updatedPosition }));
2571
+ Clearing.recordDeleverageTrade(contractId, cp.address, cp);
2572
+ }
2573
+
2574
+ //------------------------------------------------------------
2575
+ // 14. Zero out liquidated position
2576
+ //------------------------------------------------------------
2577
+ console.log('zero out liqd addr '+liquidatingAddress+' '+ctxKey)
2578
+ Clearing.updatePositionInCache(ctxKey, liquidatingAddress, old => ({
2579
+ ...old,
2580
+ contracts: 0,
2581
+ margin: 0,
2582
+ unrealizedPNL: 0,
2583
+ averagePrice: null,
2584
+ bankruptcyPrice: null,
2585
+ lastMark: markPrice
2586
+ }));
2587
+
2588
+ //------------------------------------------------------------
2589
+ // 15. Return summary
2590
+ //------------------------------------------------------------
2591
+ return {
2592
+ liquidation: liq,
2593
+ systemicLoss: systemicLoss.toNumber(),
2594
+ counterparties: result.counterparties || [],
2595
+ totalDeleveraged: obFill.plus(remainder).dp(8).toNumber()
2596
+ };
2597
+ }
2598
+
2599
+
2600
+ /**
2601
+ * Settle all options expiring at or before currentBlock for a given series.
2602
+ * Intrinsic only (European-style cash). Premium MTM is for equity/liq calcs only.
2603
+ */
2604
+ static async settleOptionExpiries(seriesId, currentBlockHeight, spot, blocksPerDay, txid) {
2605
+ const mm = await MarginMap.getInstance(seriesId);
2606
+ const seriesInfo = await ContractRegistry.getContractInfo(seriesId);
2607
+ if (!seriesInfo) return;
2608
+ const collateralPropertyId = seriesInfo.collateralPropertyId;
2609
+
2610
+ const expTickers = await mm.getExpiringTickersUpTo(currentBlockHeight);
2611
+ if (!expTickers.length) return;
2612
+
2613
+ // For each address with positions
2614
+ for (const [address, pos] of mm.margins.entries()) {
2615
+ if (!pos || !pos.options) continue;
2616
+
2617
+ for (const ticker of expTickers) {
2618
+ const optPos = pos.options[ticker];
2619
+ if (!optPos) continue;
2620
+
2621
+ const qty = Number(optPos.contracts || 0);
2622
+ if (!qty) {
2623
+ // remove the empty slot to keep map clean
2624
+ delete pos.options[ticker];
2625
+ continue;
2626
+ }
2627
+
2628
+ const meta = Options.parseTicker(ticker);
2629
+ if (!meta) continue;
2630
+
2631
+ // Intrinsic payoff at settlement
2632
+ const iv = Options.intrinsic(meta.type, Number(meta.strike || 0), Number(spot || 0));
2633
+ const cash = iv * Math.abs(qty); // per-contract * absolute qty
2634
+
2635
+ // Long options receive; short options pay
2636
+ const availableDelta = qty > 0 ? +cash : -cash;
2637
+
2638
+ // Free any margin previously held on this option leg
2639
+ const marginHeld = Number(optPos.margin || 0);
2640
+ const marginDelta = marginHeld ? -marginHeld : 0;
2641
+
2642
+ // Tally: available +/- intrinsic; margin -= marginHeld
2643
+ await TallyMap.updateBalance(
2644
+ address,
2645
+ collateralPropertyId,
2646
+ availableDelta, // availableChange
2647
+ 0, // reservedChange
2648
+ marginDelta, // marginChange
2649
+ 0, // vestingChange
2650
+ 'optionExpire',
2651
+ currentBlockHeight,
2652
+ txid
2653
+ );
2654
+
2655
+ // Remove the option sub-position from the blob
2656
+ delete pos.options[ticker];
2657
+
2658
+ // Record margin map delta
2659
+ await mm.recordMarginMapDelta(
2660
+ address,
2661
+ ticker,
2662
+ 0, // position after (expired → closed)
2663
+ -qty, // delta contracts to flat
2664
+ iv, // settled at intrinsic (for audit)
2665
+ 0, // uPNL delta
2666
+ marginHeld ? -marginHeld : 0, // margin freed
2667
+ 'optionExpire',
2668
+ currentBlockHeight
2669
+ );
2670
+ }
2671
+
2672
+ // Save back the mutated blob
2673
+ mm.margins.set(address, pos);
2674
+ }
2675
+
2676
+ // Global index cleanup (remove those expiries)
2677
+ await mm.cleanupExpiredTickersUpTo(currentBlockHeight);
2678
+ }
2679
+
2680
+ static getLatestPositionByAddress(trades, address) {
2681
+ // Loop backwards since later trades are more recent
2682
+ for (let i = trades.length - 1; i >= 0; i--) {
2683
+ const trade = trades[i];
2684
+ // Check buyerPosition first
2685
+ if (trade.buyerPosition && trade.buyerPosition.address === address) {
2686
+ return trade.buyerPosition;
2687
+ }
2688
+ // Check sellerPosition
2689
+ if (trade.sellerPosition && trade.sellerPosition.address === address) {
2690
+ return trade.sellerPosition;
2691
+ }
2692
+ }
2693
+ // If no matching position is found, return null
2694
+ return null;
2695
+ }
2696
+
2697
+
2698
+ static sortPositionsForPNL(positions, priceDiff) {
2699
+ return positions.sort((a, b) => {
2700
+ if (priceDiff) {
2701
+ // Price is increasing -> Shorts should go first
2702
+ return a.contracts - b.contracts;
2703
+ } else {
2704
+ // Price is decreasing -> Longs should go first
2705
+ return b.contracts - a.contracts;
2706
+ }
2707
+ });
2708
+ }
2709
+
2710
+
2711
+ static calculateClearingPNL({
2712
+ oldContracts,
2713
+ previousMarkPrice,
2714
+ currentMarkPrice,
2715
+ inverse,
2716
+ notional
2717
+ }) {
2718
+ const BigNumber = require('bignumber.js');
2719
+
2720
+ const size = new BigNumber(oldContracts || 0);
2721
+ if (size.isZero()) return new BigNumber(0);
2722
+
2723
+ const last = new BigNumber(previousMarkPrice || 0);
2724
+ const cur = new BigNumber(
2725
+ currentMarkPrice != null ? currentMarkPrice : previousMarkPrice || 0
2726
+ );
2727
+
2728
+ // no mark movement → no clearing PnL
2729
+ if (last.eq(cur)) return new BigNumber(0);
2730
+
2731
+ const noto = new BigNumber(notional || 1);
2732
+ let pnl;
2733
+
2734
+ if (!inverse) {
2735
+ // linear
2736
+ pnl = size
2737
+ .times(cur.minus(last))
2738
+ .times(noto);
2739
+ } else {
2740
+ // inverse
2741
+ if (last.isZero() || cur.isZero()) return new BigNumber(0);
2742
+
2743
+ pnl = size
2744
+ .times(
2745
+ new BigNumber(1).div(last)
2746
+ .minus(new BigNumber(1).div(cur))
2747
+ )
2748
+ .times(noto);
2749
+ }
2750
+
2751
+ return pnl.isFinite() ? pnl : new BigNumber(0);
2752
+ }
2753
+
2754
+ // newContractPnL.js
2755
+ static calculateNewContractPNL({
2756
+ newContracts,
2757
+ avgEntryPrice,
2758
+ lastPrice,
2759
+ inverse,
2760
+ notional
2761
+ }){
2762
+ const BigNumber = require('bignumber.js');
2763
+
2764
+ const size = new BigNumber(newContracts || 0);
2765
+ if (size.isZero()) return new BigNumber(0);
2766
+
2767
+ const avg = new BigNumber(avgEntryPrice || 0);
2768
+ const exec = new BigNumber(lastPrice || 0);
2769
+ if (avg.isZero() || exec.eq(avg)) return new BigNumber(0);
2770
+
2771
+ const noto = new BigNumber(notional || 1);
2772
+ let pnl;
2773
+
2774
+ if (!inverse) {
2775
+ pnl = size
2776
+ .times(exec.minus(avg))
2777
+ .times(noto);
2778
+ } else {
2779
+ pnl = size
2780
+ .times(
2781
+ new BigNumber(1).div(avg)
2782
+ .minus(new BigNumber(1).div(exec))
2783
+ )
2784
+ .times(noto);
2785
+ }
2786
+ console.log('new contract clearing PNL '+pnl.toNumber()+' '+size.toNumber()+' '+avg.toNumber()+' '+exec.toNumber())
2787
+ return pnl.isFinite() ? pnl : new BigNumber(0);
2788
+ }
2789
+
2790
+ static async getBalance(holderAddress) {
2791
+ // Replace this with actual data fetching logic for your system
2792
+ try {
2793
+ let balance = await database.getBalance(holderAddress);
2794
+ return balance;
2795
+ } catch (error) {
2796
+ console.error('Error fetching balance for address:', holderAddress, error);
2797
+ //throw error;
2798
+ }
2799
+ }
2800
+
2801
+ static async settleIousForBlock(contractId, collateralId, blockHeight) {
2802
+ const doc = await PnlIou.getDoc(contractId, collateralId);
2803
+ if (!doc) return;
2804
+
2805
+ const TallyMap = require('./tally.js');
2806
+ const BigNumber = require('bignumber.js');
2807
+
2808
+ console.log('doc in settleIous: ' + JSON.stringify(doc));
2809
+
2810
+ // CRITICAL FIX: Use blockLosses directly instead of blockReductionTowardZero
2811
+ // blockLosses = real tokens debited from losers this block, available for payout
2812
+ const blockLosses = new BigNumber(doc.blockLosses || 0);
2813
+
2814
+ console.log('blockLosses for payout: ' + blockLosses.toNumber());
2815
+
2816
+ if (blockLosses.lte(0)) {
2817
+ console.log('[settleIous] No losses this block to pay out');
2818
+ return;
2819
+ }
2820
+
2821
+ const allocations = await PnlIou.payOutstandingIous(
2822
+ contractId,
2823
+ collateralId,
2824
+ blockLosses.toNumber(),
2825
+ blockHeight
2826
+ );
2827
+
2828
+ console.log('allocations: ' + JSON.stringify(allocations));
2829
+
2830
+ if (!allocations.length) return;
2831
+
2832
+ for (const a of allocations) {
2833
+ await TallyMap.updateBalance(
2834
+ a.address,
2835
+ collateralId,
2836
+ a.amount,
2837
+ 0, 0, 0,
2838
+ 'iouPayout',
2839
+ blockHeight,
2840
+ ''
2841
+ );
2842
+ }
2843
+ }
2844
+
2845
+
2846
+ static async performAdditionalSettlementTasks(blockHeight,positions, contractId, mark,totalLossSN,collateralId,pnlDelta){
2847
+
2848
+ const totalLoss= new BigNumber(totalLossSN)
2849
+ //try {
2850
+ // Step 2: Check if insurance fund payout is needed
2851
+ console.log(
2852
+ 'total loss for '+contractId+' '+
2853
+ (typeof totalLoss === 'object' && totalLoss.toNumber ? totalLoss.toNumber() : totalLoss)
2854
+ );
2855
+
2856
+ if (totalLoss.gte(0)) {
2857
+ const ContractRegistry = require('./contractRegistry.js');
2858
+ const isOracleContract = await ContractRegistry.isOracleContract(contractId);
2859
+ const insurance = await Insurance.getInstance(contractId, isOracleContract);
2860
+
2861
+ const payout = await insurance.calcPayout(totalLoss.abs(), blockHeight);
2862
+ console.log('payout to distribute '+payout)
2863
+ if (payout>0) {
2864
+ await Clearing.distributeInsuranceProRataToDelev(
2865
+ contractId,
2866
+ collateralId,
2867
+ payout, // ✅ PASS PAYOUT, NOT totalLoss
2868
+ blockHeight
2869
+ );
2870
+ }
2871
+
2872
+ const remainingLoss = totalLoss.minus(payout);
2873
+ console.log('remaining loss ' + remainingLoss);
2874
+ }
2875
+ //} catch (error) {
2876
+ // console.error('Error performing additional settlement tasks:', error);
2877
+ // throw error;
2878
+ //}
2879
+ }
2880
+
2881
+ // -------------------------
2882
+ // INSURANCE RESOLUTION HELPERS
2883
+ // -------------------------
2884
+ static async resolveInsuranceMeta(tradingContractId) {
2885
+ const ContractRegistry = require('./contractRegistry.js');
2886
+
2887
+ // For now: fund is keyed by same contractId,
2888
+ // but type is determined by registry (drives -oracle storage behavior).
2889
+ const isOracle = await ContractRegistry.isOracleContract(tradingContractId);
2890
+ const insuranceContractId = tradingContractId;
2891
+
2892
+ return { insuranceContractId, isOracle };
2893
+ }
2894
+
2895
+ static async distributeInsuranceProRataToDelev(
2896
+ tradingContractId,
2897
+ collateralId,
2898
+ payout, // NUMBER
2899
+ blockHeight
2900
+ ) {
2901
+ const BigNumber = require('bignumber.js');
2902
+ const Tally = require('./tally.js');
2903
+ const PnlIou = require('./iou.js');
2904
+
2905
+ if (!payout || payout <= 0) return;
2906
+
2907
+ const payoutBN = new BigNumber(payout);
2908
+
2909
+ let totalContracts = new BigNumber(0);
2910
+
2911
+ for (const [key, trades] of Clearing.deleverageTrades.entries()) {
2912
+ if (!key.startsWith(`${tradingContractId}:`)) continue;
2913
+ for (const t of trades) {
2914
+ totalContracts = totalContracts.plus(t.matchSize || 0);
2915
+ }
2916
+ }
2917
+
2918
+ if (totalContracts.lte(0)) return;
2919
+
2920
+ let distributed = new BigNumber(0);
2921
+
2922
+ for (const [key, trades] of Clearing.deleverageTrades.entries()) {
2923
+ if (!key.startsWith(`${tradingContractId}:`)) continue;
2924
+
2925
+ const address = key.split(':')[1];
2926
+
2927
+ let addressContracts = new BigNumber(0);
2928
+ for (const t of trades) {
2929
+ addressContracts = addressContracts.plus(t.matchSize || 0);
2930
+ }
2931
+
2932
+ if (addressContracts.lte(0)) continue;
2933
+
2934
+ const share = payoutBN.times(addressContracts).div(totalContracts);
2935
+
2936
+ if (share.lte(0)) continue;
2937
+
2938
+ await Tally.updateBalance(
2939
+ address,
2940
+ collateralId,
2941
+ share, // availableChange (+)
2942
+ 0, // reservedChange
2943
+ 0, // marginChange
2944
+ 0, // vestingChange
2945
+ 'insuranceDelev',
2946
+ blockHeight,
2947
+ '' // txid (synthetic / none)
2948
+ );
2949
+
2950
+
2951
+ distributed = distributed.plus(share);
2952
+ }
2953
+
2954
+ const dust = payoutBN.minus(distributed);
2955
+ if (dust.abs().gt(0)) {
2956
+ await PnlIou.absorbDust(
2957
+ tradingContractId,
2958
+ collateralId,
2959
+ dust,
2960
+ blockHeight
2961
+ );
2962
+ }
2963
+ }
2964
+
2965
+ /**
2966
+ * Summarize options for an address under a given series (for liquidation offsets).
2967
+ * Returns:
2968
+ * {
2969
+ * premiumMTM, // mark-to-model value of options (can be +/-) at current spot
2970
+ * intrinsicNet, // net intrinsic (>=0 longs, <=0 shorts aggregated)
2971
+ * maintNaked // maintenance add-on for naked shorts (padding for triggers)
2972
+ * }
2973
+ */
2974
+ async computeOptionAdjustments(seriesId, address, spot, currentBlockHeight, blocksPerDay) {
2975
+ const mm = await MarginMap.getInstance(seriesId);
2976
+ const pos = mm.margins.get(address) || {};
2977
+ const optionsBag = pos.options || {};
2978
+ const seriesInfo = await ContractRegistry.getContractInfo(seriesId);
2979
+ // If you store a vol index on the series, grab it; else fallback conservatively
2980
+ const volAnnual = Number(seriesInfo?.volAnnual || 0); // e.g. 0.6 means 60% annualized
2981
+ const bpd = Math.max(1, Number(blocksPerDay || 144));
2982
+ let premiumMTM = 0;
2983
+ let intrinsicNet = 0;
2984
+ let maintNaked = 0;
2985
+
2986
+ for (const [ticker, o] of Object.entries(optionsBag)) {
2987
+ const meta = Options.parseTicker(ticker);
2988
+ if (!meta) continue;
2989
+
2990
+ const blocksToExp = Math.max(0, Number(meta.expiryBlock || 0) - Number(currentBlockHeight || 0));
2991
+ const daysToExpiry = blocksToExp / bpd;
2992
+
2993
+ // qty is signed: >0 long options, <0 short options
2994
+ const qty = Number(o.contracts || 0);
2995
+ if (!qty) continue;
2996
+
2997
+ // MTM premium approximation (treating options as assets for equity)
2998
+ const px = Options.priceEUApprox(meta.type, Number(spot || 0), Number(meta.strike || 0), volAnnual, daysToExpiry);
2999
+ premiumMTM += px * qty;
3000
+
3001
+ // Intrinsic (floor/ceiling) can be used as an extra conservative cushion
3002
+ const iv = Options.intrinsic(meta.type, Number(meta.strike || 0), Number(spot || 0));
3003
+ intrinsicNet += iv * qty;
3004
+
3005
+ // Naked maintenance padding for shorts only (10× rule via helper)
3006
+ if (qty < 0) {
3007
+ maintNaked += Options.nakedMaintenance(meta.type, Number(meta.strike || 0), Number(spot || 0)) * Math.abs(qty);
3008
+ }
3009
+ }
3010
+
3011
+ return { premiumMTM, intrinsicNet, maintNaked };
3012
+ }
3013
+
3014
+ static async saveClearingSettlementEvent(contractId, settlementDetails, blockHeight) {
3015
+ const clearingDB = await dbInstance.getDatabase('clearing');
3016
+ const recordKey = `clearing-${contractId}-${blockHeight}`;
3017
+
3018
+ const clearingRecord = {
3019
+ _id: recordKey,
3020
+ contractId,
3021
+ settlementDetails,
3022
+ blockHeight
3023
+ };
3024
+
3025
+ try {
3026
+ await clearingDB.updateAsync(
3027
+ { _id: recordKey },
3028
+ clearingRecord,
3029
+ { upsert: true }
3030
+ );
3031
+ console.log(`Clearing settlement event record saved successfully: ${recordKey}`);
3032
+ } catch (error) {
3033
+ console.error(`Error saving clearing settlement event record: ${recordKey}`, error);
3034
+ //throw error;
3035
+ }
3036
+ }
3037
+
3038
+ static async loadClearingSettlementEvents(contractId, startBlockHeight = 0, endBlockHeight = Number.MAX_SAFE_INTEGER) {
3039
+ const clearingDB = await dbInstance.getDatabase('clearing');
3040
+ try {
3041
+ const query = {
3042
+ contractId: contractId,
3043
+ blockHeight: { $gte: startBlockHeight, $lte: endBlockHeight }
3044
+ };
3045
+ const clearingRecords = await clearingDB.findAsync(query);
3046
+ return clearingRecords.map(record => ({
3047
+ blockHeight: record.blockHeight,
3048
+ settlementDetails: record.settlementDetails
3049
+ }));
3050
+ } catch (error) {
3051
+ console.error(`Error loading clearing settlement events for contractId ${contractId}:`, error);
3052
+ //throw error;
3053
+ }
3054
+ }
3055
+
3056
+ // Implement or reference these helper methods as per your system's logic
3057
+ static calculateTotalMargin(positions) {
3058
+ let totalMargin = 0;
3059
+ positions.forEach(position => {
3060
+ totalMargin += position.margin; // Assuming each position object has a 'margin' property
3061
+ });
3062
+ return totalMargin;
3063
+ }
3064
+
3065
+ static isMarginConsistent(totalMargin) {
3066
+ const expectedMargin = this.getExpectedTotalMargin(); // Implement this method based on your system
3067
+ // You can also implement a range-based check instead of an exact value match
3068
+ return totalMargin === expectedMargin;
3069
+ }
3070
+
3071
+ static async saveFundingEvent(contractId, fundingRate, blockHeight) {
3072
+ try {
3073
+ const fundingDB = await db.getDatabase('fundingEvents');
3074
+
3075
+ const event = {
3076
+ _id: `funding-${contractId}-${blockHeight}`,
3077
+ contractId,
3078
+ fundingRate,
3079
+ blockHeight,
3080
+ timestamp: new Date().toISOString()
3081
+ };
3082
+
3083
+ await fundingDB.updateAsync({ _id: event._id }, event, { upsert: true });
3084
+
3085
+ console.log(`✅ [Funding Event Saved] Contract: ${contractId}, Block: ${blockHeight}, Rate: ${fundingRate} bps`);
3086
+ } catch (error) {
3087
+ console.error(`❌ Error saving funding event for contract ${contractId}:`, error);
3088
+ }
3089
+ }
3090
+
3091
+ static async loadFundingEvents(contractId, startBlock, endBlock) {
3092
+ try {
3093
+ const fundingDB = await db.getDatabase('fundingEvents');
3094
+
3095
+ const query = {
3096
+ contractId: contractId,
3097
+ blockHeight: { $gte: startBlock, $lte: endBlock }
3098
+ };
3099
+
3100
+ return await fundingDB.findAsync(query);
3101
+ } catch (error) {
3102
+ console.error(`❌ Error loading funding events:`, error);
3103
+ return [];
3104
+ }
3105
+ }
3106
+ // Additional helper methods or logic as required
3107
+ }
3108
+
3109
+ module.exports = Clearing;