@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,2165 @@
1
+ // Assuming the LevelDB database is stored at './path_to_margin_db'
2
+ const db = require('./db.js');
3
+ const BigNumber = require('bignumber.js')
4
+ const { v4: uuidv4 } = require('uuid');
5
+
6
+
7
+ class MarginMap {
8
+ constructor(seriesId) {
9
+ this.seriesId = seriesId;
10
+ this.margins = new Map();
11
+ if (!this.expiryIndex) this.expiryIndex = new Map();
12
+ if (!this.tickerExpiry) this.tickerExpiry = new Map();
13
+ if (!this.optionOI) this.optionOI = new Map();
14
+ }
15
+
16
+ static async getInstance(contractId) {
17
+ // Load the margin map for the given contractId from the database
18
+ // If it doesn't exist, create a new instance
19
+ const marginMap = await MarginMap.loadMarginMap(contractId);
20
+ return marginMap;
21
+ }
22
+
23
+ static async loadMarginMap(seriesId,flag=false) {
24
+ const key = JSON.stringify({ seriesId });
25
+ //console.log('loading margin map for ' + seriesId);
26
+ // Retrieve the marginMaps database from your Database instance
27
+ const marginMapsDB = await db.getDatabase('marginMaps');
28
+
29
+ try {
30
+ const doc = await marginMapsDB.findOneAsync({ _id: key });
31
+ console.log(Boolean(doc))
32
+ if (!doc) {
33
+ // Return a new instance if not found
34
+ //console.log('no MarginMap found, spinning up a fresh one');
35
+ return new MarginMap(seriesId);
36
+ }
37
+
38
+ if(flag){console.log('marginMap parsed from DB ' + JSON.stringify(doc));
39
+ return doc.value}
40
+ const map = new MarginMap(seriesId);
41
+
42
+ // Parse the value property assuming it's a JSON string
43
+ const parsedValue = JSON.parse(doc.value);
44
+ console.log(JSON.stringify(parsedValue))
45
+ if (parsedValue instanceof Array) {
46
+ // Assuming parsedValue is an array
47
+ map.margins = new Map(parsedValue);
48
+ } else {
49
+ console.error('Error parsing margin map value. Expected an array.');
50
+ }
51
+
52
+ //console.log('returning a map from the file ' + JSON.stringify(map.margins));
53
+ return map;
54
+ } catch (err) {
55
+ console.error('Error loading margin Map ' + err);
56
+ }
57
+ }
58
+
59
+ /*static async loadMarginMap(seriesId) {
60
+ const key = JSON.stringify({ seriesId});
61
+ console.log('loading margin map for '+seriesId)
62
+ // Retrieve the marginMaps database from your Database instance
63
+ const marginMapsDB = db.getDatabase('marginMaps');
64
+
65
+ try {
66
+ const doc = await marginMapsDB.findOneAsync({ _id: key });
67
+ if (!doc) {
68
+ // Return a new instance if not found
69
+ console.log('no MarginMap found, spinning up a fresh one')
70
+ return new MarginMap(seriesId);
71
+ }
72
+ console.log('marginMap parsed from DB '+JSON.stringify(doc))
73
+ var map = new MarginMap(seriesId);
74
+ map.margins = new Map(JSON.parse(doc.value));
75
+ console.log('returning a map from the file '+JSON.stringify(map))
76
+ return map;
77
+ } catch (err) {
78
+ console.log('err loading margin Map '+err)
79
+ }
80
+ }*/
81
+
82
+ /*initMargin(address, contracts, price) {
83
+ const notional = contracts * price;
84
+ const margin = notional * 0.1;
85
+
86
+ this.margins.set(address, {
87
+ contracts,
88
+ margin,
89
+ unrealizedPl: 0
90
+ });
91
+
92
+ return margin;
93
+ }*/
94
+ async getAllPositions(contractId,flag) {
95
+ let map = await MarginMap.loadMarginMap(contractId,flag);
96
+
97
+ // If the margins map is empty, attempt to reload from the database
98
+ /*if (!map.margins || map.margins.size === 0) {
99
+ //console.log(`🔄 Margins map empty for contract ${contractId}, reloading from DB...`);
100
+ map = await MarginMap.loadMarginMap(contractId); // Assuming this method exists
101
+ }*/
102
+
103
+ //console.log(`📊 Getting positions for contract ${contractId}:`, JSON.stringify([...map.margins]));
104
+
105
+ const allPositions = [];
106
+ for (const [address, position] of map.margins.entries()) {
107
+ if (!address) continue;
108
+
109
+ allPositions.push({
110
+ address: address,
111
+ contracts: position.contracts,
112
+ margin: position.margin,
113
+ unrealizedPNL: position.unrealizedPNL,
114
+ avgPrice: position.avgPrice,
115
+ liqPrice: position.liqPrice,
116
+ bankruptcyPrice: position.bankruptcyPrice,
117
+ newPosThisBlock: position.newPosThisBlock
118
+ // Add other relevant fields if necessary
119
+ });
120
+ }
121
+ return allPositions;
122
+ }
123
+
124
+ async writePositionToMap(contractId, position) {
125
+ try {
126
+ const key = JSON.stringify({ seriesId: contractId });
127
+ const marginMapsDB = await db.getDatabase('marginMaps');
128
+
129
+ // Load existing doc (if any)
130
+ const existing = await marginMapsDB.findOneAsync({ _id: key });
131
+
132
+ let map;
133
+ if (existing) {
134
+ const parsedValue = JSON.parse(existing.value);
135
+ console.log('db instance pre-write in write to position '+JSON.stringify(parsedValue))
136
+ map = new Map(parsedValue);
137
+ } else {
138
+ map = new Map();
139
+ }
140
+
141
+ // Update sender’s position
142
+ map.set(position.address, position);
143
+
144
+ // Serialize and save
145
+ const doc = {
146
+ _id: key,
147
+ value: JSON.stringify(Array.from(map.entries()))
148
+ };
149
+ console.log('doc to write over marginMap '+JSON.stringify(doc))
150
+ await marginMapsDB.updateAsync({ _id: key }, doc, { upsert: true });
151
+
152
+ console.log(`📝 Saved position for ${position.address} in contract ${contractId}`);
153
+ return true;
154
+ } catch (err) {
155
+ console.error('❌ Error writing position to map:', err);
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /*async readPosition(address,seriesId) {
161
+ const sid = Number(seriesId);
162
+ if (Number.isNaN(sid)) {
163
+ //throw new Error(`Invalid seriesId: ${seriesId}`);
164
+ }
165
+ if (!address) {
166
+ //throw new Error(`Address required`);
167
+ }
168
+ const marginDB = await db.getDatabase('marginMaps');
169
+ // 1) Try to load the single doc keyed by {"seriesId":sid}
170
+ const idKey = JSON.stringify({ seriesId: sid });
171
+ let doc = await marginDB.findOneAsync({ _id: idKey });
172
+
173
+ if (!doc) {
174
+ // fallback: get latest for this seriesId
175
+ const candidates = await marginDB.findAsync({ 'key.seriesId': sid });
176
+ if (!candidates || candidates.length === 0) return null;
177
+ candidates.sort((a, b) => (Number(b.block) || 0) - (Number(a.block) || 0));
178
+ doc = candidates[0];
179
+ }
180
+
181
+ if (!doc || !doc.value) return null;
182
+
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(doc.value);
186
+ } catch (e) {
187
+ console.error('Failed to parse marginMap value', e);
188
+ return null;
189
+ }
190
+
191
+ for (const [addr, pos] of parsed) {
192
+ if (addr === address) {
193
+ console.log('what getPositionForAddress returns '+addr +' '+JSON.stringify(pos)+' '+JSON.stringify(parsed))
194
+ return pos;
195
+ }
196
+ }
197
+ return null;
198
+ }*/
199
+
200
+ // Set initial margin for a new position in the MarginMap
201
+ async setInitialMargin(sender, contractId, totalInitialMargin, block, position) {
202
+ const BigNumber = require('bignumber.js');
203
+ if(sender=="")
204
+ console.log(
205
+ `[MarginMap.setInitialMargin] sender=${sender} contractId=${contractId} totalInitialMargin=${totalInitialMargin} block=${block}`
206
+ );
207
+
208
+ // ------------------------------------------------------------
209
+ // 0) HYDRATE SAFETY: if this instance has an empty margins map,
210
+ // load positions before we mutate + persist, otherwise we can
211
+ // overwrite the DB with a partial map (the "contracts=0 wipe").
212
+ // ------------------------------------------------------------
213
+ const hadMap = !!this.margins;
214
+ const wasEmpty = !hadMap || this.margins.size === 0;
215
+ let hydrated = false;
216
+
217
+ if (wasEmpty) {
218
+ try {
219
+ const loaded = await this.getAllPositions(contractId);
220
+ if (loaded && loaded instanceof Map) {
221
+ this.margins = loaded;
222
+ } else if (loaded && typeof loaded.entries === 'function') {
223
+ // Map-like fallback
224
+ this.margins = new Map(loaded);
225
+ } else if (!this.margins) {
226
+ this.margins = new Map();
227
+ }
228
+
229
+ hydrated = this.margins.size > 0;
230
+ console.log(
231
+ `[MarginMap.setInitialMargin] hydrate attempt: size=${this.margins.size} hydrated=${hydrated}`
232
+ );
233
+ } catch (e) {
234
+ console.warn(
235
+ `[MarginMap.setInitialMargin] hydrate FAILED: ${e && e.message ? e.message : e}`
236
+ );
237
+ if (!this.margins) this.margins = new Map();
238
+ }
239
+ }
240
+
241
+ // ------------------------------------------------------------
242
+ // 1) Resolve position
243
+ // ------------------------------------------------------------
244
+ if (!position) position = this.margins.get(sender);
245
+
246
+ console.log('[MarginMap.setInitialMargin] resolved position ' + JSON.stringify(position));
247
+
248
+ if (!position) {
249
+ position = {
250
+ contracts: 0,
251
+ margin: 0,
252
+ unrealizedPNL: 0,
253
+ avgPrice: 0,
254
+ address: sender
255
+ };
256
+ } else {
257
+ // normalize missing fields without changing names/types
258
+ if (position.contracts == null) position.contracts = 0;
259
+ if (position.margin == null) position.margin = 0;
260
+ if (position.unrealizedPNL == null) position.unrealizedPNL = 0;
261
+ if (position.avgPrice == null) position.avgPrice = 0;
262
+ if (position.address == null) position.address = sender;
263
+ }
264
+
265
+ // ------------------------------------------------------------
266
+ // 2) Apply margin
267
+ // ------------------------------------------------------------
268
+ const before = position.margin;
269
+ position.margin = new BigNumber(position.margin || 0)
270
+ .plus(totalInitialMargin || 0)
271
+ .decimalPlaces(8)
272
+ .toNumber();
273
+
274
+ console.log(
275
+ `[MarginMap.setInitialMargin] margin ${before} -> ${position.margin} (delta=${totalInitialMargin}) contracts=${position.contracts}`
276
+ );
277
+
278
+ this.margins.set(sender, position);
279
+
280
+ // ------------------------------------------------------------
281
+ // 3) Record delta
282
+ // ------------------------------------------------------------
283
+ await this.recordMarginMapDelta(
284
+ sender,
285
+ contractId,
286
+ position.contracts, // totalPosition
287
+ 0, // position delta (this fn is just margin posting)
288
+ totalInitialMargin, // margin delta
289
+ 0, // uPNL
290
+ position.avgPrice || 0,
291
+ 'initialMargin',
292
+ block
293
+ );
294
+
295
+ // ------------------------------------------------------------
296
+ // 4) Persist SAFELY:
297
+ // Only persist if we either already had a populated map,
298
+ // or we successfully hydrated it. Otherwise we risk wiping DB.
299
+ // ------------------------------------------------------------
300
+ if (!wasEmpty || hydrated) {
301
+ await this.saveMarginMap(true);
302
+ } else {
303
+ console.warn(
304
+ `[MarginMap.setInitialMargin] SKIP saveMarginMap(true): margins map not hydrated (would risk partial overwrite)`
305
+ );
306
+ }
307
+
308
+ return position;
309
+ }
310
+
311
+ /**
312
+ * Atomically replace all positions for this contract with the final versions
313
+ * produced by the clearing engine.
314
+ *
315
+ * @param {Array} finalPositions - array of fully-computed positions from cache
316
+ * @param {number} contractId
317
+ * @param {boolean} flattenZeroes - if true, zero-contract positions are omitted
318
+ */
319
+ async mergePositions(finalPositions, contractId, flattenZeroes = true) {
320
+ const newMap = new Map();
321
+
322
+ for (const pos of finalPositions) {
323
+ if (flattenZeroes) {
324
+ const iou = new BigNumber(pos.iouClaim || 0);
325
+ if ((!pos.contracts || pos.contracts === 0) && iou.isZero()) {
326
+ continue;
327
+ }
328
+ }
329
+ newMap.set(pos.address, pos);
330
+ }
331
+
332
+ // Replace the map atomically
333
+ this.margins = newMap;
334
+
335
+ // persist in DB
336
+ await this.saveMarginMap();
337
+ }
338
+
339
+
340
+ // add save/load methods
341
+ async saveMarginMap(block) {
342
+ console.log('saving margin map')
343
+ try {
344
+ const key = JSON.stringify({ seriesId: this.seriesId });
345
+ const marginMapsDB = await db.getDatabase('marginMaps');
346
+ const value = JSON.stringify([...this.margins]);
347
+ //console.log(value)
348
+ // Save the margin map to the database
349
+ await marginMapsDB.updateAsync({ _id: key }, { $set: {block: block, value: value}},{upsert: true})
350
+ //await marginMapsDB.loadDatabase();
351
+ //console.log('MarginMap saved successfully.');
352
+ } catch (err) {
353
+ console.error('Error saving MarginMap:', err);
354
+ throw err;
355
+ }
356
+ }
357
+
358
+ async updateContractBalancesWithMatch(match, channelTrade, buyerClose,flipLong,sellerClose,flipShort,block) {
359
+ console.log('updating contract balances, buyer '+JSON.stringify(match.buyerPosition)+ ' and seller '+JSON.stringify(match.sellerPosition))
360
+ console.log('with match '+JSON.stringify(match))
361
+ let buyerPosition = await this.updateContractBalances(
362
+ match.buyOrder.buyerAddress,
363
+ match.buyOrder.amount,
364
+ match.tradePrice,
365
+ match.buyOrder.sell,
366
+ match.buyerPosition,
367
+ match.inverse,
368
+ buyerClose,
369
+ flipLong,
370
+ match.buyOrder.contractId,
371
+ match.buyOrder.isLiq,
372
+ block
373
+ );
374
+
375
+ let sellerPosition = await this.updateContractBalances(
376
+ match.sellOrder.sellerAddress,
377
+ match.sellOrder.amount,
378
+ match.tradePrice,
379
+ match.sellOrder.sell,
380
+ match.sellerPosition,
381
+ match.inverse,
382
+ sellerClose,
383
+ flipShort,
384
+ match.sellOrder.contractId,
385
+ match.sellOrder.isLiq,
386
+ block
387
+ );
388
+ return {bp: buyerPosition, sp: sellerPosition}
389
+ }
390
+
391
+ async updateContractBalances(
392
+ address,
393
+ amount,
394
+ price,
395
+ isSell,
396
+ position,
397
+ inverse,
398
+ close,
399
+ flip,
400
+ contractId,
401
+ inClearing,
402
+ block,
403
+ initial
404
+ ) {
405
+ console.log('pre-liq check in update contracts ' + amount + ' ' + JSON.stringify(position));
406
+ console.log('checking isSell in match update '+isSell)
407
+ /*if (position.contracts == null) {
408
+ position.contracts = 0;
409
+ }
410
+ if (position.newPosThisBlock === undefined) {
411
+ position.newPosThisBlock = 0;
412
+ }*/
413
+
414
+ // Capture old size BEFORE contract update
415
+ const oldSize = position.contracts;
416
+
417
+ console.log(
418
+ 'inside updateContractBalances ' + close + ' ' + flip +
419
+ ' position ' + oldSize + ' avgPrice ' + position.avgPrice
420
+ );
421
+
422
+ // ----------------------------
423
+ // AVG PRICE LOGIC (correct)
424
+ // ----------------------------
425
+
426
+ // Case 1: Flip → full close + new open in opposite direction
427
+ if (flip > 0) {
428
+ position.avgPrice = price;
429
+ console.log('FLIP: avgPrice reset to', position.avgPrice);
430
+ }
431
+
432
+ // Case 2: Not a flip, not a close → open or extend
433
+ else if (close === 0) {
434
+
435
+ // New open (flat → nonflat)
436
+ if (oldSize === 0) {
437
+ position.avgPrice = price;
438
+ console.log('NEW OPEN: avgPrice =', position.avgPrice);
439
+ }
440
+
441
+ // Extend existing same-side position (weighted avg)
442
+ else if (
443
+ (oldSize > 0 && !isSell) ||
444
+ (oldSize < 0 && isSell)
445
+ ) {
446
+ console.log('Weighted avg update →', amount, price, contractId);
447
+ position.avgPrice = await this.updateAveragePrice(
448
+ position, amount, price, contractId, isSell
449
+ );
450
+ console.log('Weighted avg result → avgPrice =', position.avgPrice);
451
+ }
452
+
453
+ // Partial close (oldSize and trade sides differ)
454
+ else {
455
+ console.log('PARTIAL CLOSE: avgPrice unchanged:', position.avgPrice);
456
+ }
457
+ }
458
+
459
+ // Case 3: Explicit close trade
460
+ else {
461
+ console.log('CLOSE TRADE: avgPrice unchanged:', position.avgPrice);
462
+ }
463
+
464
+ // -----------------------------------------------------
465
+ // UPDATE CONTRACT COUNT AFTER avgPrice adjustments
466
+ // -----------------------------------------------------
467
+ console.log('position size before update ' + position.contracts);
468
+
469
+ const amountBN = new BigNumber(amount);
470
+ let newPositionSize = isSell
471
+ ? new BigNumber(oldSize).minus(amountBN).toNumber() // SELL
472
+ : new BigNumber(oldSize).plus(amountBN).toNumber(); // BUY
473
+
474
+
475
+ console.log(
476
+ 'newPositionSize ' + newPositionSize +
477
+ ' address ' + address +
478
+ ' amount ' + amount +
479
+ ' isSell ' + isSell
480
+ );
481
+
482
+ position.contracts = newPositionSize;
483
+
484
+ // -----------------------------------------------------
485
+ // NOW fetch contract info and compute liquidation
486
+ // -----------------------------------------------------
487
+ const ContractList = require('./contractRegistry.js');
488
+ const TallyMap = require('./tally.js');
489
+
490
+ const contractInfo = await ContractList.getContractInfo(contractId);
491
+ const notionalValue = contractInfo.notionalValue;
492
+ const collateralId = contractInfo.collateralPropertyId;
493
+
494
+ const balances = await TallyMap.getTally(address, collateralId);
495
+ const available = balances.available;
496
+
497
+ const isLong = position.contracts > 0;
498
+
499
+ console.log('about to calc liq for pos ' + JSON.stringify(position));
500
+ const liquidationInfo = this.calculateLiquidationPrice(
501
+ available,
502
+ position.margin,
503
+ position.contracts,
504
+ notionalValue,
505
+ inverse,
506
+ isLong,
507
+ position.avgPrice
508
+ );
509
+
510
+ console.log('liquidation info ' + JSON.stringify(liquidationInfo));
511
+
512
+ // -----------------------------------------------------
513
+ // Liquidation / reset logic
514
+ // -----------------------------------------------------
515
+ if (!liquidationInfo || position.contracts === 0) {
516
+ position.liqPrice = 0;
517
+ position.bankruptcyPrice = 0;
518
+
519
+ // If flat, avgPrice must be reset to null
520
+ if (position.contracts === 0) {
521
+ position.avgPrice = null;
522
+ }
523
+ } else {
524
+ position.liqPrice = liquidationInfo.liquidationPrice || null;
525
+ position.bankruptcyPrice = liquidationInfo.bankruptcyPrice;
526
+ }
527
+
528
+ // -----------------------------------------------------
529
+ // SAVE POSITION TO MARGIN MAP
530
+ // -----------------------------------------------------
531
+ console.log('Saving position', JSON.stringify(position));
532
+ this.margins.set(address, position);
533
+
534
+ let tag = 'updateContractBalances';
535
+ if (inClearing) {
536
+ tag = initial ? 'initialLiq' : 'liquidatingContract';
537
+ }
538
+
539
+ await this.saveMarginMap(block);
540
+
541
+ await this.recordMarginMapDelta(
542
+ address,
543
+ contractId,
544
+ newPositionSize,
545
+ amount,
546
+ 0,
547
+ 0,
548
+ 0,
549
+ tag,
550
+ block,
551
+ position.bankruptcyPrice
552
+ );
553
+
554
+ return position;
555
+ }
556
+
557
+
558
+ calculateLiquidationPrice(available, margin, contracts, notionalValue, isInverse, isLong, avgPrice,uPNL) {
559
+ console.log(available, margin, contracts, notionalValue, isInverse, isLong, avgPrice,uPNL)
560
+ const balanceBN = new BigNumber(available);
561
+ const marginBN = new BigNumber(margin);
562
+ const uPNLBN = new BigNumber(uPNL || 0);
563
+ const totalCollateralBN = balanceBN.plus(marginBN).minus(BigNumber.max(uPNLBN, 0));
564
+
565
+ const contractsBN = new BigNumber(Math.abs(contracts));
566
+ const notionalValueBN = new BigNumber(notionalValue);
567
+ const avgPriceBN = new BigNumber(avgPrice);
568
+
569
+ // For linear contracts, use your existing formulas.
570
+ const positionNotional = notionalValueBN.times(contractsBN);
571
+ let bankruptcyPriceBN = new BigNumber(0);
572
+ let liquidationPriceBN = new BigNumber(0);
573
+ const adjustment = marginBN.dividedBy(2).dividedBy(contractsBN); // This is used for linear
574
+
575
+ console.log('inside calc liq price', isInverse, isLong, 'avail and margin', available, margin);
576
+
577
+ if (!isInverse) {
578
+ // Linear contracts: existing logic
579
+ if (isLong) {
580
+ if (totalCollateralBN.isGreaterThanOrEqualTo(positionNotional.times(avgPriceBN))) {
581
+ return { bankruptcyPrice: null, liquidationPrice: null };
582
+ } else {
583
+ bankruptcyPriceBN = avgPriceBN.minus(totalCollateralBN.dividedBy(positionNotional)).times(1.005);
584
+ liquidationPriceBN = bankruptcyPriceBN.plus(adjustment);
585
+ }
586
+ } else {
587
+ bankruptcyPriceBN = avgPriceBN.plus(totalCollateralBN.dividedBy(positionNotional)).times(0.995);
588
+ liquidationPriceBN = bankruptcyPriceBN.minus(adjustment);
589
+ }
590
+ } else {
591
+ // Inverse contracts: use reciprocal PnL logic.
592
+ // Define term = (margin / 2) / (contracts * notional)
593
+ const term = marginBN.dividedBy(2).dividedBy(contractsBN.multipliedBy(notionalValueBN));
594
+
595
+ if (isLong) {
596
+ // For a long inverse position:
597
+ // 1/Pliq = 1/Pentry + term => Pliq = 1 / (1/Pentry + term)
598
+ const reciprocalLiq = new BigNumber(1).dividedBy(avgPriceBN).plus(term);
599
+ liquidationPriceBN = new BigNumber(1).dividedBy(reciprocalLiq);
600
+ // For bankruptcy, you might apply a slight multiplier:
601
+ const reciprocalBankruptcy = new BigNumber(1).dividedBy(avgPriceBN).plus(term.multipliedBy(1.005));
602
+ bankruptcyPriceBN = new BigNumber(1).dividedBy(reciprocalBankruptcy);
603
+ } else {
604
+ // For a short inverse position:
605
+ // 1/Pliq = 1/Pentry - term. If that term becomes <= 0, liquidation price is null.
606
+ const reciprocalLiq = new BigNumber(1).dividedBy(avgPriceBN).minus(term);
607
+ if (reciprocalLiq.lte(0)) {
608
+ return { bankruptcyPrice: null, liquidationPrice: null };
609
+ } else {
610
+ liquidationPriceBN = new BigNumber(1).dividedBy(reciprocalLiq);
611
+ // Bankruptcy price with a slight multiplier:
612
+ const reciprocalBankruptcy = new BigNumber(1).dividedBy(avgPriceBN).minus(term.multipliedBy(1.005));
613
+ bankruptcyPriceBN = new BigNumber(1).dividedBy(reciprocalBankruptcy);
614
+ }
615
+ }
616
+ }
617
+
618
+ let bankruptcyPrice = Math.abs(bankruptcyPriceBN.decimalPlaces(4).toNumber());
619
+ let liquidationPrice = Math.abs(liquidationPriceBN.decimalPlaces(4).toNumber());
620
+
621
+ return {
622
+ bankruptcyPrice,
623
+ liquidationPrice
624
+ };
625
+ }
626
+
627
+ async updateAveragePrice(position, amount, price, contractId, isSell) {
628
+ const oldSize = position.contracts;
629
+ const oldAvg = new BigNumber(position.avgPrice || 0);
630
+ const tradeSize = new BigNumber(amount);
631
+
632
+ // Determine new total position size
633
+ const newSize = isSell
634
+ ? new BigNumber(oldSize).minus(tradeSize)
635
+ : new BigNumber(oldSize).plus(tradeSize);
636
+ // If newSize is zero, the position is fully closed
637
+ if (newSize.isZero()) {
638
+ return null; // avgPrice resets in updateContractBalances
639
+ }
640
+
641
+ // Weighted average price formula (always absolute sizes)
642
+ const weighted = oldAvg.times(Math.abs(oldSize))
643
+ .plus(new BigNumber(price).times(amount))
644
+ .dividedBy(Math.abs(newSize))
645
+ .decimalPlaces(8);
646
+
647
+ return weighted.toNumber();
648
+ }
649
+
650
+
651
+ async moveMarginAndContractsForMint(address, propertyId, contractId, contracts, margin,block) {
652
+ // Check if the margin map exists for the given contractId
653
+ const position = this.margins.get(address);
654
+ const synthId = 's-'+propertyId+'-'+contractId
655
+
656
+ let vaultPosition = this.margins.get(synthId)
657
+ let first = false
658
+
659
+ if(!vaultPosition){
660
+ console.log('first time establishing vault on marginMap '+synthId)
661
+ first = true
662
+ vaultPosition = {contracts:0,margin:0,avgPrice:0,liqPrice:null,address:synthId}
663
+ }
664
+ // If no position exists for the propertyId, initialize a new one
665
+ if (!position) {
666
+ return console.log('error: no position found for mint with '+propertyId+' collateral and contract '+contractId)
667
+ }
668
+ let excess = 0
669
+ //we're assuming contracts is a negative number reflecting funky math in the validity function
670
+ console.log('inside moveMarginAndContractsForMint '+contracts, margin, position.margin)
671
+ // Update the existing position
672
+ position.contracts = BigNumber(position.contracts).minus(contracts).toNumber();
673
+ if(margin>position.margin){
674
+ if(Math.abs(position.contracts)>Math.abs(contracts)){
675
+ //instead of trying to calculate init/main. margin in between trade and mark prices, let's keep it simple and liquidation safer
676
+ //and just not take from margin, mmkay
677
+ excess = margin
678
+ margin = 0
679
+ }else{
680
+ excess = BigNumber(margin).minus(position.margin).decimalPlaces(8).toNumber()
681
+ margin = position.margin
682
+ }
683
+ }
684
+ let prevMargin = position.margin
685
+ position.margin = BigNumber(position.margin).minus(margin).decimalPlaces(8).toNumber();
686
+ let marginChange = BigNumber(prevMargin).minus(position.margin)
687
+ let avgDelta = 0
688
+ if(first==false){
689
+ vaultPosition.contracts = BigNumber(vaultPosition.contracts).plus(contracts).toNumber();
690
+ vaultPosition.margin = BigNumber(position.margin).plus(margin).decimalPlaces(8).toNumber();
691
+ let oldAvg = vaultPosition.avgPrice
692
+ if(!oldAvg){oldAvg=0}
693
+ vaultPosition.avgPrice = this.updatedAvgPrice(vaultPosition, amount, position.avgPrice, contractId, false)
694
+ avgDelta = vaultPosition.avgPrice-oldAvg
695
+ }else if(first==true){
696
+ vaultPosition.contracts = contracts
697
+ vaultPosition.margin = margin
698
+ vaultPosition.avgPrice = position.avgPrice
699
+ avgDelta = vaultPosition.avgPrice
700
+ }
701
+
702
+ // Save the updated position
703
+ //if(address==null){throw new Error()}
704
+ this.margins.set(address, position);
705
+ this.margins.set(synthId,vaultPosition)
706
+ await this.recordMarginMapDelta(synthId, contractId, vaultPosition.contracts, contracts, margin, 0, avgDelta, 'mintMarginAndContractsToVault');
707
+ await this.recordMarginMapDelta(propertyId, contractId, position.contracts, contracts*-1, -margin, 0, 0, 'moveMarginAndContractsForMint');
708
+ await this.saveMarginMap(block);
709
+
710
+ return {contracts, margin,excess};
711
+ }
712
+
713
+ async moveMarginAndContractsForRedeem(address, propertyId, contractId, amount, vault, notional, initMargin,mark,block) {
714
+ const position = this.margins.get(address);
715
+ const vaultPosition = this.margins.get(propertyId)
716
+ if (!position) {
717
+ //throw new Error(`No position found for redemption with ${propertyId} collateral and contract ${contractId}`);
718
+ }
719
+
720
+ let excess = 0;
721
+ let contracts = vault.contracts;
722
+ let margin = vault.margin;
723
+ let contractShort = BigNumber(amount).dividedBy(notional).toNumber();
724
+ let longClosed = 0;
725
+ let covered = 0;
726
+ let shortsAdded = 0
727
+ let transferAvg = false
728
+ let modifyAvg = false
729
+ if (position.contracts > 0 && contractShort < position.contracts) {
730
+ longClosed = contractShort;
731
+ covered = contractShort;
732
+ } else if (position.contracts > 0 && contractShort === position.contracts) {
733
+ longClosed = contractShort;
734
+ covered = contractShort;
735
+ } else if (position.contracts > 0 && contractShort > position.contracts) {
736
+ longClosed = position.contracts;
737
+ shortsAdded = BigNumber(contractShort).minus(longClosed).toNumber();
738
+ covered = longClosed;
739
+ //this is going to simply transpose the avg. price in the vault position to the user as it opens a short
740
+ transferAvg = true
741
+ } else if(position.contract<0){
742
+ //this is going to modify the avg. price as it increases the short position
743
+ modifyAvg = true
744
+ shortsAdded = contractShort
745
+ }
746
+
747
+ if(shortsAdded>0){
748
+ position.avgPrice = this.updatedAvgPrice(position, amount, vaultPosition.avgPrice, contractId, false)
749
+ }
750
+ // Adjust the contract and margin positions for redemption
751
+ position.contracts = BigNumber(position.contracts).plus(contracts).toNumber();
752
+
753
+ // Calculate pro-rata factor and margin to return
754
+ let totalOutstanding = vault.outstanding;
755
+ let proRataFactor = BigNumber(amount).dividedBy(totalOutstanding).decimalPlaces(8).toNumber();
756
+ let marginToReturn = BigNumber(vault.margin).multipliedBy(proRataFactor).decimalPlaces(8).toNumber();
757
+ let availToReturn = BigNumber(vault.available).multipliedBy(proRataFactor).decimalPlaces(8).toNumber()
758
+
759
+ let returnMargin = BigNumber(contractShort).times(initMargin).decimalPlaces(8).toNumber()
760
+ let returnAvail = BigNumber(marginToReturn).minus(returnMargin).decimalPlaces(8).toNumber()
761
+
762
+ if (notCovered > 0) {
763
+ returnAvail = marginToReturn - (marginToReturn * (notCovered / contractShort));
764
+ }
765
+
766
+ // Record any excess margin
767
+ excess = BigNumber(margin).minus(marginToReturn).decimalPlaces(8).toNumber();
768
+
769
+ // Adjust the margin position for redemption (add margin back to the position)
770
+ position.margin = BigNumber(position.margin).plus(returnMargin).decimalPlaces(8).toNumber();
771
+ if(!modifyAvg&&!transferAvg){
772
+ position.contracts = BigNumber(position.contracts).minus(longClosed).toNumber();
773
+ }
774
+ let oldAvg = position.avgPrice
775
+ let avgDelta = 0
776
+ if(!oldAvg){oldAvg=0}
777
+ if(transferAvg){
778
+ position.avgPrice=vaultPosition.avgPrice
779
+ position.contracts=BigNumber(position.contracts).minus(longClosed).minus(shortsAdded).toNumber()
780
+ }
781
+ if(modifyAvg){
782
+ position.avgPrice=this.updatedAvgPrice(position, amount, vaultPosition.avgPrice, contractId, false)
783
+ position.contracts= BigNumber(position.contracts).minus(shortsAdded).toNumber()
784
+ }
785
+ let accountingPNL =0
786
+ let reduction = 0
787
+ if(longClosed>0){
788
+ accountingPNL = await this.realizePnl(address, longClosed, vaultPosition.avgPrice, position.avgPrice, true, notional, position, false,contractId);
789
+ console.log('calculating rPNL in redeem '+accountingPNL)
790
+ reduction = await this.reduceMargin(position, longClosed, accountingPNL, true, contractId, address, false,false,0);
791
+ //somehow adding logic to realize profits on the shorts on this address as they are inherited/covered
792
+ //going to remove this edge case in validity for now then return later
793
+ }
794
+
795
+ console.log('updating margin map in redeem '+address+' '+JSON.stringify(position))
796
+ //if(address==null){throw new Error()}
797
+ this.margins.set(address, position);
798
+ await this.recordMarginMapDelta(propertyId, contractId, vault.contracts, contractShort, -returnMargin, -accountingPNL, 0, 'redeemMarginAndContractsFromVault');
799
+ await this.recordMarginMapDelta(address,contractId, position.contracts, contractShort,returnMargin, accountingPNL,0,'moveMarginAndContractsForRedeem')
800
+ await this.saveMarginMap(block);
801
+
802
+ return { contracts: contractShort, margin: marginToReturn, available: availToReturn, excess: excess, rPNL: accountingPNL, reduction:reduction };
803
+ }
804
+
805
+ /**
806
+ * Internal: update OI and expiry index when an option pos opens/closes for any address.
807
+ * beforeQty: previous signed qty for this address
808
+ * afterQty: new signed qty for this address
809
+ * tickerMeta: Options.parseTicker(ticker)
810
+ */
811
+ _touchExpiryIndex(beforeQty, afterQty, ticker, tickerMeta) {
812
+ if (!tickerMeta || !tickerMeta.expiryBlock) return;
813
+
814
+ const wasZero = (Number(beforeQty) || 0) === 0;
815
+ const isZero = (Number(afterQty) || 0) === 0;
816
+
817
+ if (wasZero && !isZero) {
818
+ // transition 0 -> nonzero: increment OI and ensure indexed
819
+ const oi = (this.optionOI.get(ticker) || 0) + 1;
820
+ this.optionOI.set(ticker, oi);
821
+
822
+ if (!this.tickerExpiry.has(ticker)) {
823
+ this.tickerExpiry.set(ticker, tickerMeta.expiryBlock);
824
+ const set = this.expiryIndex.get(tickerMeta.expiryBlock) || new Set();
825
+ set.add(ticker);
826
+ this.expiryIndex.set(tickerMeta.expiryBlock, set);
827
+ }
828
+ } else if (!wasZero && isZero) {
829
+ // transition nonzero -> 0: decrement OI; remove ticker from index if OI hits 0
830
+ const oi = Math.max(0, (this.optionOI.get(ticker) || 0) - 1);
831
+ if (oi === 0) {
832
+ this.optionOI.delete(ticker);
833
+ const exp = this.tickerExpiry.get(ticker);
834
+ if (exp != null) {
835
+ const set = this.expiryIndex.get(exp);
836
+ if (set) {
837
+ set.delete(ticker);
838
+ if (set.size === 0) this.expiryIndex.delete(exp);
839
+ else this.expiryIndex.set(exp, set);
840
+ }
841
+ }
842
+ this.tickerExpiry.delete(ticker);
843
+ } else {
844
+ this.optionOI.set(ticker, oi);
845
+ }
846
+ }
847
+ }
848
+
849
+ /**
850
+ * Return a flat array of tickers with expiry <= blockHeight.
851
+ */
852
+ async getExpiringTickersUpTo(blockHeight) {
853
+ const out = [];
854
+ for (const [exp, set] of this.expiryIndex.entries()) {
855
+ if (Number(exp) <= Number(blockHeight)) {
856
+ for (const t of set) out.push(t);
857
+ }
858
+ }
859
+ return out;
860
+ }
861
+
862
+ /**
863
+ * Remove all index entries with expiry <= blockHeight (call AFTER you process them).
864
+ */
865
+ async cleanupExpiredTickersUpTo(blockHeight) {
866
+ for (const [exp, set] of Array.from(this.expiryIndex.entries())) {
867
+ if (Number(exp) <= Number(blockHeight)) {
868
+ this.expiryIndex.delete(exp);
869
+ for (const t of set) {
870
+ this.tickerExpiry.delete(t);
871
+ this.optionOI.delete(t); // safe: OI should be zero after settlement
872
+ }
873
+ }
874
+ }
875
+ }
876
+
877
+ async cleanupFlatPositions(contractId) {
878
+ for (const [address, pos] of this.margins.entries()) {
879
+ // Only remove if truly flat and safe
880
+ if (
881
+ pos.contracts === 0 &&
882
+ (!pos.margin || pos.margin === 0) &&
883
+ (!pos.unrealizedPNL || pos.unrealizedPNL === 0) &&
884
+ new BigNumber(pos.iouClaim || 0).isZero()
885
+ ) {
886
+ this.margins.delete(address);
887
+ }
888
+ }
889
+
890
+ // Persist the pruned state
891
+ await this.saveMarginMap(true);
892
+ }
893
+
894
+
895
+
896
+ calculateMarginRequirement(contracts, price, inverse) {
897
+
898
+ // Ensure that the input values are BigNumber instances
899
+ let bnContracts = new BigNumber(contracts);
900
+ let bnPrice = new BigNumber(price);
901
+
902
+ let notional
903
+
904
+ // Calculate the notional value
905
+ if (inverse === true) {
906
+ // For inverse contracts, the notional value in denominator collateral is typically the number of contracts divided by the price
907
+ notional = bnContracts.dividedBy(bnPrice);
908
+ } else {
909
+ // For regular contracts, the notional value is the number of contracts multiplied by the price
910
+ notional = bnContracts.multipliedBy(bnPrice);
911
+ }
912
+
913
+ // Return 10% of the notional value as the margin requirement
914
+ return notional.multipliedBy(0.1).decimalPlaces(8).toNumber();
915
+ }
916
+
917
+ /**
918
+ * Checks whether the margin of a given position is below the maintenance margin.
919
+ * If so, it could trigger liquidations or other necessary actions.
920
+ * @param {string} address - The address of the position holder.
921
+ * @param {string} contractId - The ID of the contract.
922
+ */
923
+ async checkMarginMaintainance(address, contractId,position){
924
+ if(!position){
925
+ position = this.margins.get(address);
926
+ }
927
+
928
+ if (!position) {
929
+ console.error(`No position found for address ${address}`);
930
+ return;
931
+ }
932
+
933
+ const ContractRegistry = require('./contractRegistry.js')
934
+ // Calculate the maintenance margin, which is half of the initial margin
935
+ let initialMargin = await ContractRegistry.getInitialMargin(contractId, position.avgPrice);
936
+ let initialMarginBN = new BigNumber(initialMargin)
937
+ let contractsBN = new BigNumber(Math.abs(position.contracts))
938
+ let maintenanceMarginFactorBN = new BigNumber(0.5)
939
+ let maintenanceMargin = contractsBN.times(initialMarginBN).times(maintenanceMarginFactorBN).decimalPlaces(8).toNumber();
940
+ console.log('components '+initialMargin+' '+position.contracts+' '+contractId+' '+position.avgPrice)
941
+ console.log('checking maint margin '+position.margin+' '+position.unrealizedPNL+' <? '+maintenanceMargin)
942
+ if (position.margin < maintenanceMargin) {
943
+ console.log(`Margin below maintenance level for address ${address}. Initiating liquidation process.`);
944
+ // Trigger liquidation or other necessary actions here
945
+ // Example: this.triggerLiquidation(address, contractId);
946
+ return true
947
+ } else {
948
+ console.log(`Margin level is adequate for address ${address}.`);
949
+ return false
950
+ }
951
+ }
952
+
953
+ async reduceMargin(pos, contracts, initPerContract, contractId, address, side, feeDebit, fee,block) {
954
+ if (!pos) return { netMargin: new BigNumber(0), mode: 'none' };
955
+
956
+ let posMargin = new BigNumber(pos.margin);
957
+ let feeBN = new BigNumber(fee || 0);
958
+ let contractAmount = new BigNumber(contracts);
959
+ let posContracts = new BigNumber(pos.contracts);
960
+
961
+ // ✅ **Calculate Required Margin for Current Position**
962
+ let requiredMargin = posContracts.abs().times(initPerContract);
963
+ console.log('inputs to calc req margin '+initPerContract+' '+pos.contracts+' '+posContracts)
964
+ console.log(`🔎 Position: ${posContracts}, Contracts: ${contracts}, Required Margin: ${requiredMargin}`);
965
+
966
+ // 🚀 **Calculate Excess Margin**
967
+ let excessMargin = posMargin.minus(requiredMargin);
968
+
969
+ if (excessMargin.isNegative()) {
970
+ console.log(`⚠️ No excess margin to return.`);
971
+ return 0;
972
+ }
973
+
974
+ // 💸 **Deduct Fee If Needed**
975
+ if (feeDebit) {
976
+ excessMargin = excessMargin.minus(feeBN);
977
+ posMargin = posMargin.minus(feeBN);
978
+ }
979
+
980
+ // 🚨 **Ensure Margin Never Goes Below Required**
981
+ let reduction = BigNumber.max(excessMargin, 0);
982
+
983
+ // 🛠 **Apply Reduction**
984
+ posMargin = posMargin.minus(reduction);
985
+
986
+ // ✅ **Update Position & Save**
987
+ pos.margin = posMargin.decimalPlaces(8).toNumber();
988
+ reduction = reduction.decimalPlaces(8).toNumber();
989
+
990
+ console.log(`✅ Final Margin: ${pos.margin} (Reduced by ${reduction}), Required Margin: ${requiredMargin.toFixed(8)}`);
991
+
992
+ this.margins.set(pos.address, pos);
993
+ await this.recordMarginMapDelta(address, contractId, 0, 0, -reduction, 0, 0, 'marginReduction');
994
+ await this.saveMarginMap(block);
995
+
996
+ return reduction
997
+ }
998
+
999
+ async feeMarginReduce(address,pos, reduction,contractId,block){
1000
+ // Now you can use the minus method
1001
+ pos.margin = new BigNumber(pos.margin).minus(reduction).decimalPlaces(8)
1002
+ .toNumber(); // Update the margin for the existing or new position
1003
+ console.log('updating margin in fee'+pos.margin)
1004
+ this.margins.set(pos.address, pos);
1005
+ await this.recordMarginMapDelta(address, contractId, 0, 0, -reduction,0,0,'marginFeeReduction')
1006
+ //console.log('returning from reduceMargin '+reduction + ' '+JSON.stringify(pos)+ 'contractAmount '+contractAmount)
1007
+ await this.saveMarginMap(block);
1008
+ return pos;
1009
+ }
1010
+
1011
+ async realizePnl(address, contracts, price, avgPrice, isInverse, notionalValue, pos, isBuy,contractId,block){
1012
+ if (!pos) return new BigNumber(0);
1013
+
1014
+ let pnl;
1015
+ console.log('inside realizedPNL ' + address + ' ' + contracts + ' trade price ' + price + ' avg. entry ' + avgPrice + ' is inverse ' + isInverse + ' notional ' + notionalValue + ' position' + JSON.stringify(pos));
1016
+
1017
+ if(avgPrice==0||avgPrice==null||avgPrice==undefined||isNaN(avgPrice)){
1018
+ console.log('weird avg. price input for realizedPNL ' +avgPrice+' '+address+ ' '+price+' '+JSON.stringify(pos))
1019
+ }
1020
+
1021
+ const priceBN = new BigNumber(price);
1022
+ const avgPriceBN = new BigNumber(avgPrice);
1023
+ const contractsBN = new BigNumber(contracts);
1024
+ const notionalValueBN = new BigNumber(notionalValue);
1025
+
1026
+ if (isInverse) {
1027
+ let one = new BigNumber(1)
1028
+ // For inverse contracts: PnL = (1/entryPrice - 1/exitPrice) * contracts * notional
1029
+ pnl = one.dividedBy(avgPriceBN).minus(one.dividedBy(priceBN))
1030
+ .times(contractsBN)
1031
+ .times(notionalValueBN)
1032
+ //console.log('pnl ' + pnl.toNumber());
1033
+ } else {
1034
+ // For linear contracts: PnL = (exitPrice - entryPrice) * contracts * notional
1035
+ pnl = priceBN
1036
+ .minus(avgPriceBN)
1037
+ .times(contractsBN)
1038
+ .times(notionalValueBN);
1039
+ //console.log('pnl ' + pnl.toNumber());
1040
+ }
1041
+
1042
+ // Adjust the sign based on the isBuy flag
1043
+ pnl = isBuy ? pnl.times(-1) : pnl;
1044
+ pnl = pnl.decimalPlaces(8).toNumber()
1045
+ const absRPNL = Math.abs(pnl)
1046
+ const sign = pos.unrealizedPNL>0 ? 1: -1
1047
+ const signBN = new BigNumber(sign)
1048
+ // Modify the position object
1049
+ const uPNLBig = new BigNumber(Math.abs(pos.unrealizedPNL))
1050
+ pos.unrealizedPNL = uPNLBig.minus(absRPNL).times(sign).decimalPlaces(8).toNumber();
1051
+ pos.realizedPNL = pnl
1052
+ console.log('inside realizePnl ' + pnl + ' price then avgPrice ' + avgPrice + ' contracts ' + contracts + ' notionalValue ' + notionalValue);
1053
+ await this.recordMarginMapDelta(address, contractId,0,0,0,pnl,0,'rPNL')
1054
+
1055
+ return pos
1056
+ }
1057
+
1058
+ async recordMarginMapDelta(address, contractId, total, contracts, margin, uPNL, avgEntry, mode,block,mark){
1059
+ const newUuid = uuidv4();
1060
+ const dbInstance = await db.getDatabase('marginMapDelta');
1061
+ const deltaKey = `${address}-${contractId}-${newUuid}`;if (typeof contracts === 'object' && contracts.toNumber) {
1062
+ contracts = contracts.toNumber();
1063
+ }
1064
+ const delta = { address, contract: contractId, totalPosition: total, position: contracts, margin: margin, uPNL: uPNL, avgEntry, mode, block: block, lastPrice:mark};
1065
+ const ContractRegistry = require('./contractRegistry.js')
1066
+ console.log('saving marginMap delta ' + JSON.stringify(delta));
1067
+
1068
+ try {
1069
+ // Try to find an existing document based on the key
1070
+ const existingDocument = await dbInstance.findOneAsync({ _id: deltaKey });
1071
+
1072
+ if (existingDocument) {
1073
+ // If the document exists, update it
1074
+ await dbInstance.updateAsync({ _id: deltaKey }, { $set: { data: delta } });
1075
+ } else {
1076
+ // If the document doesn't exist, insert a new one
1077
+ await dbInstance.insertAsync({ _id: deltaKey, data: delta });
1078
+ }
1079
+
1080
+ ContractRegistry.setModFlag(true)
1081
+
1082
+ return; // Return success or handle as needed
1083
+ } catch (error) {
1084
+ console.error('Error saving marginMap delta:', error);
1085
+ throw error; // Rethrow the error or handle as needed
1086
+ }
1087
+ }
1088
+
1089
+
1090
+ /**
1091
+ * Allocate an IOU CLAIM for this match's imbalance delta (>0).
1092
+ * delta is the unfunded profit portion (e.g. buyerPnl + sellerPnl after loss caps).
1093
+ * We assign CLAIMS to whichever side(s) have positive pnl, proportional to their positive pnl.
1094
+ *
1095
+ * NOTE: This only mints claim amount == delta (not full pnl), so it won't double-pay.
1096
+ */
1097
+ /**
1098
+ * Allocate IOU CLAIMs for an unfunded PnL imbalance.
1099
+ *
1100
+ * @param {string} buyerAddress address of buyer in this match
1101
+ * @param {string} sellerAddress address of seller in this match
1102
+ * @param {number} buyerPnl realized PnL for buyer (can be negative)
1103
+ * @param {number} sellerPnl realized PnL for seller (can be negative)
1104
+ * @param {number} imbalanceDelta unfunded profit amount to be represented as IOUs (>0 only)
1105
+ */
1106
+ async applyIouClaimDelta(
1107
+ buyerAddress,
1108
+ sellerAddress,
1109
+ buyerPnl,
1110
+ sellerPnl,
1111
+ imbalanceDelta,
1112
+ contractId
1113
+ ) {
1114
+ const totalImbalance = new BigNumber(imbalanceDelta || 0);
1115
+ if (totalImbalance.lte(0)) return;
1116
+
1117
+ const buyerPnlBN = new BigNumber(buyerPnl || 0);
1118
+ const sellerPnlBN = new BigNumber(sellerPnl || 0);
1119
+
1120
+ const buyerPositivePnl = buyerPnlBN.gt(0) ? buyerPnlBN : new BigNumber(0);
1121
+ const sellerPositivePnl = sellerPnlBN.gt(0) ? sellerPnlBN : new BigNumber(0);
1122
+
1123
+ const totalPositivePnl = buyerPositivePnl.plus(sellerPositivePnl);
1124
+ if (totalPositivePnl.lte(0)) return;
1125
+
1126
+ const allPositions = await this.getAllPositions(contractId);
1127
+ if (!Array.isArray(allPositions)) return;
1128
+
1129
+ const findPosition = (addr) =>
1130
+ allPositions.find(p => p.address === addr) || null;
1131
+
1132
+ // Allocate buyer share
1133
+ if (buyerPositivePnl.gt(0) && buyerAddress) {
1134
+ const buyerPosition = findPosition(buyerAddress);
1135
+ if (buyerPosition) {
1136
+ const buyerShare = totalImbalance
1137
+ .times(buyerPositivePnl)
1138
+ .div(totalPositivePnl)
1139
+ .decimalPlaces(8);
1140
+
1141
+ buyerPosition.iouClaim = new BigNumber(buyerPosition.iouClaim || 0)
1142
+ .plus(buyerShare)
1143
+ .decimalPlaces(8)
1144
+ .toNumber();
1145
+ }
1146
+ }
1147
+
1148
+ // Allocate seller share
1149
+ if (sellerPositivePnl.gt(0) && sellerAddress) {
1150
+ const sellerPosition = findPosition(sellerAddress);
1151
+ if (sellerPosition) {
1152
+ const sellerShare = totalImbalance
1153
+ .times(sellerPositivePnl)
1154
+ .div(totalPositivePnl)
1155
+ .decimalPlaces(8);
1156
+
1157
+ sellerPosition.iouClaim = new BigNumber(sellerPosition.iouClaim || 0)
1158
+ .plus(sellerShare)
1159
+ .decimalPlaces(8)
1160
+ .toNumber();
1161
+ }
1162
+ }
1163
+ }
1164
+
1165
+
1166
+
1167
+ /*realizePnl(address, contracts, price, avgPrice, isInverse, notionalValue, pos) {
1168
+ //const pos = this.margins.get(address);
1169
+
1170
+ if (!pos) return 0;
1171
+
1172
+ let pnl;
1173
+ console.log('inside realizedPNL '+address + ' '+contracts + ' trade price ' +price + ' avg. entry '+avgPrice + ' is inverse '+ isInverse + ' notional '+notionalValue + ' position' +JSON.stringify(pos))
1174
+ if (isInverse) {
1175
+ // For inverse contracts: PnL = (1/entryPrice - 1/exitPrice) * contracts * notional
1176
+ pnl = (1 / avgPrice - 1 / price) * contracts * notionalValue;
1177
+ console.log('pnl '+pnl)
1178
+ } else {
1179
+ // For linear contracts: PnL = (exitPrice - entryPrice) * contracts * notional
1180
+ pnl = (price - avgPrice) * contracts * notionalValue;
1181
+ console.log('pnl '+(price - avgPrice), contracts, notionalValue, pnl)
1182
+ }
1183
+
1184
+ //pos.margin -= Math.abs(pnl);
1185
+ //pos.unrealizedPl += pnl; //be sure to modify uPNL and scoop it out for this value...
1186
+ console.log('inside realizePnl '+price + ' price then avgPrice '+avgPrice +' contracts '+contracts + ' notionalValue '+notionalValue)
1187
+ return pnl;
1188
+ }*/
1189
+
1190
+ async settlePNL(address, contracts, price, lastMark, contractId, currentBlockHeight,inverse,notional) {
1191
+ const pos = this.margins.get(address);
1192
+ console.log('pos at top of settle '+JSON.stringify(pos))
1193
+ const avgPriceBN = new BigNumber(pos.avgPrice);
1194
+ const priceBN = new BigNumber(price)
1195
+ const contractsBN = new BigNumber(contracts)
1196
+ if (!pos) return 0;
1197
+ const ContractRegistry = require('./ContractRegistry.js')
1198
+ if (notional == null || isNaN(Number(notional))) {
1199
+ // Lazy fetch from ContractRegistry
1200
+ const info = await ContractRegistry.getContractInfo(contractId);
1201
+ if (info && info.notionalValue != null) {
1202
+ notional = info.notionalValue;
1203
+ } else {
1204
+ console.log('SOMEHOW CONTRACT INFO FAILED TO LOAD in SETTLEPNL')
1205
+ // Couldn’t find it—fail this tx gracefully!
1206
+ // e.g., return 0 or set pnl=0, maybe set a params.reason if you track validation
1207
+ notional = 1; // or 0, or whatever "safe" fallback makes sense for your protocol
1208
+ }
1209
+ }
1210
+ const notionalValueBN = new BigNumber(notional)
1211
+ // Check if the contract is associated with an orac
1212
+ let pnl = 0
1213
+ if(!inverse){
1214
+ // Calculate PnL based on settlement price
1215
+ if (lastMark == null){lastMark = pos.avgPrice;}
1216
+ console.log('inside settlePNL ' +lastMark+' '+price+' '+contracts)
1217
+ pnl = new BigNumber((price - lastMark) * contracts);
1218
+ console.log('calculated settle PNL '+pnl.toNumber()+' '+JSON.stringify(pnl))
1219
+
1220
+ }else{
1221
+ let one = new BigNumber(1)
1222
+ // For inverse contracts: PnL = (1/entryPrice - 1/exitPrice) * contracts * notional
1223
+ if (lastMark == null){lastMark = pos.avgPrice;}
1224
+ console.log('inside settlePNL ' +lastMark+' '+price+' '+contracts)
1225
+ let lastMarkBN = new BigNumber(lastMark)
1226
+ console.log('settlePNL: lastMarkBN', lastMarkBN.toString(), typeof lastMarkBN)
1227
+ console.log('settlePNL: priceBN', priceBN.toString(), typeof priceBN)
1228
+ console.log('settlePNL: contractsBN', contractsBN.toString(), typeof contractsBN)
1229
+ console.log('settlePNL: notionalValueBN', notionalValueBN.toString(), typeof notionalValueBN)
1230
+
1231
+ pnl = one.dividedBy(lastMarkBN).minus(one.dividedBy(priceBN))
1232
+ .times(contractsBN)
1233
+ .times(notionalValueBN)
1234
+ console.log('calculated settle PNL '+pnl.toNumber()+' '+JSON.stringify(pnl))
1235
+ }
1236
+ // Update margin and unrealized PnL
1237
+ //pos.margin -= Math.abs(pnl);
1238
+ const uPNLBN = new BigNumber(pos.unrealizedPNL)
1239
+ pos.unrealizedPNL -= uPNLBN.minus(pnl).decimalPlaces(8).toNumber();
1240
+ console.log('pos before save in settle '+JSON.stringify(pos))
1241
+ this.margins.set(pos.address, pos)
1242
+ await this.recordMarginMapDelta(address, contractId, pos.contracts, contracts, 0, -pnl, 0, 'settlementPNL', currentBlockHeight)
1243
+
1244
+ return pnl.decimalPlaces(8).toNumber();
1245
+ }
1246
+
1247
+ /**
1248
+ * Estimate PnL between two prices for a given position.
1249
+ * Pure function: no side effects, no margin map mutations.
1250
+ *
1251
+ * @param {number} contracts - number of contracts (+long / -short)
1252
+ * @param {number} entryPrice - entry/mark price
1253
+ * @param {number} exitPrice - settlement/strike price
1254
+ * @param {boolean} inverse - contract type (true if inverse, false if linear)
1255
+ * @param {number} notional - notional value per contract
1256
+ * @returns {number} estimated PnL in collateral units
1257
+ */
1258
+ estimatePNL(contracts, entryPrice, exitPrice, inverse, notional) {
1259
+ const contractsBN = new BigNumber(contracts);
1260
+ const entryBN = new BigNumber(entryPrice);
1261
+ const exitBN = new BigNumber(exitPrice);
1262
+ const notionalBN = new BigNumber(notional || 1);
1263
+
1264
+ let pnl;
1265
+
1266
+ if (!inverse) {
1267
+ // Linear contract: PnL = (exit - entry) * contracts
1268
+ pnl = exitBN.minus(entryBN).times(contractsBN);
1269
+ } else {
1270
+ // Inverse contract:
1271
+ // PnL = (1/entry - 1/exit) * contracts * notional
1272
+ const one = new BigNumber(1);
1273
+ pnl = one.div(entryBN).minus(one.div(exitBN))
1274
+ .times(contractsBN)
1275
+ .times(notionalBN);
1276
+ }
1277
+
1278
+ return pnl.decimalPlaces(8).toNumber();
1279
+ }
1280
+
1281
+
1282
+ // === marginMap.js ===
1283
+ async applyOptionTrade(address, fullTicker, signedQty, tradePrice, blockHeight, creditMargin){
1284
+ // fetch or init the series-level blob for this address
1285
+ let pos = this.margins.get(address);
1286
+ if (!pos) {
1287
+ pos = {
1288
+ address,
1289
+ contractId: this.seriesId || null, // series scope
1290
+ contracts: 0, // perp/futures core (untouched here)
1291
+ avgPrice: 0, // perp/futures core (untouched here)
1292
+ unrealizedPNL: 0, // perp/futures core (untouched here)
1293
+ margin: 0, // series-level margin bucket (optional)
1294
+ options: {} // per-option map
1295
+ };
1296
+ }
1297
+ if (!pos.options) pos.options = {};
1298
+
1299
+ // fetch or init this specific option sub-position
1300
+ let optPos = pos.options[fullTicker] || { contracts: 0, avgPrice: 0, margin: 0 };
1301
+
1302
+ const delta = Number(signedQty) || 0;
1303
+ const px = Number(tradePrice) || 0;
1304
+ const before = Number(optPos.contracts) || 0;
1305
+ const after = before + delta;
1306
+
1307
+
1308
+ const meta = Options.parseTicker(fullTicker);
1309
+ this._touchExpiryIndex(this, before, after, fullTicker, meta);
1310
+
1311
+ // compute reduce/flip characteristics (for avgPrice update only)
1312
+ const beforeSign = Math.sign(before);
1313
+ const afterSign = Math.sign(after);
1314
+ const isFlip = (before !== 0 && delta !== 0 && beforeSign !== Math.sign(delta) && Math.abs(delta) > Math.abs(before));
1315
+
1316
+ // update avgPrice:
1317
+ // - if same sign or opening from flat → weighted average
1318
+ // - if crossing to exactly zero → avg=0
1319
+ // - if flip to opposite sign → reset avg to trade price for the leftover
1320
+ if (after === 0) {
1321
+ optPos.avgPrice = 0;
1322
+ } else if (!isFlip && (before === 0 || beforeSign === afterSign)) {
1323
+ // weighted average only when growing in same direction or opening
1324
+ const numer = (before * optPos.avgPrice) + (delta * px);
1325
+ optPos.avgPrice = numer / after;
1326
+ } else {
1327
+ // flip: new side takes the trade price as new avg
1328
+ optPos.avgPrice = px;
1329
+ }
1330
+
1331
+ // update signed contracts
1332
+ optPos.contracts = after;
1333
+
1334
+ // margin: accumulate package credit if provided (seller side handled upstream)
1335
+ optPos.margin = Number(optPos.margin || 0) + Number(creditMargin || 0);
1336
+
1337
+ // persist back
1338
+ pos.options[fullTicker] = optPos;
1339
+ this.margins.set(address, pos);
1340
+
1341
+ // record delta
1342
+ await this.recordMarginMapDelta(
1343
+ address,
1344
+ fullTicker,
1345
+ after, // position after
1346
+ delta, // contracts delta
1347
+ px, // trade price
1348
+ 0, // uPNL delta (none here)
1349
+ Number(creditMargin || 0), // margin delta
1350
+ 'optionTrade',
1351
+ blockHeight
1352
+ );
1353
+
1354
+ // return the updated sub-position (and some useful derived info if caller wants it)
1355
+ const closedQty = (before !== 0 && Math.sign(before) !== Math.sign(delta))
1356
+ ? Math.min(Math.abs(before), Math.abs(delta))
1357
+ : 0;
1358
+ const flipQty = (before !== 0 && Math.sign(before) !== Math.sign(delta) && Math.abs(delta) > Math.abs(before))
1359
+ ? (Math.abs(delta) - Math.abs(before))
1360
+ : 0;
1361
+
1362
+ return {
1363
+ contracts: optPos.contracts,
1364
+ avgPrice: optPos.avgPrice,
1365
+ margin: optPos.margin,
1366
+ closedQty,
1367
+ flipQty
1368
+ };
1369
+ }
1370
+
1371
+ /**
1372
+ * Approximate liquidation price with option protection.
1373
+ * - For LONG underlying exposure: long puts cap downside → liqPrice = min(baseLiq, maxProtectStrike)
1374
+ * - For SHORT underlying: long calls cap upside → liqPrice = max(baseLiq, minProtectStrike)
1375
+ * Assumes hybrid series blob: pos.options[ticker] exists.
1376
+ *
1377
+ * @param {string} address
1378
+ * @param {number} baseLiqPrice // your existing calcLiquidation result (perps/futures only)
1379
+ * @param {number} spot
1380
+ * @param {number} currentBlock
1381
+ * @returns {number} adjustedLiq
1382
+ */
1383
+ async calcLiquidationWithOptions(address, baseLiqPrice, spot, currentBlock) {
1384
+ const pos = this.margins.get(address) || {};
1385
+ const optionsBag = pos.options || {};
1386
+ if (!optionsBag || !Object.keys(optionsBag).length) return baseLiqPrice;
1387
+
1388
+ // Determine net underlying side from your top-level series fields
1389
+ const netContracts = Number(pos.contracts || 0); // + long, - short
1390
+ if (netContracts === 0) return baseLiqPrice;
1391
+
1392
+ let protectiveStrikes = [];
1393
+
1394
+ for (const [ticker, o] of Object.entries(optionsBag)) {
1395
+ const meta = Options.parseTicker(ticker);
1396
+ if (!meta) continue;
1397
+ const qty = Number(o.contracts || 0);
1398
+ if (qty <= 0) continue; // protection only from LONG options
1399
+ // Only options that protect the direction matter
1400
+ if (netContracts > 0 && meta.type === 'Put') protectiveStrikes.push(Number(meta.strike || 0));
1401
+ if (netContracts < 0 && meta.type === 'Call') protectiveStrikes.push(Number(meta.strike || 0));
1402
+ }
1403
+
1404
+ if (protectiveStrikes.length === 0) return baseLiqPrice;
1405
+
1406
+ if (netContracts > 0) {
1407
+ // Long underlying: puts protect → lowest liq can’t fall below the highest protective strike
1408
+ const floor = Math.max(...protectiveStrikes);
1409
+ return Math.min(baseLiqPrice, floor);
1410
+ } else {
1411
+ // Short underlying: calls protect → highest liq can’t rise above the lowest protective strike
1412
+ const cap = Math.min(...protectiveStrikes);
1413
+ return Math.max(baseLiqPrice, cap);
1414
+ }
1415
+ }
1416
+
1417
+
1418
+ async updateMargin(address, contractId, newMargin, block,position) {
1419
+ console.log(`Updating margin for ${address} on contract ${contractId} to ${newMargin}`);
1420
+
1421
+ // Ensure the position exists
1422
+ if(!position){ position = this.margins.get(address)};
1423
+
1424
+ if (!position) {
1425
+ console.warn(`No position found for ${address} on contract ${contractId}, initializing a new one.`);
1426
+ position = {
1427
+ contracts: 0,
1428
+ margin: 0,
1429
+ unrealizedPNL: 0,
1430
+ avgPrice: 0,
1431
+ };
1432
+ }
1433
+ const marginBN = new BigNumber(position.margin)
1434
+ const marginChange = new BigNumber(newMargin).plus(marginBN).decimalPlaces(8).toNumber();
1435
+ // Update the margin
1436
+ position.margin = marginChange
1437
+
1438
+ // Save the updated position
1439
+ this.margins.set(position.address, position);
1440
+
1441
+ // Record the change in margin map deltas
1442
+ await this.recordMarginMapDelta(address, contractId, position.contracts, 0, marginChange, 0, 0, 'updateMargin',block);
1443
+
1444
+ // Persist changes to the database
1445
+ await this.saveMarginMap(block);
1446
+ return position
1447
+ console.log(`Margin successfully updated for ${address} on contract ${contractId}`);
1448
+ }
1449
+
1450
+ async clear(position, address, pnlChange, avgPrice,contractId,block,markPrice,oldPrice) {
1451
+ //const position = await this.getPositionForAddress(address,contractId)
1452
+ console.log('position before clear '+JSON.stringify(position))
1453
+ if(position.unrealizedPNL==null||position.unrealizedPNL==undefined){
1454
+ position.unrealizedPNL=0
1455
+ }
1456
+ position.oldMark = oldPrice
1457
+ position.lastMark = markPrice
1458
+ const uPNLBN = new BigNumber(position.unrealizedPNL)
1459
+ position.unrealizedPNL=new BigNumber(pnlChange).plus(uPNLBN).decimalPlaces(8).toNumber()
1460
+ this.margins.set(position.address, position)
1461
+ console.log('set clearing in position '+JSON.stringify(position))
1462
+ await this.writePositionToMap(contractId, position)
1463
+
1464
+ await this.recordMarginMapDelta(address, contractId, position.contracts, 0, 0, pnlChange, avgPrice, 'markPrice',block,markPrice)
1465
+ return position
1466
+ }
1467
+
1468
+ generateLiquidationOrder(position, contractId, amount, block, lastPrice, bankruptcyPrice) {
1469
+ if (!amount || amount <= 0) return null;
1470
+ if (position.contracts === 0) return null;
1471
+
1472
+ const sell = position.contracts > 0;
1473
+
1474
+ const equity = new BigNumber(position.margin || 0)
1475
+ .plus(position.available || 0);
1476
+
1477
+ if (equity.lte(0)) return null;
1478
+
1479
+ return {
1480
+ address: position.address,
1481
+ contractId,
1482
+ amount: Math.abs(amount),
1483
+ price: bankruptcyPrice.toNumber(), // 🔑 THE boundary
1484
+ sell,
1485
+ isLiq: true,
1486
+ isMarket: true, // market-style but bounded
1487
+ blockTime: block
1488
+ };
1489
+ }
1490
+
1491
+
1492
+ async saveLiquidationOrders(contractId, position, order,reason,blockHeight,liquidationLoss,contractsDeleveraged, realizedLiquidation, delverageResults, infoBlob) {
1493
+ try {
1494
+ // Access the liquidations database
1495
+ const liquidationsDB = await db.getDatabase('liquidations');
1496
+
1497
+ // Construct the key and value for storing the liquidation orders
1498
+ const key = `liquidationOrders-${contractId}-${blockHeight}`;
1499
+ const value = {
1500
+ _id: key, // Ensure uniqueness by setting the _id field
1501
+ order: order,
1502
+ position: position,
1503
+ reason: reason,
1504
+ blockHeight: blockHeight,
1505
+ liquidationLoss: liquidationLoss,
1506
+ contractsDeleveraged: contractsDeleveraged,
1507
+ realizedLiquidation: realizedLiquidation,
1508
+ deleverage: delverageResults,
1509
+ info: infoBlob
1510
+ };
1511
+
1512
+ // Use updateAsync with upsert to insert or update the document
1513
+ await liquidationsDB.updateAsync(
1514
+ { _id: key }, // Query to find the document
1515
+ { $set: value }, // Data to set/update
1516
+ { upsert: true } // Enable upsert (insert if not found)
1517
+ );
1518
+
1519
+ console.log(`Successfully saved liquidation order for contract ${contractId} at block height ${blockHeight}`);
1520
+ } catch (error) {
1521
+ console.error(`Error saving liquidation orders for contract ${contractId} at block height ${blockHeight}:`, error);
1522
+ throw error;
1523
+ }
1524
+ }
1525
+
1526
+
1527
+ // =======================
1528
+ // simpleDeleverage - POOL-ONLY VERSION
1529
+ // - Uses vintage-aware matching
1530
+ // - Calls adjustDeleveraging() to shrink positions & move margin
1531
+ // - Returns ONLY pool distribution data (per-CP share of liquidationPool)
1532
+ // =======================
1533
+ async simpleDeleverage(
1534
+ positionCache,
1535
+ contractId,
1536
+ unfilledContracts,
1537
+ sell,
1538
+ liqPrice,
1539
+ liquidatingAddress,
1540
+ inverse,
1541
+ notional,
1542
+ block,
1543
+ markPrice,
1544
+ collateralId,
1545
+ liquidationPool // NEW (optional)
1546
+ ) {
1547
+ const BigNumber = require('bignumber.js');
1548
+ const bn = x => new BigNumber(x || 0);
1549
+ const DB = require('./db.js');
1550
+ const liquidationsDB = await DB.getDatabase('liquidations');
1551
+
1552
+
1553
+ const result = {
1554
+ contractId,
1555
+ liquidatingAddress,
1556
+ attemptedDeleverage: unfilledContracts,
1557
+ totalDeleveraged: 0,
1558
+ counterparties: [],
1559
+ poolAssignments: [] // NEW
1560
+ };
1561
+
1562
+ let remaining = bn(unfilledContracts);
1563
+
1564
+ // 1) Collect counterparties...
1565
+ let cps = positionCache
1566
+ .map((p, i) => ({ ...p, _i: i }))
1567
+ .filter(p => {
1568
+ if (p.address === liquidatingAddress) return false;
1569
+ return sell ? p.contracts < 0 : p.contracts > 0;
1570
+ });
1571
+
1572
+ console.log(`[simpleDeleverage] liquidatingAddress=${liquidatingAddress} unfilledContracts=${unfilledContracts}`);
1573
+ console.log(`[simpleDeleverage] Total positions in cache: ${positionCache.length}`);
1574
+ console.log(`[simpleDeleverage] After filter, counterparties: ${cps.length}`);
1575
+ cps.forEach((cp, i) => {
1576
+ console.log(` CP[${i}]: address=${cp.address} contracts=${cp.contracts} lastMark=${cp.lastMark}`);
1577
+ });
1578
+
1579
+ // 2) Compute mark-to-mark PnL...
1580
+ cps = cps.map(p => {
1581
+ const size = bn(p.contracts);
1582
+ const last = bn(p.lastMark);
1583
+ const mark = bn(markPrice);
1584
+
1585
+ let pnl = bn(0);
1586
+ if (last.gt(0) && mark.gt(0)) {
1587
+ if (!inverse) {
1588
+ pnl = size.times(mark.minus(last)).times(notional);
1589
+ } else {
1590
+ pnl = size.times(bn(1).div(last).minus(bn(1).div(mark)));
1591
+ }
1592
+ }
1593
+
1594
+ return { ...p, movePnl: pnl };
1595
+ });
1596
+
1597
+ // 3) Winners only
1598
+ const winners = cps.filter(p => p.movePnl.gt(0));
1599
+
1600
+ // If no winners, FALL BACK to all opposite-side cps (net-0 guarantee path)
1601
+ if (winners.length === 0) {
1602
+ // keep cps as-is (already opposite side), optionally sort by size
1603
+ cps.sort((a, b) => Math.abs(b.contracts) - Math.abs(a.contracts));
1604
+ } else {
1605
+ cps = winners;
1606
+
1607
+ // 4) Sort winners...
1608
+ cps.sort((a, b) => {
1609
+ const ap = a.movePnl.abs();
1610
+ const bp = b.movePnl.abs();
1611
+ if (!ap.eq(bp)) return bp.minus(ap).toNumber();
1612
+ return Math.abs(b.contracts) - Math.abs(a.contracts);
1613
+ });
1614
+ }
1615
+
1616
+ // 4) Sort...
1617
+ cps.sort((a, b) => {
1618
+ const ap = a.movePnl.abs();
1619
+ const bp = b.movePnl.abs();
1620
+ if (!ap.eq(bp)) return bp.minus(ap).toNumber();
1621
+ return Math.abs(b.contracts) - Math.abs(a.contracts);
1622
+ });
1623
+
1624
+ // 5) Deleveraging loop
1625
+ for (const cp of cps) {
1626
+ if (remaining.lte(0)) break;
1627
+
1628
+ const absPos = Math.abs(cp.contracts);
1629
+ if (absPos <= 0) continue;
1630
+
1631
+ const matchSize = Math.min(absPos, remaining.toNumber());
1632
+
1633
+ const updatedPos = await this.adjustDeleveraging(
1634
+ positionCache,
1635
+ cp._i,
1636
+ cp.address,
1637
+ contractId,
1638
+ matchSize,
1639
+ sell,
1640
+ block,
1641
+ liqPrice
1642
+ );
1643
+
1644
+ remaining = remaining.minus(matchSize);
1645
+ result.totalDeleveraged += matchSize;
1646
+
1647
+ await liquidationsDB.updateAsync(
1648
+ {
1649
+ _id: `adl-${contractId}-${block}-${liquidatingAddress}-${cp.address}`
1650
+ },
1651
+ {
1652
+ _id: `adl-${contractId}-${block}-${liquidatingAddress}-${cp.address}`,
1653
+ block,
1654
+ contractId,
1655
+ liquidatingAddress,
1656
+ counterparty: cp.address,
1657
+ contractsReduced: matchSize,
1658
+ contractsBefore: cp.contracts,
1659
+ contractsAfter: updatedPos?.contracts ?? 0,
1660
+ markPrice,
1661
+ liqPrice,
1662
+ reason: 'autoDeleverage'
1663
+ },
1664
+ { upsert: true }
1665
+ );
1666
+
1667
+ // NEW: capture weights for pool distribution
1668
+ result.counterparties.push({
1669
+ address: cp.address,
1670
+ matchSize,
1671
+ updatedPosition: updatedPos,
1672
+ _adlAbsPos: absPos,
1673
+ _adlMovePnl: cp.movePnl
1674
+ });
1675
+ }
1676
+
1677
+ // NEW: produce poolAssignments so handleLiquidation can credit deleveragePoolCredit
1678
+ const poolBN = bn(liquidationPool);
1679
+ if (poolBN.gt(0) && result.counterparties.length > 0) {
1680
+ // weight by per-contract pnl * matched contracts
1681
+ let denom = bn(0);
1682
+ for (const cp of result.counterparties) {
1683
+ const perContract = bn(cp._adlMovePnl).div(bn(cp._adlAbsPos || 1));
1684
+ denom = denom.plus(perContract.times(bn(cp.matchSize)));
1685
+ }
1686
+
1687
+ // fallback: just matchSize-proportional
1688
+ const totalMatched = bn(result.totalDeleveraged || 0);
1689
+
1690
+ for (const cp of result.counterparties) {
1691
+ let share = bn(0);
1692
+
1693
+ if (denom.gt(0)) {
1694
+ const perContract = bn(cp._adlMovePnl).div(bn(cp._adlAbsPos || 1));
1695
+ const w = perContract.times(bn(cp.matchSize));
1696
+ share = poolBN.times(w.div(denom));
1697
+ } else if (totalMatched.gt(0)) {
1698
+ share = poolBN.times(bn(cp.matchSize).div(totalMatched));
1699
+ }
1700
+
1701
+ result.poolAssignments.push({
1702
+ address: cp.address,
1703
+ poolShare: share.dp(8).toNumber()
1704
+ });
1705
+
1706
+ // cleanup internal fields (optional, but keeps logs sane)
1707
+ delete cp._adlAbsPos;
1708
+ delete cp._adlMovePnl;
1709
+ }
1710
+ }
1711
+
1712
+ return result;
1713
+ }
1714
+
1715
+ // =======================
1716
+ // calcPriceDelta - PnL helper
1717
+ // =======================
1718
+ calcPriceDelta(contracts, fromPrice, toPrice, isInverse, notional, isLong) {
1719
+ const c = new BigNumber(contracts);
1720
+ const f = new BigNumber(fromPrice);
1721
+ const t = new BigNumber(toPrice);
1722
+ const n = new BigNumber(notional || 1);
1723
+
1724
+ let d;
1725
+ if (isInverse) {
1726
+ d = new BigNumber(1).div(f).minus(new BigNumber(1).div(t)).times(c).times(n);
1727
+ } else {
1728
+ d = t.minus(f).times(c).times(n);
1729
+ }
1730
+
1731
+ return (isLong ? d : d.negated()).dp(8);
1732
+ }
1733
+
1734
+ // =======================
1735
+ // adjustDeleveraging
1736
+ // =======================
1737
+ async adjustDeleveraging(
1738
+ positionCache,
1739
+ index,
1740
+ address,
1741
+ contractId,
1742
+ size,
1743
+ sell,
1744
+ block,
1745
+ liqPrice
1746
+ ) {
1747
+ console.log(`Adjusting position for ${address}: reduce ${size} contracts on ${contractId}`);
1748
+
1749
+ const ContractRegistry = require('./contractRegistry.js');
1750
+ const BigNumber = require('bignumber.js');
1751
+
1752
+ let requestedSize = Number(size);
1753
+ if (!requestedSize || requestedSize <= 0) return positionCache[index];
1754
+
1755
+ // Get position from cache by index
1756
+ let position = positionCache[index];
1757
+ if (!position || !position.contracts) return position;
1758
+
1759
+ const before = Number(position.contracts);
1760
+ const maxReducible = Math.abs(before);
1761
+ if (maxReducible <= 0) return position;
1762
+
1763
+ const effectiveSize = Math.min(requestedSize, maxReducible);
1764
+
1765
+ // FIXED: Correct sign handling
1766
+ // If position is short (negative contracts) and we're deleveraging shorts,
1767
+ // we ADD contracts (make less negative)
1768
+ // If position is long (positive) and we're deleveraging longs,
1769
+ // we SUBTRACT contracts
1770
+ let contractChange;
1771
+ if (before > 0) {
1772
+ // Long position - reduce by subtracting
1773
+ contractChange = -effectiveSize;
1774
+ } else {
1775
+ // Short position - reduce by adding (making less negative)
1776
+ contractChange = effectiveSize;
1777
+ }
1778
+
1779
+ const after = before + contractChange;
1780
+
1781
+ // Handle potential sign flip - if we overshoot, clamp to 0
1782
+ if ((before > 0 && after < 0) || (before < 0 && after > 0)) {
1783
+ position.contracts = 0;
1784
+ } else {
1785
+ position.contracts = after;
1786
+ }
1787
+
1788
+ // Return margin
1789
+ const initPerContract = await ContractRegistry.getInitialMargin(contractId, liqPrice);
1790
+ const collateral = await ContractRegistry.getCollateralId(contractId);
1791
+
1792
+ let reduction = await this.reduceMargin(
1793
+ position,
1794
+ Math.abs(contractChange),
1795
+ initPerContract,
1796
+ contractId,
1797
+ address,
1798
+ sell,
1799
+ false,
1800
+ 0
1801
+ );
1802
+ const Tally=require('./tally.js')
1803
+ const hasSufficient = await Tally.hasSufficientMargin(address, collateral, reduction);
1804
+ if (!hasSufficient.hasSufficient) {
1805
+ reduction = new BigNumber(reduction)
1806
+ .minus(hasSufficient.shortfall)
1807
+ .dp(8)
1808
+ .toNumber();
1809
+ }
1810
+
1811
+ if (reduction !== 0) {
1812
+ await Tally.updateBalance(
1813
+ address,
1814
+ collateral,
1815
+ reduction,
1816
+ 0,
1817
+ -reduction,
1818
+ 0,
1819
+ 'contractDelevMarginReturn',
1820
+ block
1821
+ );
1822
+ }
1823
+
1824
+ await this.recordMarginMapDelta(
1825
+ address,
1826
+ contractId,
1827
+ after, // delta totalPosition
1828
+ contractChange, // delta position
1829
+ -reduction, // delta margin
1830
+ 0, // delta uPNL
1831
+ 0, // delta avgPrice
1832
+ 'deleverage'
1833
+ );
1834
+
1835
+ if (position.contracts === 0) {
1836
+ position.bankruptcyPrice = null;
1837
+ position.averagePrice = null;
1838
+ position.unrealizedPNL = 0;
1839
+ }
1840
+
1841
+ await this.recordMarginMapDelta(
1842
+ address,
1843
+ contractId,
1844
+ after, // delta totalPosition
1845
+ contractChange, // delta position
1846
+ -reduction, // delta margin
1847
+ 0, // delta uPNL
1848
+ 0, // delta avgPrice
1849
+ 'deleverage'
1850
+ );
1851
+
1852
+ return position;
1853
+ }
1854
+
1855
+
1856
+ async dynamicDeleverage(contractId, side, unfilledContracts, liqPrice) {
1857
+ console.log(`Starting dynamic deleveraging for contract ${contractId} at liquidation price ${liqPrice}`);
1858
+
1859
+ let remainingSize = new BigNumber(unfilledContracts);
1860
+
1861
+ // Load marginMap instance for the given contractId
1862
+ const marginMap = await MarginMap.getInstance(contractId);
1863
+
1864
+ // Fetch all positions from marginMap
1865
+ const allPositions = await marginMap.getAllPositions();
1866
+
1867
+ // Load contract details for collateral filtering
1868
+ const contractInfo = await ContractRegistry.getContractInfo(contractId);
1869
+ const collateralId = contractInfo.collateralPropertyId;
1870
+ const notionalValue = new BigNumber(contractInfo.notionalValue);
1871
+
1872
+ let potentialCounterparties = [];
1873
+
1874
+ for (let position of allPositions) {
1875
+ if (position.contracts === 0) continue; // Skip inactive positions
1876
+
1877
+ // Ensure the position belongs to the same collateral pool
1878
+ if (position.collateralId !== collateralId) continue;
1879
+
1880
+ // Fetch available and reserved balances from TallyMap
1881
+ const tally = await TallyMap.getTally(position.address, collateralId);
1882
+ const availableCollateral = new BigNumber(tally.available);
1883
+ const reservedCollateral = new BigNumber(tally.reserved);
1884
+
1885
+ // Calculate position notional value at liquidation price
1886
+ const positionNotional = notionalValue.times(Math.abs(position.contracts)).times(liqPrice);
1887
+
1888
+ // Compute net exposure by summing positions for this collateral across all contracts
1889
+ const totalExposure = await calculateNetExposure(position.address, collateralId);
1890
+
1891
+ // Ensure the side is opposite (we need shorts to absorb long liquidations and vice versa)
1892
+ const isCounterparty = side ? position.contracts < 0 : position.contracts > 0;
1893
+
1894
+ if (isCounterparty) {
1895
+ // Calculate leverage = (position notional) / (available + reserved collateral)
1896
+ const totalCollateral = availableCollateral.plus(reservedCollateral);
1897
+ const leverage = totalCollateral.isZero() ? new BigNumber(Infinity) : positionNotional.dividedBy(totalCollateral);
1898
+
1899
+ potentialCounterparties.push({
1900
+ address: position.address,
1901
+ contracts: position.contracts,
1902
+ leverage,
1903
+ exposure: totalExposure
1904
+ });
1905
+ }
1906
+ }
1907
+
1908
+ // Sort counterparties by highest leverage, then by naked exposure (descending order)
1909
+ potentialCounterparties.sort((a, b) => {
1910
+ if (!b.exposure && a.exposure) return 1; // Prefer naked positions
1911
+ if (!a.exposure && b.exposure) return -1;
1912
+ return b.leverage.minus(a.leverage).toNumber(); // Highest leverage first
1913
+ });
1914
+
1915
+ // Match positions for deleveraging
1916
+ for (let counterparty of potentialCounterparties) {
1917
+ if (remainingSize.isZero()) break;
1918
+
1919
+ let absorbAmount = new BigNumber(Math.abs(counterparty.contracts));
1920
+ let matchedAmount = BigNumber.min(remainingSize, absorbAmount);
1921
+
1922
+ console.log(`Matching ${matchedAmount} contracts to ${counterparty.address}`);
1923
+
1924
+ await executeDeleveraging(counterparty.address, contractId, matchedAmount, side, liqPrice);
1925
+
1926
+ remainingSize = remainingSize.minus(matchedAmount);
1927
+ }
1928
+
1929
+ if (!remainingSize.isZero()) {
1930
+ console.log(`WARNING: Unable to fully deleverage ${remainingSize.toString()} contracts`);
1931
+ }
1932
+ console.log(`Deleveraging complete.`);
1933
+ }
1934
+
1935
+ // Helper function to compute net exposure across all contract positions for an address
1936
+ async calculateNetExposure(address, collateralId) {
1937
+ const allContracts = await ContractRegistry.getContractsForCollateral(collateralId);
1938
+ let netExposure = new BigNumber(0);
1939
+
1940
+ for (let contract of allContracts) {
1941
+ const marginMap = await MarginMap.getInstance(contract.contractId);
1942
+ const position = await marginMap.getPositionForAddress(address, contract.contractId);
1943
+ if (position) {
1944
+ netExposure = netExposure.plus(position.contracts);
1945
+ }
1946
+ }
1947
+ return netExposure;
1948
+ }
1949
+
1950
+ // Helper function to execute deleveraging trade
1951
+ async executeDeleveraging(address, contractId, size, side, liqPrice,block) {
1952
+ console.log(`Executing deleveraging: ${address} ${size} contracts at ${liqPrice}`);
1953
+
1954
+ const marginMap = await MarginMap.getInstance(contractId);
1955
+ let position = await marginMap.getPositionForAddress(address, contractId);
1956
+
1957
+ if (!position) return;
1958
+
1959
+ position.contracts = new BigNumber(position.contracts).plus(side ? size : -size).toNumber();
1960
+
1961
+ if (position.contracts === 0) {
1962
+ position.liqPrice = null;
1963
+ position.bankruptcyPrice = null;
1964
+ }
1965
+
1966
+ marginMap.margins.set(position.address, position);
1967
+ await marginMap.saveMarginMap(block);
1968
+ }
1969
+
1970
+ async fetchLiquidationVolume(blockHeight, contractId, mark) {
1971
+ const liquidationsDB = await db.getDatabase('liquidations');
1972
+ // Fetch liquidations from the database for the given contract and blockHeight
1973
+ let liquidations = []
1974
+
1975
+ try {
1976
+ // Construct the key based on the provided structure
1977
+ const key = `liquidationOrders-${contractId}-${blockHeight}`;
1978
+
1979
+ // Find the document with the constructed key
1980
+ liquidations = await liquidationsDB.findOneAsync({ _id: key });
1981
+ } catch (error) {
1982
+ console.error('Error fetching liquidations:', error);
1983
+ }
1984
+ // Initialize BigNumber instances
1985
+ let liquidatedContracts = new BigNumber(0);
1986
+ let filledLiqContracts = new BigNumber(0);
1987
+ let bankruptcyVWAPPreFill = new BigNumber(0);
1988
+ let filledVWAP = new BigNumber(0);
1989
+ let avgBankrupcyPrice = new BigNumber(0);
1990
+ let liquidationOrders = new BigNumber(0);
1991
+ let sells = new BigNumber(0);
1992
+ let buys = new BigNumber(0);
1993
+
1994
+ // Calculate values using BigNumber
1995
+ if (liquidations && liquidations.length > 0) {
1996
+ liquidations.forEach(liquidation => {
1997
+ liquidationOrders = liquidationOrders.plus(1);
1998
+ liquidatedContracts = liquidatedContracts.plus(liquidation.contractCount);
1999
+ bankruptcyVWAPPreFill = bankruptcyVWAPPreFill.plus(new BigNumber(liquidation.size).times(new BigNumber(liquidation.bankruptcyPrice)));
2000
+ avgBankrupcyPrice = avgBankrupcyPrice.plus(new BigNumber(liquidation.bankruptcyPrice));
2001
+ if (liquidation.side == false) {
2002
+ sells = sells.plus(0);
2003
+ } else if (liquidation.side == true) {
2004
+ buys = buys.plus(0);
2005
+ }
2006
+ });
2007
+ }else{
2008
+ console.log("No liquidations found for the given criteria.");
2009
+ }
2010
+
2011
+ bankruptcyVWAPPreFill = bankruptcyVWAPPreFill.dividedBy(liquidatedContracts);
2012
+ avgBankrupcyPrice = avgBankrupcyPrice.dividedBy(liquidationOrders);
2013
+
2014
+ const tradeHistoryDB = await db.getDatabase('tradeHistory');
2015
+ const tradeKey = `liquidationOrders-${contractId}-${blockHeight}`;
2016
+ // Fetch trade history for the given blockHeight and contractId
2017
+ const trades = await tradeHistoryDB.findAsync();
2018
+
2019
+ // Count the number of liquidation orders in the trade history
2020
+ let liquidationTradeMatches = new BigNumber(0);
2021
+ trades.forEach(trade => {
2022
+ if (trade.trade.isLiq === true&&trade.blockHeight==blockHeight) {
2023
+ liquidationTradeMatches = liquidationTradeMatches.plus(1);
2024
+ filledLiqContracts = filledLiqContracts.plus(trade.trade.amount);
2025
+ filledVWAP = filledVWAP.plus(trade.trade.tradePrice);
2026
+ }
2027
+ });
2028
+ filledVWAP = filledVWAP.dividedBy(filledLiqContracts);
2029
+
2030
+ // Calculate the unfilled liquidation order contract count
2031
+ const unfilledLiquidationContracts = liquidatedContracts.minus(filledLiqContracts);
2032
+ const lossDelta = bankruptcyVWAPPreFill.minus(filledVWAP);
2033
+
2034
+ return {
2035
+ liqTotal: liquidatedContracts.toNumber(),
2036
+ liqOrders: liquidationOrders.toNumber(),
2037
+ unfilled: unfilledLiquidationContracts.toNumber(),
2038
+ bankruptcyVWAPPreFill: bankruptcyVWAPPreFill.toNumber(),
2039
+ filledVWAP: filledVWAP.toNumber(),
2040
+ lossDelta: lossDelta.toNumber()
2041
+ };
2042
+ }
2043
+
2044
+
2045
+ needsLiquidation(contract) {
2046
+ const maintenanceMarginFactor = 0.05; // Maintenance margin is 5% of the notional value
2047
+
2048
+ for (const [address, position] of Object.entries(this.margins[contract.id])) {
2049
+ const notionalValue = position.contracts * contract.marketPrice;
2050
+ const maintenanceMargin = notionalValue * maintenanceMarginFactor;
2051
+
2052
+ if (position.margin < maintenanceMargin) {
2053
+ return true; // Needs liquidation
2054
+ }
2055
+ }
2056
+ return false; // No positions require liquidation
2057
+ }
2058
+
2059
+ async getPositionForAddress(address, contractId) {
2060
+ // This expects your loader to return { margins: array }
2061
+ // where margins is like: [ [address, {contracts: ...}], ... ]
2062
+
2063
+ const map = await MarginMap.loadMarginMap(contractId);
2064
+ const arr = map.margins
2065
+
2066
+ console.log("[DEBUG] Loaded margin map addresses:", JSON.stringify(arr))
2067
+ console.log("[DEBUG] Looking for address:", address);
2068
+
2069
+ // First try exact match
2070
+ for (const [addr, pos] of arr) {
2071
+ if (addr === address) {
2072
+ if (!pos.address) pos.address = addr;
2073
+ console.log("[DEBUG] Found exact match:", addr);
2074
+ return pos;
2075
+ }
2076
+ }
2077
+
2078
+ // Fallback for Bech32 (case-insensitive)
2079
+ // Bech32-insensitive match for BTC + LTC mainnet/testnet
2080
+ if (
2081
+ address.startsWith('ltc1') || address.startsWith('tltc1') ||
2082
+ address.startsWith('bc1') || address.startsWith('tb1')
2083
+ ) {
2084
+ for (const [addr, pos] of arr) {
2085
+ const lowerAddr = addr.toLowerCase();
2086
+ const lowerInput = address.toLowerCase();
2087
+ if (lowerAddr === lowerInput) {
2088
+ console.log("[DEBUG] Found Bech32 (BTC/LTC lowercase) match:", addr);
2089
+ if (!pos.address) pos.address = addr;
2090
+ return pos;
2091
+ }
2092
+ }
2093
+ }
2094
+
2095
+
2096
+ // Still not found
2097
+ console.log("[DEBUG] Address not found in margin map for contractId", contractId);
2098
+ return { contracts: 0, margin: 0, unrealizedPNL: 0 };
2099
+ }
2100
+
2101
+ async getMarketPrice(contract) {
2102
+ let marketPrice;
2103
+
2104
+ if (ContractsRegistry.isOracleContract(contract.id)) {
2105
+ // Fetch the 3-block TWAP for oracle-based contracts
2106
+ marketPrice = await Oracles.getTwap(contract.id, 3); // Assuming the getTwap method accepts block count as an argument
2107
+ } else if (ContractsRegistry.isNativeContract(contract.id)) {
2108
+ // Fetch VWAP data for native contracts
2109
+ const contractInfo = ContractsRegistry.getContractInfo(contract.id);
2110
+ if (contractInfo && contractInfo.indexPair) {
2111
+ const [propertyId1, propertyId2] = contractInfo.indexPair;
2112
+ marketPrice = await VolumeIndex.getVwapData(propertyId1, propertyId2,3);
2113
+ }
2114
+ } else {
2115
+ throw new Error(`Unknown contract type for contract ID: ${contract.id}`);
2116
+ }
2117
+
2118
+ return marketPrice;
2119
+ }
2120
+
2121
+ async recordContractTrade(trade, blockHeight, sellerTx, buyerTx) {
2122
+ const tradeRecordKey = `contract-${trade.contractId}`;
2123
+ const tradeRecord = {
2124
+ key: tradeRecordKey,
2125
+ type: 'contract',
2126
+ trade,
2127
+ blockHeight,
2128
+ sellerTx,
2129
+ buyerTx
2130
+ };
2131
+ //console.log('saving contract trade ' +JSON.stringify(trade))
2132
+ await this.saveTrade(tradeRecord);
2133
+ }
2134
+
2135
+ async saveTrade(tradeRecord) {
2136
+ const tradeDB =await db.getDatabase('tradeHistory');
2137
+
2138
+ const uuid = uuidv4();
2139
+
2140
+ // Use the key provided in the trade record for storage
2141
+ const tradeId = `${tradeRecord.key}-${uuid}-${tradeRecord.blockHeight}`;
2142
+
2143
+ // Construct the document to be saved
2144
+ const tradeDoc = {
2145
+ _id: tradeId,
2146
+ ...tradeRecord
2147
+ };
2148
+
2149
+ // Save or update the trade record in the database
2150
+ try {
2151
+ await tradeDB.updateAsync(
2152
+ { _id: tradeId },
2153
+ tradeDoc,
2154
+ { upsert: true }
2155
+ );
2156
+ console.log(`Trade record saved successfully: ${tradeId}`);
2157
+ } catch (error) {
2158
+ //console.error(`Error saving trade record: ${tradeId}`, error);
2159
+ throw error; // Rethrow the error for handling upstream
2160
+ }
2161
+ }
2162
+
2163
+ }
2164
+
2165
+ module.exports = MarginMap