@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/LICENSE.md ADDED
@@ -0,0 +1 @@
1
+ MIT License
package/README.md CHANGED
@@ -1,3 +1,154 @@
1
- # Temporary Holding Version
1
+ # @reyaxyz/account-figures
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Account figures from public V2 data: equity, margin requirements, margin usage,
4
+ available margin and per-position liquidation prices. V2 returns raw state
5
+ only; this module turns it into the figures a trader reads.
6
+
7
+ The figures follow Core's definitions (reya-network `AccountExposure.sol` and
8
+ `PriceHelpers.sol`). The code started as a TypeScript port of the Agent Kit's
9
+ `agent_kit/scripts/_margin.py` (kit 0.12.20), itself a port of the matching
10
+ engine's risk engine (revision 4d8ed012). The tests replay the kit's vectors
11
+ where the kit agrees with Core. Where it does not, they derive the expected
12
+ values from Core's definitions. Those cases are collateral valuation and
13
+ funding (see [Where this differs from the kit](#where-this-differs-from-the-kit)).
14
+
15
+ Pure functions: no I/O, no service configuration. The only dependency is
16
+ `bignumber.js`. Every amount in and out is a decimal string.
17
+
18
+ ## Use
19
+
20
+ ```ts
21
+ import {
22
+ computeAccountFigures,
23
+ formatPct,
24
+ formatUsd,
25
+ } from '@reyaxyz/account-figures';
26
+
27
+ const figures = computeAccountFigures({
28
+ accountId,
29
+ balances, // GET /wallet/{address}/accountBalances
30
+ positions, // GET /wallet/{address}/positions
31
+ marketDefinitions, // GET /perpMarketDefinitions
32
+ marketSummaries, // GET /perpMarkets/summary
33
+ assetDefinitions, // GET /assetDefinitions (non-rUSD collateral only)
34
+ assetOraclePrices, // GET /assetOraclePrices (non-rUSD collateral only)
35
+ });
36
+
37
+ formatUsd(figures.equity); // "2292.50"
38
+ formatUsd(figures.lmr, 'up');
39
+ formatPct(figures.liquidationMarginUsage, 'up'); // "14.14", the app's "Cross margin ratio"
40
+ figures.positions[0].liquidationPrice; // on the tick, toward the mark, or null
41
+ ```
42
+
43
+ Rows of other accounts are ignored, so a wallet's full lists can be passed.
44
+ Missing or non-numeric data (a price, a market, an asset definition, a margin
45
+ parameter) throws `AccountFiguresInputError`; it is never counted as zero.
46
+
47
+ ## Figures
48
+
49
+ | Figure | Definition |
50
+ | -- | -- |
51
+ | `equity` | The margin balance, as Core computes it: each collateral at its oracle price × (1 − haircut), or a debt at price / (1 − haircut), plus unrealised PnL at mark. V2's `WITHDRAWAL_ONLY` status is a deposit policy and changes nothing here. V2's rUSD balance already includes realised PnL and the funding and ADL cashflows up to its latest market trackers, so no funding is added. |
52
+ | `realBalance` | The same, without unrealised PnL |
53
+ | `lmr` | Liquidation margin requirement: Σ s × \|position\| × mark, with s the market's `liquidationMarginParameter`, rounded up to 6 dp per market. The account is liquidated when equity falls below it. Core's maintenance margin is a different threshold, not computed here. |
54
+ | `imr` | Initial margin requirement: IM multiplier × LMR, the multiplier read from the definitions (1.3 on mainnet) |
55
+ | `liquidationMarginUsage` | LMR / margin balance: the app's "Cross margin ratio". The figure liquidation depends on; 1 is liquidation. `null` when equity is not positive. |
56
+ | `initialMarginUsage` | IMR / margin balance: the app's "Cross margin usage". At 1 no new position fits; liquidation comes later. `null` when equity is not positive. |
57
+ | `marginBuffer` | equity − LMR |
58
+ | `availableMargin` | equity − IMR (negative when below the IMR) |
59
+ | `liquidatable` | equity < LMR |
60
+
61
+ Per position:
62
+ - `unrealizedPnl`, `notional` and `lmr` (its own);
63
+ - `liquidationPrice` and `liquidationPriceExact`;
64
+ - `distanceToLiquidation`: a fraction of the mark, negative when past the line;
65
+ - `assumesFixedCollateral`, described in the next section.
66
+
67
+ ### Liquidation price
68
+
69
+ Cross margin, per position: the price in that market at which equity equals
70
+ the liquidation margin. Every other mark, collateral price and funding is held
71
+ fixed, and the position's own liquidation margin is recomputed at that price.
72
+ With q the signed base, P the mark, E equity, s the market's rate and
73
+ LM_other the other positions' liquidation margin:
74
+
75
+ ```
76
+ liq = (q·P − E + LM_other) / (q·(1 − sign(q)·s))
77
+ ```
78
+
79
+ It is rounded onto the tick toward the mark, so it never reads farther away
80
+ than it is under that assumption. It is `null` when no positive price is the
81
+ liquidation line. For a long, that means no price liquidates it. For a short,
82
+ it means the account is already past the line at every price, and
83
+ `liquidatable` is then true.
84
+
85
+ **`assumesFixedCollateral`** is true when the account holds non-stable
86
+ collateral whose price follows the position's market: ETH, WETH or wstETH
87
+ with an ETH perp, BTC with a BTC perp, and so on. The formula holds that
88
+ collateral's price fixed, but it moves with the market. For a long, the
89
+ collateral falls as the mark falls, so liquidation comes sooner than shown.
90
+ For example, with 1 ETH collateral and a long of 1 ETH at 2,500, the price
91
+ shown is 257.73, while liquidation with the collateral moving is near 1,337.
92
+
93
+ Example (RET-256): one position, mark 100, s = 5%, equity 20. A long
94
+ liquidates at 84.2105… (84.22 on a 0.01 tick), a short at 114.2857… (114.28).
95
+
96
+ ### Against the app's figures
97
+
98
+ The app's trade page divides both requirements by the same margin balance:
99
+ "Cross margin usage" is IMR / margin balance (`initialMarginUsage` here), and
100
+ "Cross margin ratio" is LMR / margin balance (`liquidationMarginUsage`). Both
101
+ the app and this module count withdrawal-only collateral after its haircut, as
102
+ Core does. They differ on a collateral debt: Core and this module value it at
103
+ price / (1 − haircut), the app at price × (1 − haircut). For an account with a
104
+ debt, the module's equity is therefore slightly lower and both ratios slightly
105
+ higher than the app's.
106
+
107
+ ## Where this differs from the kit
108
+
109
+ - **Withdrawal-only collateral:** the kit credits it at 0. Core credits every
110
+ supporting collateral, and so does this module.
111
+ - **Debts:** the kit values a debt at price. Core and this module use
112
+ price / (1 − haircut).
113
+ - **Funding:** the kit subtracts a debit-only funding estimate. V2's rUSD
114
+ balance already contains the funding cashflow, so this module adds none.
115
+ - **A short past the line at every price:** the kit returns a negative
116
+ liquidation price; this module returns `null`, per RET-256.
117
+
118
+ ## Display rounding
119
+
120
+ `formatUsd` and `formatPct` round to 2 dp, with the kit's conventions:
121
+ - cash, equity, PnL and buffers round down;
122
+ - requirements and margin usage round up;
123
+ - the distance to liquidation rounds down.
124
+
125
+ ## Lower-level functions
126
+
127
+ For figures after a hypothetical fill, such as an order preview, compose
128
+ `buildMarginSnapshot`, `applyFill`, `project`, `liquidationPrice` and
129
+ `roundTowardMark`.
130
+
131
+ `project` and `admit` are the matching engine's projection and admission
132
+ verdict, with its messages. The engine credits collateral by its own rules,
133
+ which give some collateral no credit. A snapshot from `buildMarginSnapshot`
134
+ values collateral as Core does, so it is not the engine's admission input.
135
+
136
+ ## What the figures cannot see
137
+
138
+ - Matched fills that have not settled yet (seconds).
139
+ - Funding and ADL since V2's latest market trackers.
140
+ - Collateral price skew: V2's oracle prices, not Core's feed at that block.
141
+ - The settlement reserve rate, pinned at 1% (the engine's V1 constant; used
142
+ only by `project`).
143
+
144
+ ## Develop
145
+
146
+ ```sh
147
+ pnpm --filter @reyaxyz/api-v2-sdk build # the V2 shape test reads its types
148
+ pnpm --filter @reyaxyz/account-figures build
149
+ pnpm --filter @reyaxyz/account-figures code-quality:check
150
+ pnpm --filter @reyaxyz/account-figures test
151
+ ```
152
+
153
+ Do not extend `calculateTotalMargin` in `@reyaxyz/common` for these figures:
154
+ it uses the raw base, not the ADL-adjusted size.
@@ -0,0 +1,32 @@
1
+ import BigNumber from 'bignumber.js';
2
+ /**
3
+ * The decimal type of the port. Addition, subtraction and multiplication are
4
+ * exact; division keeps 100 decimal places, rounding half to even. The kit
5
+ * runs Python's `decimal` at 100 significant digits with the same rounding, so
6
+ * every figure rounded onto a unit afterwards comes out the same.
7
+ *
8
+ * A private clone, so the global BigNumber configuration of a consumer is
9
+ * neither read nor changed.
10
+ */
11
+ export declare const Dec: typeof BigNumber;
12
+ export type Dec = BigNumber;
13
+ export declare const ZERO: Dec;
14
+ export declare const ONE: Dec;
15
+ /** One wei: the E18 step the engine rounds intermediate products to. */
16
+ export declare const E18: Dec;
17
+ /** rUSD's decimals: cash is floored and requirements are rounded up to them. */
18
+ export declare const RUSD_DECIMALS = 6;
19
+ /** The smallest unit of a token with `decimals` decimals, never finer than one wei. */
20
+ export declare function nativeUnit(decimals: number): Dec;
21
+ export declare function roundTo(value: Dec, unit: Dec, mode: BigNumber.RoundingMode): Dec;
22
+ export declare function floorTo(value: Dec, unit?: Dec): Dec;
23
+ export declare function ceilTo(value: Dec, unit?: Dec): Dec;
24
+ export declare function minOf(...values: Dec[]): Dec;
25
+ export declare function maxOf(...values: Dec[]): Dec;
26
+ export type Decimalish = string | number;
27
+ /** A finite decimal, or an input error naming `label`. */
28
+ export declare function parseDecimal(value: Decimalish | null | undefined, label: string): Dec;
29
+ /** Missing prices are not zero: a price must be present, finite and positive. */
30
+ export declare function requirePrice(value: Decimalish | null | undefined, label: string): Dec;
31
+ /** Plain decimal notation without trailing zeros, as V2 writes decimals. */
32
+ export declare function toText(value: Dec): string;
@@ -0,0 +1,82 @@
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.toText = exports.requirePrice = exports.parseDecimal = exports.maxOf = exports.minOf = exports.ceilTo = exports.floorTo = exports.roundTo = exports.nativeUnit = exports.RUSD_DECIMALS = exports.E18 = exports.ONE = exports.ZERO = exports.Dec = void 0;
7
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
8
+ const errors_1 = require("./errors");
9
+ /**
10
+ * The decimal type of the port. Addition, subtraction and multiplication are
11
+ * exact; division keeps 100 decimal places, rounding half to even. The kit
12
+ * runs Python's `decimal` at 100 significant digits with the same rounding, so
13
+ * every figure rounded onto a unit afterwards comes out the same.
14
+ *
15
+ * A private clone, so the global BigNumber configuration of a consumer is
16
+ * neither read nor changed.
17
+ */
18
+ exports.Dec = bignumber_js_1.default.clone({
19
+ DECIMAL_PLACES: 100,
20
+ ROUNDING_MODE: bignumber_js_1.default.ROUND_HALF_EVEN,
21
+ EXPONENTIAL_AT: 1e9,
22
+ });
23
+ exports.ZERO = new exports.Dec(0);
24
+ exports.ONE = new exports.Dec(1);
25
+ /** One wei: the E18 step the engine rounds intermediate products to. */
26
+ exports.E18 = new exports.Dec('1e-18');
27
+ /** rUSD's decimals: cash is floored and requirements are rounded up to them. */
28
+ exports.RUSD_DECIMALS = 6;
29
+ /** The smallest unit of a token with `decimals` decimals, never finer than one wei. */
30
+ function nativeUnit(decimals) {
31
+ return decimals >= 18 ? exports.E18 : exports.ONE.shiftedBy(-decimals);
32
+ }
33
+ exports.nativeUnit = nativeUnit;
34
+ function roundTo(value, unit, mode) {
35
+ return value.div(unit).integerValue(mode).times(unit);
36
+ }
37
+ exports.roundTo = roundTo;
38
+ function floorTo(value, unit = exports.E18) {
39
+ return roundTo(value, unit, bignumber_js_1.default.ROUND_FLOOR);
40
+ }
41
+ exports.floorTo = floorTo;
42
+ function ceilTo(value, unit = exports.E18) {
43
+ return roundTo(value, unit, bignumber_js_1.default.ROUND_CEIL);
44
+ }
45
+ exports.ceilTo = ceilTo;
46
+ function minOf(...values) {
47
+ return exports.Dec.min(...values);
48
+ }
49
+ exports.minOf = minOf;
50
+ function maxOf(...values) {
51
+ return exports.Dec.max(...values);
52
+ }
53
+ exports.maxOf = maxOf;
54
+ /** A finite decimal, or an input error naming `label`. */
55
+ function parseDecimal(value, label) {
56
+ if (value === undefined || value === null) {
57
+ throw new errors_1.AccountFiguresInputError(`${label} is missing`);
58
+ }
59
+ const result = new exports.Dec(typeof value === 'string' ? value.trim() : value);
60
+ if (!result.isFinite()) {
61
+ throw new errors_1.AccountFiguresInputError(`${label} is not a finite number: ${JSON.stringify(value)}`);
62
+ }
63
+ return result;
64
+ }
65
+ exports.parseDecimal = parseDecimal;
66
+ /** Missing prices are not zero: a price must be present, finite and positive. */
67
+ function requirePrice(value, label) {
68
+ if (value === undefined || value === null) {
69
+ throw new errors_1.AccountFiguresInputError(`${label} is unavailable`);
70
+ }
71
+ const price = new exports.Dec(typeof value === 'string' ? value.trim() : value);
72
+ if (!price.isFinite() || price.lte(0)) {
73
+ throw new errors_1.AccountFiguresInputError(`${label} must be a positive finite price`);
74
+ }
75
+ return price;
76
+ }
77
+ exports.requirePrice = requirePrice;
78
+ /** Plain decimal notation without trailing zeros, as V2 writes decimals. */
79
+ function toText(value) {
80
+ return value.isZero() ? '0' : value.toFixed();
81
+ }
82
+ exports.toText = toText;
@@ -0,0 +1,115 @@
1
+ import { Dec } from './decimal';
2
+ /**
3
+ * The engine's V1 policy constant (`[risk] reserve_rate`), applied once to the
4
+ * notional of the order and of the account's matched-but-unsettled fills. V2
5
+ * does not publish it.
6
+ */
7
+ export declare const SETTLEMENT_RESERVE_RATE = "0.01";
8
+ /** One market as the projection reads it (`PositionRiskInputs`). */
9
+ export interface MarketRisk {
10
+ /** Signed settled base after ADL rescaling; 0 for a market without a position. */
11
+ base: string;
12
+ mark: string;
13
+ /** Liquidation-margin rate s: V2 `liquidationMarginParameter`. */
14
+ marginRate: string;
15
+ /** Average entry price (Core `lastPrice`); the mark for a flat market. */
16
+ entry: string;
17
+ }
18
+ /** Settled account state (`AccountMargin`). */
19
+ export interface AccountMargin {
20
+ realBalance: string;
21
+ /** Equity: the real balance plus unrealised PnL at mark. */
22
+ marginBalance: string;
23
+ markets: Readonly<Record<string, MarketRisk>>;
24
+ }
25
+ /** A fill the projection considers: an unsettled one, or an order at its limit price. */
26
+ export interface Lot {
27
+ market: string;
28
+ /** Signed: positive buys. */
29
+ qty: string;
30
+ price: string;
31
+ }
32
+ export interface Projection {
33
+ mbFloor: string;
34
+ rbFloor: string;
35
+ /** Liquidation margin requirement (LMR). */
36
+ lm: string;
37
+ /** Initial margin requirement (IMR). */
38
+ im: string;
39
+ reserve: string;
40
+ /** Health: min(mbFloor − lm, rbFloor). */
41
+ h: string;
42
+ /** h − reserve. */
43
+ c: string;
44
+ }
45
+ export declare const LIQUIDATION_THRESHOLD = "below_liquidation_threshold";
46
+ export declare const INITIAL_MARGIN = "below_initial_margin";
47
+ export declare const SETTLEMENT_RESERVE = "below_settlement_reserve";
48
+ export type RejectionCode = typeof LIQUIDATION_THRESHOLD | typeof INITIAL_MARGIN | typeof SETTLEMENT_RESERVE;
49
+ export interface Rejection {
50
+ code: RejectionCode;
51
+ /** The engine's error code. */
52
+ errorCode: string;
53
+ shortfall: string;
54
+ /** The engine's own message, as sent on the wire. */
55
+ engineMessage: string;
56
+ }
57
+ export interface ProjectOptions {
58
+ imMultiplier: string;
59
+ /** Matched fills not settled yet. V2 does not show them, so live callers pass none. */
60
+ inFlight?: readonly Lot[];
61
+ reserveRate?: string;
62
+ decimals?: number;
63
+ }
64
+ export interface RiskD {
65
+ base: Dec;
66
+ mark: Dec;
67
+ marginRate: Dec;
68
+ entry: Dec;
69
+ }
70
+ export interface MarginD {
71
+ realBalance: Dec;
72
+ marginBalance: Dec;
73
+ markets: Map<string, RiskD>;
74
+ }
75
+ interface LotD {
76
+ market: string;
77
+ qty: Dec;
78
+ price: Dec;
79
+ }
80
+ interface ProjectionD {
81
+ mbFloor: Dec;
82
+ rbFloor: Dec;
83
+ lm: Dec;
84
+ im: Dec;
85
+ reserve: Dec;
86
+ h: Dec;
87
+ c: Dec;
88
+ }
89
+ export declare function parseRisk(market: string, risk: MarketRisk): RiskD;
90
+ export declare function parseMargin(margin: AccountMargin): MarginD;
91
+ export declare function riskText(risk: RiskD): MarketRisk;
92
+ export declare function marginText(margin: MarginD): AccountMargin;
93
+ /**
94
+ * Core's average-entry and PnL truncation bound: one native unit per
95
+ * transition for any realistic size.
96
+ */
97
+ export declare function roundingAllowance(worstBase: string, transitions: number, decimals?: number): string;
98
+ export declare function projectD(margin: MarginD, imMultiplier: Dec, candidates?: readonly LotD[], inFlight?: readonly LotD[], reserveRate?: Dec, decimals?: number): ProjectionD;
99
+ /**
100
+ * The engine's `project_account_with_lots`: settled state plus unsettled lots
101
+ * plus candidates. With no lots it is the settled account, whose `lm` and `im`
102
+ * are the account's LMR and IMR.
103
+ */
104
+ export declare function project(margin: AccountMargin, candidates: readonly Lot[], options: ProjectOptions): Projection;
105
+ /**
106
+ * The engine's `admit_taker`: the liquidation-threshold precondition for every
107
+ * request, then initial margin before and after and the settlement reserve
108
+ * after, for an ordinary one only. `strictReduceOnly` is the reduce-only flag
109
+ * or a TP/SL; an order that merely opposes the position is ordinary. The
110
+ * messages are the engine's own.
111
+ */
112
+ export declare function admit(before: Projection, after: Projection, strictReduceOnly: boolean): Rejection | null;
113
+ /** The account after `lot` fills in full at its price, settled the way Core updates a position. */
114
+ export declare function applyFill(margin: AccountMargin, lot: Lot, fee?: string): AccountMargin;
115
+ export {};