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