@reyaxyz/account-figures 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js ADDED
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyFill = exports.admit = exports.project = exports.projectD = exports.roundingAllowance = exports.marginText = exports.riskText = exports.parseMargin = exports.parseRisk = exports.SETTLEMENT_RESERVE = exports.INITIAL_MARGIN = exports.LIQUIDATION_THRESHOLD = exports.SETTLEMENT_RESERVE_RATE = void 0;
7
+ /**
8
+ * The matching engine's margin projection and admission verdict, ported from
9
+ * the Agent Kit's `agent_kit/scripts/_margin.py` (kit 0.12.20), which ports the
10
+ * engine's risk engine (`matching-engine/src/base/risk_engine`, revision
11
+ * 4d8ed012): `compute.rs` (`project_account_with_lots`, `finish_projection`)
12
+ * and `checks/taker_margin.rs` (`admit_taker`).
13
+ *
14
+ * All amounts are rUSD. Like the engine, cash is floored and requirements are
15
+ * rounded up to rUSD's 6 decimals per component, and E18 steps are reproduced
16
+ * where it rounds. Every amount in and out is a decimal string.
17
+ *
18
+ * The account figures use `project` only for the liquidation and initial
19
+ * margin, which depend on positions alone. For the engine's admission verdict
20
+ * (`admit`), the balances must be credited by the engine's own collateral
21
+ * rules, which give some collateral no credit: `buildMarginSnapshot` values
22
+ * collateral as Core does, not as the engine admits it.
23
+ */
24
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
25
+ const decimal_1 = require("./decimal");
26
+ const errors_1 = require("./errors");
27
+ /**
28
+ * The engine's V1 policy constant (`[risk] reserve_rate`), applied once to the
29
+ * notional of the order and of the account's matched-but-unsettled fills. V2
30
+ * does not publish it.
31
+ */
32
+ exports.SETTLEMENT_RESERVE_RATE = '0.01';
33
+ exports.LIQUIDATION_THRESHOLD = 'below_liquidation_threshold';
34
+ exports.INITIAL_MARGIN = 'below_initial_margin';
35
+ exports.SETTLEMENT_RESERVE = 'below_settlement_reserve';
36
+ function parseRisk(market, risk) {
37
+ return {
38
+ base: (0, decimal_1.parseDecimal)(risk.base, `${market} base`),
39
+ mark: (0, decimal_1.parseDecimal)(risk.mark, `${market} mark price`),
40
+ marginRate: (0, decimal_1.parseDecimal)(risk.marginRate, `${market} margin rate`),
41
+ entry: (0, decimal_1.parseDecimal)(risk.entry, `${market} entry price`),
42
+ };
43
+ }
44
+ exports.parseRisk = parseRisk;
45
+ function parseMargin(margin) {
46
+ const markets = new Map();
47
+ for (const [market, risk] of Object.entries(margin.markets)) {
48
+ markets.set(market, parseRisk(market, risk));
49
+ }
50
+ return {
51
+ realBalance: (0, decimal_1.parseDecimal)(margin.realBalance, 'real balance'),
52
+ marginBalance: (0, decimal_1.parseDecimal)(margin.marginBalance, 'margin balance'),
53
+ markets,
54
+ };
55
+ }
56
+ exports.parseMargin = parseMargin;
57
+ function riskText(risk) {
58
+ return {
59
+ base: (0, decimal_1.toText)(risk.base),
60
+ mark: (0, decimal_1.toText)(risk.mark),
61
+ marginRate: (0, decimal_1.toText)(risk.marginRate),
62
+ entry: (0, decimal_1.toText)(risk.entry),
63
+ };
64
+ }
65
+ exports.riskText = riskText;
66
+ function marginText(margin) {
67
+ const markets = {};
68
+ for (const [market, risk] of margin.markets) {
69
+ markets[market] = riskText(risk);
70
+ }
71
+ return {
72
+ realBalance: (0, decimal_1.toText)(margin.realBalance),
73
+ marginBalance: (0, decimal_1.toText)(margin.marginBalance),
74
+ markets,
75
+ };
76
+ }
77
+ exports.marginText = marginText;
78
+ function parseLot(lot) {
79
+ return {
80
+ market: lot.market,
81
+ qty: (0, decimal_1.parseDecimal)(lot.qty, `${lot.market} lot qty`),
82
+ price: (0, decimal_1.parseDecimal)(lot.price, `${lot.market} lot price`),
83
+ };
84
+ }
85
+ function parseProjection(p) {
86
+ return {
87
+ mbFloor: (0, decimal_1.parseDecimal)(p.mbFloor, 'mbFloor'),
88
+ rbFloor: (0, decimal_1.parseDecimal)(p.rbFloor, 'rbFloor'),
89
+ lm: (0, decimal_1.parseDecimal)(p.lm, 'lm'),
90
+ im: (0, decimal_1.parseDecimal)(p.im, 'im'),
91
+ reserve: (0, decimal_1.parseDecimal)(p.reserve, 'reserve'),
92
+ h: (0, decimal_1.parseDecimal)(p.h, 'h'),
93
+ c: (0, decimal_1.parseDecimal)(p.c, 'c'),
94
+ };
95
+ }
96
+ function projectionText(p) {
97
+ return {
98
+ mbFloor: (0, decimal_1.toText)(p.mbFloor),
99
+ rbFloor: (0, decimal_1.toText)(p.rbFloor),
100
+ lm: (0, decimal_1.toText)(p.lm),
101
+ im: (0, decimal_1.toText)(p.im),
102
+ reserve: (0, decimal_1.toText)(p.reserve),
103
+ h: (0, decimal_1.toText)(p.h),
104
+ c: (0, decimal_1.toText)(p.c),
105
+ };
106
+ }
107
+ // --------------------------------------------------------------------------
108
+ // Projection
109
+ // --------------------------------------------------------------------------
110
+ class Fold {
111
+ constructor(buys = decimal_1.ZERO,
112
+ /** Summed signed, so never positive. */
113
+ sells = decimal_1.ZERO, instantLoss = decimal_1.ZERO, reserveNotional = decimal_1.ZERO, transitions = 0) {
114
+ this.buys = buys;
115
+ this.sells = sells;
116
+ this.instantLoss = instantLoss;
117
+ this.reserveNotional = reserveNotional;
118
+ this.transitions = transitions;
119
+ }
120
+ add(lot, mark, unit) {
121
+ if (lot.qty.gt(0)) {
122
+ this.buys = this.buys.plus(lot.qty);
123
+ }
124
+ else {
125
+ this.sells = this.sells.plus(lot.qty);
126
+ }
127
+ // A fill priced worse than the mark loses at once; a better one is never credited.
128
+ this.instantLoss = this.instantLoss.plus((0, decimal_1.floorTo)((0, decimal_1.minOf)((0, decimal_1.floorTo)(lot.qty.times(mark.minus(lot.price))), decimal_1.ZERO), unit));
129
+ this.reserveNotional = this.reserveNotional.plus((0, decimal_1.floorTo)(lot.qty.abs().times(lot.price)));
130
+ if (!lot.qty.isZero()) {
131
+ this.transitions += 1;
132
+ }
133
+ }
134
+ merged(other) {
135
+ return new Fold(this.buys.plus(other.buys), this.sells.plus(other.sells), this.instantLoss.plus(other.instantLoss), this.reserveNotional.plus(other.reserveNotional), this.transitions + other.transitions);
136
+ }
137
+ }
138
+ function roundingAllowanceD(worstBase, transitions, decimals) {
139
+ if (transitions === 0) {
140
+ return decimal_1.ZERO;
141
+ }
142
+ const drift = worstBase.integerValue(bignumber_js_1.default.ROUND_CEIL).plus(2).times(decimal_1.E18);
143
+ return (0, decimal_1.ceilTo)(drift, (0, decimal_1.nativeUnit)(decimals)).times(transitions);
144
+ }
145
+ /**
146
+ * Core's average-entry and PnL truncation bound: one native unit per
147
+ * transition for any realistic size.
148
+ */
149
+ function roundingAllowance(worstBase, transitions, decimals = decimal_1.RUSD_DECIMALS) {
150
+ return (0, decimal_1.toText)(roundingAllowanceD((0, decimal_1.parseDecimal)(worstBase, 'worst base'), transitions, decimals));
151
+ }
152
+ exports.roundingAllowance = roundingAllowance;
153
+ function projectD(margin, imMultiplier, candidates = [], inFlight = [], reserveRate = new decimal_1.Dec(exports.SETTLEMENT_RESERVE_RATE), decimals = decimal_1.RUSD_DECIMALS) {
154
+ const unit = (0, decimal_1.nativeUnit)(decimals);
155
+ let mbFloor = margin.marginBalance;
156
+ let rbFloor = margin.realBalance;
157
+ let lm = decimal_1.ZERO;
158
+ let reserveNotional = decimal_1.ZERO;
159
+ const names = [
160
+ ...new Set([
161
+ ...margin.markets.keys(),
162
+ ...inFlight.map((lot) => lot.market),
163
+ ...candidates.map((lot) => lot.market),
164
+ ]),
165
+ ].sort();
166
+ for (const market of names) {
167
+ const risk = margin.markets.get(market);
168
+ if (risk === undefined) {
169
+ throw new errors_1.AccountFiguresInputError(`No mark or margin rate for ${market}`);
170
+ }
171
+ const optional = new Fold();
172
+ const candidate = new Fold();
173
+ for (const lot of inFlight) {
174
+ if (lot.market === market) {
175
+ optional.add(lot, risk.mark, unit);
176
+ }
177
+ }
178
+ let candidateQty = decimal_1.ZERO;
179
+ for (const lot of candidates) {
180
+ if (lot.market === market) {
181
+ candidate.add(lot, risk.mark, unit);
182
+ candidateQty = candidateQty.plus(lot.qty);
183
+ }
184
+ }
185
+ const possible = optional.merged(candidate);
186
+ const settled = risk.base;
187
+ const worst = (0, decimal_1.maxOf)(settled.abs(), settled.plus(possible.buys).abs(), settled.plus(possible.sells).abs());
188
+ const delta = possible.instantLoss.minus(roundingAllowanceD(worst, (settled.isZero() ? 0 : 1) + possible.transitions, decimals));
189
+ mbFloor = mbFloor.plus(delta);
190
+ rbFloor = rbFloor.plus(delta);
191
+ // Exposure is the worst settle/bust corner of the unsettled lots, shifted by the candidate.
192
+ const shifted = settled.plus(candidateQty);
193
+ const exposureBase = (0, decimal_1.maxOf)(shifted.abs(), shifted.plus(optional.buys).abs(), shifted.plus(optional.sells).abs());
194
+ lm = lm.plus((0, decimal_1.ceilTo)((0, decimal_1.ceilTo)(risk.marginRate.times((0, decimal_1.ceilTo)(exposureBase.times(risk.mark)))), unit));
195
+ if (!settled.isZero()) {
196
+ // Closing part of the position realizes its loss at mark into the real balance.
197
+ const opposite = settled.gt(0) ? possible.sells.negated() : possible.buys;
198
+ const closable = (0, decimal_1.minOf)(settled.abs(), opposite);
199
+ const signed = settled.gt(0) ? closable : closable.negated();
200
+ rbFloor = rbFloor.plus((0, decimal_1.floorTo)((0, decimal_1.minOf)((0, decimal_1.floorTo)(signed.times(risk.mark.minus(risk.entry))), decimal_1.ZERO), unit));
201
+ }
202
+ reserveNotional = reserveNotional.plus(possible.reserveNotional);
203
+ }
204
+ const im = (0, decimal_1.ceilTo)((0, decimal_1.ceilTo)(imMultiplier.times(lm)), unit);
205
+ const reserve = (0, decimal_1.ceilTo)(reserveRate.times(reserveNotional), unit);
206
+ const h = (0, decimal_1.minOf)(mbFloor.minus(lm), rbFloor);
207
+ return { mbFloor, rbFloor, lm, im, reserve, h, c: h.minus(reserve) };
208
+ }
209
+ exports.projectD = projectD;
210
+ /**
211
+ * The engine's `project_account_with_lots`: settled state plus unsettled lots
212
+ * plus candidates. With no lots it is the settled account, whose `lm` and `im`
213
+ * are the account's LMR and IMR.
214
+ */
215
+ function project(margin, candidates, options) {
216
+ return projectionText(projectD(parseMargin(margin), (0, decimal_1.parseDecimal)(options.imMultiplier, 'IM multiplier'), candidates.map(parseLot), (options.inFlight ?? []).map(parseLot), (0, decimal_1.parseDecimal)(options.reserveRate ?? exports.SETTLEMENT_RESERVE_RATE, 'reserve rate'), options.decimals ?? decimal_1.RUSD_DECIMALS));
217
+ }
218
+ exports.project = project;
219
+ // --------------------------------------------------------------------------
220
+ // Verdict
221
+ // --------------------------------------------------------------------------
222
+ function shortfallText(delta) {
223
+ return (0, decimal_1.toText)(delta.abs());
224
+ }
225
+ /**
226
+ * The engine's `admit_taker`: the liquidation-threshold precondition for every
227
+ * request, then initial margin before and after and the settlement reserve
228
+ * after, for an ordinary one only. `strictReduceOnly` is the reduce-only flag
229
+ * or a TP/SL; an order that merely opposes the position is ordinary. The
230
+ * messages are the engine's own.
231
+ */
232
+ function admit(before, after, strictReduceOnly) {
233
+ const p0 = parseProjection(before);
234
+ const pq = parseProjection(after);
235
+ if (p0.h.lt(0)) {
236
+ return {
237
+ code: exports.LIQUIDATION_THRESHOLD,
238
+ errorCode: 'ACCOUNT_BELOW_LIQUIDATION_MARGIN_ERROR',
239
+ shortfall: (0, decimal_1.toText)(p0.h.negated()),
240
+ engineMessage: `This account is ${shortfallText(p0.h)} rUSD below its liquidation safety threshold. ` +
241
+ 'Add collateral before placing another order.',
242
+ };
243
+ }
244
+ if (strictReduceOnly) {
245
+ return null;
246
+ }
247
+ for (const projection of [p0, pq]) {
248
+ if (projection.mbFloor.lt(projection.im)) {
249
+ const delta = projection.mbFloor.minus(projection.im);
250
+ return {
251
+ code: exports.INITIAL_MARGIN,
252
+ errorCode: 'ACCOUNT_BELOW_INITIAL_MARGIN_ERROR',
253
+ shortfall: (0, decimal_1.toText)(delta.negated()),
254
+ engineMessage: `Insufficient margin: the account is ${shortfallText(delta)} rUSD below the initial margin ` +
255
+ 'requirement for this order. Add collateral, reduce the order size, or submit a reduce-only order.',
256
+ };
257
+ }
258
+ }
259
+ if (pq.h.lt(pq.reserve)) {
260
+ const delta = pq.h.minus(pq.reserve);
261
+ return {
262
+ code: exports.SETTLEMENT_RESERVE,
263
+ errorCode: 'ACCOUNT_BELOW_INITIAL_MARGIN_ERROR',
264
+ shortfall: (0, decimal_1.toText)(delta.negated()),
265
+ engineMessage: `Insufficient margin: this order would leave the account ${shortfallText(delta)} rUSD below its ` +
266
+ 'settlement reserve. Reduce the order size or add collateral.',
267
+ };
268
+ }
269
+ return null;
270
+ }
271
+ exports.admit = admit;
272
+ // --------------------------------------------------------------------------
273
+ // A fill applied the way Core updates a position
274
+ // --------------------------------------------------------------------------
275
+ /** The account after `lot` fills in full at its price, settled the way Core updates a position. */
276
+ function applyFill(margin, lot, fee = '0') {
277
+ const parsed = parseMargin(margin);
278
+ const fill = parseLot(lot);
279
+ const feeD = (0, decimal_1.parseDecimal)(fee, 'fee');
280
+ const risk = parsed.markets.get(fill.market);
281
+ if (risk === undefined) {
282
+ throw new errors_1.AccountFiguresInputError(`No mark or margin rate for ${fill.market}`);
283
+ }
284
+ const base = risk.base;
285
+ let entry = risk.entry;
286
+ let realized = decimal_1.ZERO;
287
+ const total = base.plus(fill.qty);
288
+ if (base.isZero() || base.times(fill.qty).gt(0)) {
289
+ if (total.isZero()) {
290
+ throw new errors_1.AccountFiguresInputError(`A fill on a flat ${fill.market} position needs a non-zero qty`);
291
+ }
292
+ entry = base.times(entry).plus(fill.qty.times(fill.price)).div(total);
293
+ }
294
+ else if (base.times(total).gte(0)) {
295
+ realized = fill.qty.negated().times(fill.price.minus(entry));
296
+ }
297
+ else {
298
+ realized = base.times(fill.price.minus(entry));
299
+ entry = fill.price;
300
+ }
301
+ if (total.isZero()) {
302
+ entry = risk.mark;
303
+ }
304
+ const oldUpnl = base.times(risk.mark.minus(risk.entry));
305
+ const newUpnl = total.times(risk.mark.minus(entry));
306
+ const markets = new Map(parsed.markets);
307
+ markets.set(fill.market, { ...risk, base: total, entry });
308
+ return marginText({
309
+ realBalance: parsed.realBalance.plus(realized).minus(feeD),
310
+ marginBalance: parsed.marginBalance
311
+ .plus(realized)
312
+ .minus(feeD)
313
+ .minus(oldUpnl)
314
+ .plus(newUpnl),
315
+ markets,
316
+ });
317
+ }
318
+ exports.applyFill = applyFill;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Input the figures cannot be computed from: a missing or non-numeric price,
3
+ * market, asset definition or margin parameter. Missing data is never counted
4
+ * as zero, since that would produce wrong risk numbers.
5
+ */
6
+ export declare class AccountFiguresInputError extends Error {
7
+ constructor(message: string);
8
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccountFiguresInputError = void 0;
4
+ /**
5
+ * Input the figures cannot be computed from: a missing or non-numeric price,
6
+ * market, asset definition or margin parameter. Missing data is never counted
7
+ * as zero, since that would produce wrong risk numbers.
8
+ */
9
+ class AccountFiguresInputError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'AccountFiguresInputError';
13
+ }
14
+ }
15
+ exports.AccountFiguresInputError = AccountFiguresInputError;
@@ -0,0 +1,81 @@
1
+ import { CollateralRow, MarginInputs, MarginSnapshot, PositionDetail } from './snapshot';
2
+ /** What the figures leave out, for a reader of them. */
3
+ export declare const MARGIN_ASSUMPTIONS: readonly string[];
4
+ export interface PositionRisk {
5
+ /** This position's own liquidation margin requirement, rounded up to 6 dp. */
6
+ lmr: string;
7
+ /**
8
+ * The liquidation price on the tick, rounded toward the mark. `null` when no
9
+ * positive price is the liquidation line.
10
+ */
11
+ liquidationPrice: string | null;
12
+ /** The same price before rounding onto the tick. */
13
+ liquidationPriceExact: string | null;
14
+ /**
15
+ * How far the mark must move against the position to reach the liquidation
16
+ * price, as a fraction of the mark (from the exact price). Negative when the
17
+ * account is already past the line.
18
+ */
19
+ distanceToLiquidation: string | null;
20
+ /**
21
+ * The account holds non-stable collateral whose price follows this
22
+ * position's market (WETH or wstETH with an ETH perp, BTC with a BTC perp).
23
+ * The liquidation price holds that collateral's price fixed, which it is
24
+ * not: for a long, the collateral falls with the mark and liquidation comes
25
+ * sooner than shown.
26
+ */
27
+ assumesFixedCollateral: boolean;
28
+ }
29
+ export interface PositionFigures extends PositionDetail, PositionRisk {
30
+ /** |qty| × mark. */
31
+ notional: string;
32
+ /** s, the market's liquidation-margin parameter. */
33
+ liquidationMarginRate: string;
34
+ }
35
+ export interface AccountFigures {
36
+ accountId: number | string;
37
+ /**
38
+ * Margin balance: collateral as Core values it (price × (1 − haircut), a
39
+ * debt at price / (1 − haircut)) + unrealised PnL at mark. Realised PnL and
40
+ * funding are already in V2's rUSD balance.
41
+ */
42
+ equity: string;
43
+ /** Collateral as Core values it, without unrealised PnL. */
44
+ realBalance: string;
45
+ unrealizedPnl: string;
46
+ /** Liquidation margin requirement (LMR): the account is liquidated when equity falls below it. */
47
+ lmr: string;
48
+ /** Initial margin requirement (IMR): new exposure needs equity at or above it. */
49
+ imr: string;
50
+ imMultiplier: string;
51
+ /**
52
+ * LMR / margin balance (`equity`): what the app calls "Cross margin ratio".
53
+ * The figure liquidation depends on; 1 is liquidation. `null` when equity
54
+ * is not positive.
55
+ */
56
+ liquidationMarginUsage: string | null;
57
+ /**
58
+ * IMR / margin balance (`equity`): what the app calls "Cross margin usage".
59
+ * At 1 no new position fits; liquidation comes later, at
60
+ * `liquidationMarginUsage` 1. `null` when equity is not positive.
61
+ */
62
+ initialMarginUsage: string | null;
63
+ /** equity < LMR. */
64
+ liquidatable: boolean;
65
+ /** equity − LMR. */
66
+ marginBuffer: string;
67
+ /** equity − IMR: the room for new exposure (negative when below the IMR). */
68
+ availableMargin: string;
69
+ collateral: CollateralRow[];
70
+ positions: PositionFigures[];
71
+ }
72
+ /**
73
+ * Liquidation margin, liquidation price and distance for one held market, on
74
+ * the settled account. `lmr` is the account's LMR when the caller already has
75
+ * it.
76
+ */
77
+ export declare function positionRisk(snapshot: MarginSnapshot, symbol: string, lmr?: string): PositionRisk;
78
+ /** Account-level equity, requirements, usage and buffers, and each position's figures. */
79
+ export declare function accountFigures(snapshot: MarginSnapshot): AccountFigures;
80
+ /** One account's figures straight from V2 reads. */
81
+ export declare function computeAccountFigures(inputs: MarginInputs): AccountFigures;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeAccountFigures = exports.accountFigures = exports.positionRisk = exports.MARGIN_ASSUMPTIONS = void 0;
4
+ /**
5
+ * Account-level and per-position figures on the settled account. The shape
6
+ * follows the Agent Kit's `account_figures` and `position_risk` (kit 0.12.20);
7
+ * the inputs are valued as Core values them (see `snapshot.ts`). Every amount
8
+ * is exact; `format.ts` rounds for display.
9
+ */
10
+ const decimal_1 = require("./decimal");
11
+ const engine_1 = require("./engine");
12
+ const liquidation_1 = require("./liquidation");
13
+ const snapshot_1 = require("./snapshot");
14
+ const errors_1 = require("./errors");
15
+ /** What the figures leave out, for a reader of them. */
16
+ exports.MARGIN_ASSUMPTIONS = [
17
+ 'An estimate from public V2 data of the margin Core computes; Core can differ slightly.',
18
+ 'Equity (margin balance) = each collateral at its oracle price × (1 − haircut), a debt at price / (1 − haircut), ' +
19
+ "+ unrealized PnL at mark. The USDC balance already includes realized PnL, funding and ADL cashflows up to V2's latest market trackers.",
20
+ 'Liquidation happens below the liquidation margin; liquidation margin usage = liquidation margin / equity (1 = liquidation).',
21
+ 'Liquidation price per position: cross margin, every other mark, collateral price and funding unchanged. ' +
22
+ "It errs when a collateral's price follows the position's market (assumesFixedCollateral).",
23
+ 'Not visible: matched fills that have not settled yet (seconds), and funding since the latest market trackers.',
24
+ ];
25
+ function settled(snapshot) {
26
+ const margin = (0, engine_1.parseMargin)(snapshot.margin);
27
+ const projection = (0, engine_1.projectD)(margin, (0, decimal_1.parseDecimal)(snapshot.imMultiplier, 'IM multiplier'));
28
+ return { margin, projection };
29
+ }
30
+ /** Whether a held collateral's price follows `symbol`'s market (rUSD never does). */
31
+ function collateralFollows(snapshot, symbol) {
32
+ const market = (0, snapshot_1.marketUnderlying)(symbol);
33
+ return snapshot.collateral.some((c) => c.asset !== snapshot_1.RUSD &&
34
+ !(0, decimal_1.parseDecimal)(c.balance, `${c.asset} balance`).isZero() &&
35
+ (0, snapshot_1.collateralUnderlying)(c.asset) === market);
36
+ }
37
+ /**
38
+ * Liquidation margin, liquidation price and distance for one held market, on
39
+ * the settled account. `lmr` is the account's LMR when the caller already has
40
+ * it.
41
+ */
42
+ function positionRisk(snapshot, symbol, lmr) {
43
+ const margin = (0, engine_1.parseMargin)(snapshot.margin);
44
+ const lm = lmr === undefined
45
+ ? settled(snapshot).projection.lm
46
+ : (0, decimal_1.parseDecimal)(lmr, 'liquidation margin');
47
+ const risk = margin.markets.get(symbol);
48
+ const definition = snapshot.definitions[symbol];
49
+ if (risk === undefined || definition === undefined) {
50
+ throw new errors_1.AccountFiguresInputError(`No position or market data for ${symbol}`);
51
+ }
52
+ const price = (0, liquidation_1.liquidationPriceD)(margin, symbol, lm);
53
+ const tick = (0, decimal_1.parseDecimal)(definition.tickSize, `${symbol} tick size`);
54
+ const ownLm = (0, decimal_1.ceilTo)((0, decimal_1.ceilTo)(risk.marginRate.times((0, decimal_1.ceilTo)(risk.base.abs().times(risk.mark)))), (0, decimal_1.nativeUnit)(decimal_1.RUSD_DECIMALS));
55
+ const rounded = (0, liquidation_1.roundTowardMarkD)(price, risk, tick);
56
+ const distance = (0, liquidation_1.distanceToLiquidationD)(risk, price);
57
+ return {
58
+ lmr: (0, decimal_1.toText)(ownLm),
59
+ liquidationPrice: rounded === null ? null : (0, decimal_1.toText)(rounded),
60
+ liquidationPriceExact: price === null ? null : (0, decimal_1.toText)(price),
61
+ distanceToLiquidation: distance === null ? null : (0, decimal_1.toText)(distance),
62
+ assumesFixedCollateral: collateralFollows(snapshot, symbol),
63
+ };
64
+ }
65
+ exports.positionRisk = positionRisk;
66
+ /** Account-level equity, requirements, usage and buffers, and each position's figures. */
67
+ function accountFigures(snapshot) {
68
+ const { margin, projection } = settled(snapshot);
69
+ const equity = margin.marginBalance;
70
+ const liquidationUsage = (0, liquidation_1.marginUsageD)(equity, projection.lm);
71
+ const initialUsage = (0, liquidation_1.marginUsageD)(equity, projection.im);
72
+ const lmr = (0, decimal_1.toText)(projection.lm);
73
+ const positions = snapshot.positions.map((detail) => {
74
+ const risk = margin.markets.get(detail.symbol);
75
+ if (risk === undefined) {
76
+ throw new errors_1.AccountFiguresInputError(`No position or market data for ${detail.symbol}`);
77
+ }
78
+ return {
79
+ ...detail,
80
+ notional: (0, decimal_1.toText)(risk.base.abs().times(risk.mark)),
81
+ liquidationMarginRate: (0, decimal_1.toText)(risk.marginRate),
82
+ ...positionRisk(snapshot, detail.symbol, lmr),
83
+ };
84
+ });
85
+ return {
86
+ accountId: snapshot.accountId,
87
+ equity: (0, decimal_1.toText)(equity),
88
+ realBalance: (0, decimal_1.toText)(margin.realBalance),
89
+ unrealizedPnl: snapshot.unrealizedPnl,
90
+ lmr,
91
+ imr: (0, decimal_1.toText)(projection.im),
92
+ imMultiplier: snapshot.imMultiplier,
93
+ liquidationMarginUsage: liquidationUsage === null ? null : (0, decimal_1.toText)(liquidationUsage),
94
+ initialMarginUsage: initialUsage === null ? null : (0, decimal_1.toText)(initialUsage),
95
+ liquidatable: equity.lt(projection.lm),
96
+ marginBuffer: (0, decimal_1.toText)(equity.minus(projection.lm)),
97
+ availableMargin: (0, decimal_1.toText)(equity.minus(projection.im)),
98
+ collateral: snapshot.collateral,
99
+ positions,
100
+ };
101
+ }
102
+ exports.accountFigures = accountFigures;
103
+ /** One account's figures straight from V2 reads. */
104
+ function computeAccountFigures(inputs) {
105
+ return accountFigures((0, snapshot_1.buildMarginSnapshot)(inputs));
106
+ }
107
+ exports.computeAccountFigures = computeAccountFigures;
@@ -0,0 +1,8 @@
1
+ export type Rounding = 'down' | 'up';
2
+ /** A USD amount to 2 dp, e.g. `formatUsd(equity)` or `formatUsd(lmr, 'up')`. */
3
+ export declare function formatUsd(value: string, rounding?: Rounding): string;
4
+ /**
5
+ * A ratio as a percentage to 2 dp, e.g. `formatPct(liquidationMarginUsage, 'up')` or
6
+ * `formatPct(distanceToLiquidation, 'down')`.
7
+ */
8
+ export declare function formatPct(ratio: string, rounding: Rounding): string;
package/dist/format.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.formatPct = exports.formatUsd = void 0;
7
+ /**
8
+ * Display rounding, with the kit's conventions: cash, equity, PnL and buffers
9
+ * round down; requirements and margin usage round up; the distance to
10
+ * liquidation rounds down. Each rounding errs toward the riskier reading.
11
+ */
12
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
13
+ const decimal_1 = require("./decimal");
14
+ function fixed2(value, rounding) {
15
+ const text = value.toFixed(2, rounding === 'down' ? bignumber_js_1.default.ROUND_FLOOR : bignumber_js_1.default.ROUND_CEIL);
16
+ return text === '-0.00' ? '0.00' : text;
17
+ }
18
+ /** A USD amount to 2 dp, e.g. `formatUsd(equity)` or `formatUsd(lmr, 'up')`. */
19
+ function formatUsd(value, rounding = 'down') {
20
+ return fixed2((0, decimal_1.parseDecimal)(value, 'USD amount'), rounding);
21
+ }
22
+ exports.formatUsd = formatUsd;
23
+ /**
24
+ * A ratio as a percentage to 2 dp, e.g. `formatPct(liquidationMarginUsage, 'up')` or
25
+ * `formatPct(distanceToLiquidation, 'down')`.
26
+ */
27
+ function formatPct(ratio, rounding) {
28
+ return fixed2((0, decimal_1.parseDecimal)(ratio, 'ratio').times(100), rounding);
29
+ }
30
+ exports.formatPct = formatPct;
@@ -0,0 +1,14 @@
1
+ export { AccountFiguresInputError } from './errors';
2
+ export { RUSD_DECIMALS } from './decimal';
3
+ export type { Decimalish } from './decimal';
4
+ export { admit, applyFill, INITIAL_MARGIN, LIQUIDATION_THRESHOLD, project, roundingAllowance, SETTLEMENT_RESERVE, SETTLEMENT_RESERVE_RATE, } from './engine';
5
+ export type { AccountMargin, Lot, MarketRisk, Projection, ProjectOptions, Rejection, RejectionCode, } from './engine';
6
+ export { distanceToLiquidation, liquidationPrice, marginUsage, roundTowardMark, } from './liquidation';
7
+ export { buildMarginSnapshot, imMultiplierFrom, RUSD } from './snapshot';
8
+ export type { CollateralRow, MarginInputs, MarginSnapshot, PositionDetail, V2AccountBalance, V2AssetDefinition, V2AssetOraclePrice, V2MarketDefinition, V2MarketSummary, V2Position, } from './snapshot';
9
+ export { accountFigures, computeAccountFigures, MARGIN_ASSUMPTIONS, positionRisk, } from './figures';
10
+ export type { AccountFigures, PositionFigures, PositionRisk } from './figures';
11
+ export { formatPct, formatUsd } from './format';
12
+ export type { Rounding } from './format';
13
+ export { accountRiskResponse } from './response';
14
+ export type { AccountRiskInputs, PositionValuation } from './response';
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.accountRiskResponse = exports.formatUsd = exports.formatPct = exports.positionRisk = exports.MARGIN_ASSUMPTIONS = exports.computeAccountFigures = exports.accountFigures = exports.RUSD = exports.imMultiplierFrom = exports.buildMarginSnapshot = exports.roundTowardMark = exports.marginUsage = exports.liquidationPrice = exports.distanceToLiquidation = exports.SETTLEMENT_RESERVE_RATE = exports.SETTLEMENT_RESERVE = exports.roundingAllowance = exports.project = exports.LIQUIDATION_THRESHOLD = exports.INITIAL_MARGIN = exports.applyFill = exports.admit = exports.RUSD_DECIMALS = exports.AccountFiguresInputError = void 0;
4
+ var errors_1 = require("./errors");
5
+ Object.defineProperty(exports, "AccountFiguresInputError", { enumerable: true, get: function () { return errors_1.AccountFiguresInputError; } });
6
+ var decimal_1 = require("./decimal");
7
+ Object.defineProperty(exports, "RUSD_DECIMALS", { enumerable: true, get: function () { return decimal_1.RUSD_DECIMALS; } });
8
+ var engine_1 = require("./engine");
9
+ Object.defineProperty(exports, "admit", { enumerable: true, get: function () { return engine_1.admit; } });
10
+ Object.defineProperty(exports, "applyFill", { enumerable: true, get: function () { return engine_1.applyFill; } });
11
+ Object.defineProperty(exports, "INITIAL_MARGIN", { enumerable: true, get: function () { return engine_1.INITIAL_MARGIN; } });
12
+ Object.defineProperty(exports, "LIQUIDATION_THRESHOLD", { enumerable: true, get: function () { return engine_1.LIQUIDATION_THRESHOLD; } });
13
+ Object.defineProperty(exports, "project", { enumerable: true, get: function () { return engine_1.project; } });
14
+ Object.defineProperty(exports, "roundingAllowance", { enumerable: true, get: function () { return engine_1.roundingAllowance; } });
15
+ Object.defineProperty(exports, "SETTLEMENT_RESERVE", { enumerable: true, get: function () { return engine_1.SETTLEMENT_RESERVE; } });
16
+ Object.defineProperty(exports, "SETTLEMENT_RESERVE_RATE", { enumerable: true, get: function () { return engine_1.SETTLEMENT_RESERVE_RATE; } });
17
+ var liquidation_1 = require("./liquidation");
18
+ Object.defineProperty(exports, "distanceToLiquidation", { enumerable: true, get: function () { return liquidation_1.distanceToLiquidation; } });
19
+ Object.defineProperty(exports, "liquidationPrice", { enumerable: true, get: function () { return liquidation_1.liquidationPrice; } });
20
+ Object.defineProperty(exports, "marginUsage", { enumerable: true, get: function () { return liquidation_1.marginUsage; } });
21
+ Object.defineProperty(exports, "roundTowardMark", { enumerable: true, get: function () { return liquidation_1.roundTowardMark; } });
22
+ var snapshot_1 = require("./snapshot");
23
+ Object.defineProperty(exports, "buildMarginSnapshot", { enumerable: true, get: function () { return snapshot_1.buildMarginSnapshot; } });
24
+ Object.defineProperty(exports, "imMultiplierFrom", { enumerable: true, get: function () { return snapshot_1.imMultiplierFrom; } });
25
+ Object.defineProperty(exports, "RUSD", { enumerable: true, get: function () { return snapshot_1.RUSD; } });
26
+ var figures_1 = require("./figures");
27
+ Object.defineProperty(exports, "accountFigures", { enumerable: true, get: function () { return figures_1.accountFigures; } });
28
+ Object.defineProperty(exports, "computeAccountFigures", { enumerable: true, get: function () { return figures_1.computeAccountFigures; } });
29
+ Object.defineProperty(exports, "MARGIN_ASSUMPTIONS", { enumerable: true, get: function () { return figures_1.MARGIN_ASSUMPTIONS; } });
30
+ Object.defineProperty(exports, "positionRisk", { enumerable: true, get: function () { return figures_1.positionRisk; } });
31
+ var format_1 = require("./format");
32
+ Object.defineProperty(exports, "formatPct", { enumerable: true, get: function () { return format_1.formatPct; } });
33
+ Object.defineProperty(exports, "formatUsd", { enumerable: true, get: function () { return format_1.formatUsd; } });
34
+ var response_1 = require("./response");
35
+ Object.defineProperty(exports, "accountRiskResponse", { enumerable: true, get: function () { return response_1.accountRiskResponse; } });
@@ -0,0 +1,29 @@
1
+ import { Dec } from './decimal';
2
+ import { AccountMargin, MarginD, MarketRisk, RiskD } from './engine';
3
+ export declare function marginUsageD(marginBalance: Dec, lm: Dec): Dec | null;
4
+ /**
5
+ * Liquidation margin / equity: 1 is the liquidation line. `null` when equity
6
+ * is not positive.
7
+ */
8
+ export declare function marginUsage(marginBalance: string, lm: string): string | null;
9
+ export declare function liquidationPriceD(margin: MarginD, market: string, lm: Dec): Dec | null;
10
+ /**
11
+ * Cross margin: the price in `market` at which equity equals the liquidation
12
+ * margin `lm` (the settled projection's), every other mark, collateral price
13
+ * and funding unchanged. `null` for a flat market and when no positive price
14
+ * is the liquidation line. Exact; see `roundTowardMark` for display.
15
+ */
16
+ export declare function liquidationPrice(margin: AccountMargin, market: string, lm: string): string | null;
17
+ export declare function distanceToLiquidationD(risk: RiskD, price: Dec | null): Dec | null;
18
+ /**
19
+ * How far the mark must move against the position to reach `price`, as a
20
+ * fraction of the mark. Negative when the account is already past the
21
+ * liquidation line.
22
+ */
23
+ export declare function distanceToLiquidation(risk: MarketRisk, price: string | null): string | null;
24
+ export declare function roundTowardMarkD(price: Dec | null, risk: RiskD, tick: Dec): Dec | null;
25
+ /**
26
+ * A liquidation price rounded onto the tick toward the mark, so it never reads
27
+ * farther away than it is: up for a long, down for a short.
28
+ */
29
+ export declare function roundTowardMark(price: string | null, risk: MarketRisk, tick: string): string | null;