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