@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 +1 -0
- package/README.md +153 -2
- package/dist/decimal.d.ts +32 -0
- package/dist/decimal.js +82 -0
- package/dist/engine.d.ts +115 -0
- package/dist/engine.js +318 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -0
- package/dist/figures.d.ts +81 -0
- package/dist/figures.js +107 -0
- package/dist/format.d.ts +8 -0
- package/dist/format.js +30 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +35 -0
- package/dist/liquidation.d.ts +29 -0
- package/dist/liquidation.js +117 -0
- package/dist/response.d.ts +47 -0
- package/dist/response.js +64 -0
- package/dist/snapshot.d.ts +123 -0
- package/dist/snapshot.js +259 -0
- package/package.json +43 -4
|
@@ -0,0 +1,117 @@
|
|
|
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.roundTowardMark = exports.roundTowardMarkD = exports.distanceToLiquidation = exports.distanceToLiquidationD = exports.liquidationPrice = exports.liquidationPriceD = exports.marginUsage = exports.marginUsageD = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Margin usage and the cross-margin liquidation price, ported from the Agent
|
|
9
|
+
* Kit's `_margin.py` (kit 0.12.20).
|
|
10
|
+
*
|
|
11
|
+
* The liquidation price of one position is the price in its market at which
|
|
12
|
+
* equity equals the liquidation margin, every other mark, collateral price and
|
|
13
|
+
* funding unchanged, with the position's own liquidation margin recomputed at
|
|
14
|
+
* that price. Per position, with q its signed base, P its mark, E the account's
|
|
15
|
+
* equity, s the market's liquidation-margin rate and LM_other the liquidation
|
|
16
|
+
* margin of the account's other positions:
|
|
17
|
+
*
|
|
18
|
+
* liq = (q·P − E + LM_other) / (q·(1 − sign(q)·s))
|
|
19
|
+
*
|
|
20
|
+
* which is the kit's `mark − (E − LM) / (q·(1 − sign(q)·s))`.
|
|
21
|
+
*/
|
|
22
|
+
const bignumber_js_1 = __importDefault(require("bignumber.js"));
|
|
23
|
+
const decimal_1 = require("./decimal");
|
|
24
|
+
const engine_1 = require("./engine");
|
|
25
|
+
const errors_1 = require("./errors");
|
|
26
|
+
function marginUsageD(marginBalance, lm) {
|
|
27
|
+
if (marginBalance.lte(0)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return lm.div(marginBalance);
|
|
31
|
+
}
|
|
32
|
+
exports.marginUsageD = marginUsageD;
|
|
33
|
+
/**
|
|
34
|
+
* Liquidation margin / equity: 1 is the liquidation line. `null` when equity
|
|
35
|
+
* is not positive.
|
|
36
|
+
*/
|
|
37
|
+
function marginUsage(marginBalance, lm) {
|
|
38
|
+
const usage = marginUsageD((0, decimal_1.parseDecimal)(marginBalance, 'margin balance'), (0, decimal_1.parseDecimal)(lm, 'liquidation margin'));
|
|
39
|
+
return usage === null ? null : (0, decimal_1.toText)(usage);
|
|
40
|
+
}
|
|
41
|
+
exports.marginUsage = marginUsage;
|
|
42
|
+
function liquidationPriceD(margin, market, lm) {
|
|
43
|
+
const risk = margin.markets.get(market);
|
|
44
|
+
if (risk === undefined) {
|
|
45
|
+
throw new errors_1.AccountFiguresInputError(`No mark or margin rate for ${market}`);
|
|
46
|
+
}
|
|
47
|
+
if (risk.base.isZero()) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const buffer = margin.marginBalance.minus(lm);
|
|
51
|
+
let price;
|
|
52
|
+
if (risk.base.gt(0)) {
|
|
53
|
+
if (risk.marginRate.gte(1)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
price = risk.mark.minus(buffer.div(risk.base.times(new decimal_1.Dec(1).minus(risk.marginRate))));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
price = risk.mark.plus(buffer.div(risk.base.negated().times(new decimal_1.Dec(1).plus(risk.marginRate))));
|
|
60
|
+
}
|
|
61
|
+
// A long that no positive price liquidates has none. A short whose price
|
|
62
|
+
// would be at or below zero is past the line at every price (equity is
|
|
63
|
+
// already below the liquidation margin, so the account reads liquidatable);
|
|
64
|
+
// it has none either, rather than the negative price the kit returns
|
|
65
|
+
// (RET-256).
|
|
66
|
+
return price.gt(0) ? price : null;
|
|
67
|
+
}
|
|
68
|
+
exports.liquidationPriceD = liquidationPriceD;
|
|
69
|
+
/**
|
|
70
|
+
* Cross margin: the price in `market` at which equity equals the liquidation
|
|
71
|
+
* margin `lm` (the settled projection's), every other mark, collateral price
|
|
72
|
+
* and funding unchanged. `null` for a flat market and when no positive price
|
|
73
|
+
* is the liquidation line. Exact; see `roundTowardMark` for display.
|
|
74
|
+
*/
|
|
75
|
+
function liquidationPrice(margin, market, lm) {
|
|
76
|
+
const price = liquidationPriceD((0, engine_1.parseMargin)(margin), market, (0, decimal_1.parseDecimal)(lm, 'liquidation margin'));
|
|
77
|
+
return price === null ? null : (0, decimal_1.toText)(price);
|
|
78
|
+
}
|
|
79
|
+
exports.liquidationPrice = liquidationPrice;
|
|
80
|
+
function distanceToLiquidationD(risk, price) {
|
|
81
|
+
if (price === null || risk.base.isZero()) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return risk.base.gt(0)
|
|
85
|
+
? risk.mark.minus(price).div(risk.mark)
|
|
86
|
+
: price.minus(risk.mark).div(risk.mark);
|
|
87
|
+
}
|
|
88
|
+
exports.distanceToLiquidationD = distanceToLiquidationD;
|
|
89
|
+
/**
|
|
90
|
+
* How far the mark must move against the position to reach `price`, as a
|
|
91
|
+
* fraction of the mark. Negative when the account is already past the
|
|
92
|
+
* liquidation line.
|
|
93
|
+
*/
|
|
94
|
+
function distanceToLiquidation(risk, price) {
|
|
95
|
+
const distance = distanceToLiquidationD((0, engine_1.parseRisk)('position', risk), price === null ? null : (0, decimal_1.parseDecimal)(price, 'liquidation price'));
|
|
96
|
+
return distance === null ? null : (0, decimal_1.toText)(distance);
|
|
97
|
+
}
|
|
98
|
+
exports.distanceToLiquidation = distanceToLiquidation;
|
|
99
|
+
function roundTowardMarkD(price, risk, tick) {
|
|
100
|
+
if (price === null) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (!tick.gt(0)) {
|
|
104
|
+
throw new errors_1.AccountFiguresInputError('tick size must be positive');
|
|
105
|
+
}
|
|
106
|
+
return (0, decimal_1.roundTo)(price, tick, risk.base.gt(0) ? bignumber_js_1.default.ROUND_CEIL : bignumber_js_1.default.ROUND_FLOOR);
|
|
107
|
+
}
|
|
108
|
+
exports.roundTowardMarkD = roundTowardMarkD;
|
|
109
|
+
/**
|
|
110
|
+
* A liquidation price rounded onto the tick toward the mark, so it never reads
|
|
111
|
+
* farther away than it is: up for a long, down for a short.
|
|
112
|
+
*/
|
|
113
|
+
function roundTowardMark(price, risk, tick) {
|
|
114
|
+
const rounded = roundTowardMarkD(price === null ? null : (0, decimal_1.parseDecimal)(price, 'liquidation price'), (0, engine_1.parseRisk)('position', risk), (0, decimal_1.parseDecimal)(tick, 'tick size'));
|
|
115
|
+
return rounded === null ? null : (0, decimal_1.toText)(rounded);
|
|
116
|
+
}
|
|
117
|
+
exports.roundTowardMark = roundTowardMark;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MarginInputs } from './snapshot';
|
|
2
|
+
/** Added to a position only when the complete account can be valued. */
|
|
3
|
+
export interface PositionValuation {
|
|
4
|
+
markPrice: string;
|
|
5
|
+
notional: string;
|
|
6
|
+
unrealizedPnl: string;
|
|
7
|
+
liquidationMarginRequirement: string;
|
|
8
|
+
liquidationPrice: string | null;
|
|
9
|
+
hasCorrelatedCollateral: boolean;
|
|
10
|
+
updatedAt: number;
|
|
11
|
+
pricesUpdatedAt: number | null;
|
|
12
|
+
}
|
|
13
|
+
export interface AccountRiskInputs extends MarginInputs {
|
|
14
|
+
marketSummaries: readonly (MarginInputs['marketSummaries'][number] & {
|
|
15
|
+
pricesUpdatedAt?: number;
|
|
16
|
+
})[];
|
|
17
|
+
assetOraclePrices?: readonly (NonNullable<MarginInputs['assetOraclePrices']>[number] & {
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
})[];
|
|
20
|
+
}
|
|
21
|
+
/** Keep source freshness separate from the time at which this response is calculated. */
|
|
22
|
+
export declare function accountRiskResponse(inputs: AccountRiskInputs, updatedAt: number): {
|
|
23
|
+
summary: {
|
|
24
|
+
updatedAt: number;
|
|
25
|
+
pricesUpdatedAt: number | null;
|
|
26
|
+
accountId: number;
|
|
27
|
+
collateralValue: string;
|
|
28
|
+
unrealizedPnl: string;
|
|
29
|
+
marginBalance: string;
|
|
30
|
+
initialMarginRequirement: string;
|
|
31
|
+
liquidationMarginRequirement: string;
|
|
32
|
+
freeCollateral: string;
|
|
33
|
+
initialMarginRatio: string | null;
|
|
34
|
+
liquidationMarginRatio: string | null;
|
|
35
|
+
};
|
|
36
|
+
positions: {
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
pricesUpdatedAt: number | null;
|
|
39
|
+
symbol: string;
|
|
40
|
+
markPrice: string;
|
|
41
|
+
notional: string;
|
|
42
|
+
unrealizedPnl: string;
|
|
43
|
+
liquidationMarginRequirement: string;
|
|
44
|
+
liquidationPrice: string | null;
|
|
45
|
+
hasCorrelatedCollateral: boolean;
|
|
46
|
+
}[];
|
|
47
|
+
};
|
package/dist/response.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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.accountRiskResponse = void 0;
|
|
7
|
+
const figures_1 = require("./figures");
|
|
8
|
+
const snapshot_1 = require("./snapshot");
|
|
9
|
+
const bignumber_js_1 = __importDefault(require("bignumber.js"));
|
|
10
|
+
/** Keep source freshness separate from the time at which this response is calculated. */
|
|
11
|
+
function accountRiskResponse(inputs, updatedAt) {
|
|
12
|
+
const figures = (0, figures_1.computeAccountFigures)(inputs);
|
|
13
|
+
const timestamps = [];
|
|
14
|
+
for (const position of figures.positions) {
|
|
15
|
+
const timestamp = inputs.marketSummaries.find((row) => row.symbol === position.symbol)?.pricesUpdatedAt;
|
|
16
|
+
if (!Number.isSafeInteger(timestamp) || timestamp <= 0) {
|
|
17
|
+
throw new Error(`Missing mark timestamp for ${position.symbol}`);
|
|
18
|
+
}
|
|
19
|
+
timestamps.push(timestamp);
|
|
20
|
+
}
|
|
21
|
+
for (const collateral of figures.collateral) {
|
|
22
|
+
if (collateral.asset === snapshot_1.RUSD || new bignumber_js_1.default(collateral.balance).isZero())
|
|
23
|
+
continue;
|
|
24
|
+
const timestamp = inputs.assetOraclePrices?.find((row) => row.asset === collateral.asset)?.updatedAt;
|
|
25
|
+
if (!Number.isSafeInteger(timestamp) || timestamp <= 0) {
|
|
26
|
+
throw new Error(`Missing collateral timestamp for ${collateral.asset}`);
|
|
27
|
+
}
|
|
28
|
+
timestamps.push(timestamp);
|
|
29
|
+
}
|
|
30
|
+
const freshness = {
|
|
31
|
+
updatedAt,
|
|
32
|
+
pricesUpdatedAt: timestamps.length ? Math.min(...timestamps) : null,
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
summary: {
|
|
36
|
+
accountId: Number(inputs.accountId),
|
|
37
|
+
collateralValue: figures.realBalance,
|
|
38
|
+
unrealizedPnl: figures.unrealizedPnl,
|
|
39
|
+
marginBalance: figures.equity,
|
|
40
|
+
initialMarginRequirement: figures.imr,
|
|
41
|
+
liquidationMarginRequirement: figures.lmr,
|
|
42
|
+
freeCollateral: figures.availableMargin,
|
|
43
|
+
initialMarginRatio: ratio(figures.initialMarginUsage),
|
|
44
|
+
liquidationMarginRatio: ratio(figures.liquidationMarginUsage),
|
|
45
|
+
...freshness,
|
|
46
|
+
},
|
|
47
|
+
positions: figures.positions
|
|
48
|
+
.map((position) => ({
|
|
49
|
+
symbol: position.symbol,
|
|
50
|
+
markPrice: position.markPrice,
|
|
51
|
+
notional: position.notional,
|
|
52
|
+
unrealizedPnl: position.unrealizedPnl,
|
|
53
|
+
liquidationMarginRequirement: position.lmr,
|
|
54
|
+
liquidationPrice: position.liquidationPrice,
|
|
55
|
+
hasCorrelatedCollateral: position.assumesFixedCollateral,
|
|
56
|
+
...freshness,
|
|
57
|
+
}))
|
|
58
|
+
.sort((a, b) => a.symbol.localeCompare(b.symbol)),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
exports.accountRiskResponse = accountRiskResponse;
|
|
62
|
+
const ratio = (value) => value === null
|
|
63
|
+
? null
|
|
64
|
+
: new bignumber_js_1.default(value).decimalPlaces(18, bignumber_js_1.default.ROUND_UP).toFixed();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Dec } from './decimal';
|
|
2
|
+
import { AccountMargin, MarginD } from './engine';
|
|
3
|
+
/** One row of `GET /wallet/{address}/accountBalances`. */
|
|
4
|
+
export interface V2AccountBalance {
|
|
5
|
+
accountId: number | string;
|
|
6
|
+
asset: string;
|
|
7
|
+
/**
|
|
8
|
+
* Net deposits; for rUSD also realised PnL and the funding and ADL
|
|
9
|
+
* cashflows up to the latest market trackers, so no funding is added here.
|
|
10
|
+
*/
|
|
11
|
+
realBalance: string;
|
|
12
|
+
}
|
|
13
|
+
/** One row of `GET /wallet/{address}/positions`. */
|
|
14
|
+
export interface V2Position {
|
|
15
|
+
accountId: number | string;
|
|
16
|
+
symbol: string;
|
|
17
|
+
/** Unsigned, ADL-adjusted. */
|
|
18
|
+
qty: string;
|
|
19
|
+
/** `B` long, `A` short. */
|
|
20
|
+
side: 'B' | 'A';
|
|
21
|
+
avgEntryPrice: string;
|
|
22
|
+
}
|
|
23
|
+
/** One row of `GET /perpMarketDefinitions`. */
|
|
24
|
+
export interface V2MarketDefinition {
|
|
25
|
+
symbol: string;
|
|
26
|
+
tickSize: string;
|
|
27
|
+
liquidationMarginParameter: string;
|
|
28
|
+
initialMarginParameter: string;
|
|
29
|
+
}
|
|
30
|
+
/** One row of `GET /perpMarkets/summary`. */
|
|
31
|
+
export interface V2MarketSummary {
|
|
32
|
+
symbol: string;
|
|
33
|
+
markPrice?: string;
|
|
34
|
+
}
|
|
35
|
+
/** One row of `GET /assetDefinitions`. */
|
|
36
|
+
export interface V2AssetDefinition {
|
|
37
|
+
asset: string;
|
|
38
|
+
priceHaircut: string;
|
|
39
|
+
/**
|
|
40
|
+
* `ENABLED` or `WITHDRAWAL_ONLY`: V2's deposit policy only. It does not
|
|
41
|
+
* change margin credit, which Core gives every supporting collateral.
|
|
42
|
+
*/
|
|
43
|
+
status: string;
|
|
44
|
+
}
|
|
45
|
+
/** One row of `GET /assetOraclePrices`. */
|
|
46
|
+
export interface V2AssetOraclePrice {
|
|
47
|
+
asset: string;
|
|
48
|
+
oraclePrice: string;
|
|
49
|
+
}
|
|
50
|
+
export interface MarginInputs {
|
|
51
|
+
accountId: number | string;
|
|
52
|
+
/** The wallet's balances; rows of other accounts are ignored. */
|
|
53
|
+
balances: readonly V2AccountBalance[];
|
|
54
|
+
/** The wallet's positions; rows of other accounts are ignored. */
|
|
55
|
+
positions: readonly V2Position[];
|
|
56
|
+
marketDefinitions: readonly V2MarketDefinition[];
|
|
57
|
+
marketSummaries: readonly V2MarketSummary[];
|
|
58
|
+
/** Needed only when the account holds collateral other than rUSD. */
|
|
59
|
+
assetDefinitions?: readonly V2AssetDefinition[];
|
|
60
|
+
/** Needed only when the account holds collateral other than rUSD. */
|
|
61
|
+
assetOraclePrices?: readonly V2AssetOraclePrice[];
|
|
62
|
+
/** Markets to include without a position, such as the one an order is for. */
|
|
63
|
+
symbols?: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
export interface CollateralRow {
|
|
66
|
+
asset: string;
|
|
67
|
+
/** V2's balance string. */
|
|
68
|
+
balance: string;
|
|
69
|
+
priceUsd: string;
|
|
70
|
+
haircut: string;
|
|
71
|
+
/** Balance × price, before the haircut. */
|
|
72
|
+
valueUsd: string;
|
|
73
|
+
/**
|
|
74
|
+
* What counts toward equity, as Core values it: balance × price × (1 −
|
|
75
|
+
* haircut), or for a debt balance × price / (1 − haircut). Withdrawal-only
|
|
76
|
+
* collateral counts like any other.
|
|
77
|
+
*/
|
|
78
|
+
marginCreditUsd: string;
|
|
79
|
+
/** The asset definition's deposit status; `null` when none was given (rUSD needs none). */
|
|
80
|
+
status: string | null;
|
|
81
|
+
}
|
|
82
|
+
export interface PositionDetail {
|
|
83
|
+
symbol: string;
|
|
84
|
+
side: 'LONG' | 'SHORT';
|
|
85
|
+
/** V2's qty string (unsigned). */
|
|
86
|
+
qty: string;
|
|
87
|
+
signedQty: string;
|
|
88
|
+
/** V2's average entry price string. */
|
|
89
|
+
avgEntryPrice: string;
|
|
90
|
+
/** V2's mark price string. */
|
|
91
|
+
markPrice: string;
|
|
92
|
+
unrealizedPnl: string;
|
|
93
|
+
}
|
|
94
|
+
/** One account's margin as read from V2. */
|
|
95
|
+
export interface MarginSnapshot {
|
|
96
|
+
accountId: number | string;
|
|
97
|
+
margin: AccountMargin;
|
|
98
|
+
imMultiplier: string;
|
|
99
|
+
definitions: Readonly<Record<string, V2MarketDefinition>>;
|
|
100
|
+
collateral: CollateralRow[];
|
|
101
|
+
positions: PositionDetail[];
|
|
102
|
+
unrealizedPnl: string;
|
|
103
|
+
}
|
|
104
|
+
export declare const RUSD = "RUSD";
|
|
105
|
+
/** The asset a collateral's price follows: `WSTETH` → `ETH`, `BTC` → `BTC`. */
|
|
106
|
+
export declare function collateralUnderlying(asset: string): string;
|
|
107
|
+
/** The asset a perp market trades: `ETHRUSDPERP` → `ETH`. */
|
|
108
|
+
export declare function marketUnderlying(symbol: string): string;
|
|
109
|
+
/**
|
|
110
|
+
* The collateral pool's IM multiplier, as V2 publishes it per market
|
|
111
|
+
* (IM = LM rate × multiplier). V2 computes both parameters in floating point,
|
|
112
|
+
* so the ratio is rounded to 12 places; markets that disagree resolve to the
|
|
113
|
+
* largest. A market without positive parameters is skipped here; one an
|
|
114
|
+
* account holds fails on its own rate instead.
|
|
115
|
+
*/
|
|
116
|
+
export declare function imMultiplierFrom(definitions: readonly Pick<V2MarketDefinition, 'symbol' | 'liquidationMarginParameter' | 'initialMarginParameter'>[]): string | null;
|
|
117
|
+
export declare function buildMarginSnapshotD(inputs: MarginInputs): {
|
|
118
|
+
snapshot: MarginSnapshot;
|
|
119
|
+
margin: MarginD;
|
|
120
|
+
imMultiplier: Dec;
|
|
121
|
+
};
|
|
122
|
+
/** One account's margin inputs from V2 reads, valued as Core values them. */
|
|
123
|
+
export declare function buildMarginSnapshot(inputs: MarginInputs): MarginSnapshot;
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
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.buildMarginSnapshot = exports.buildMarginSnapshotD = exports.imMultiplierFrom = exports.marketUnderlying = exports.collateralUnderlying = exports.RUSD = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Assembles an account's margin inputs from V2 reads, the way Core values them
|
|
9
|
+
* (reya-network `AccountExposure.sol`, `PriceHelpers.sol`): every supporting
|
|
10
|
+
* collateral at price × (1 − haircut), a debt at price / (1 − haircut), plus
|
|
11
|
+
* unrealised PnL at mark. The structure follows the Agent Kit's
|
|
12
|
+
* `build_snapshot` (kit 0.12.20); where the kit differs from Core (it credits
|
|
13
|
+
* withdrawal-only collateral at 0, counts a debt at price and subtracts a
|
|
14
|
+
* funding estimate already in the balance), this follows Core. Missing data
|
|
15
|
+
* throws instead of counting as zero.
|
|
16
|
+
*
|
|
17
|
+
* The input types name only the fields the figures read, with V2's names and
|
|
18
|
+
* types, so V2 responses (and the `@reyaxyz/api-v2-sdk` models) pass as they
|
|
19
|
+
* are.
|
|
20
|
+
*/
|
|
21
|
+
const bignumber_js_1 = __importDefault(require("bignumber.js"));
|
|
22
|
+
const decimal_1 = require("./decimal");
|
|
23
|
+
const engine_1 = require("./engine");
|
|
24
|
+
const errors_1 = require("./errors");
|
|
25
|
+
exports.RUSD = 'RUSD';
|
|
26
|
+
/**
|
|
27
|
+
* Collateral that tracks another asset's price: wrapped and staked forms of
|
|
28
|
+
* ETH, BTC and REYA.
|
|
29
|
+
*/
|
|
30
|
+
const UNDERLYING = {
|
|
31
|
+
WETH: 'ETH',
|
|
32
|
+
STETH: 'ETH',
|
|
33
|
+
WSTETH: 'ETH',
|
|
34
|
+
WBTC: 'BTC',
|
|
35
|
+
SREYA: 'REYA',
|
|
36
|
+
};
|
|
37
|
+
/** The asset a collateral's price follows: `WSTETH` → `ETH`, `BTC` → `BTC`. */
|
|
38
|
+
function collateralUnderlying(asset) {
|
|
39
|
+
const upper = asset.toUpperCase();
|
|
40
|
+
return UNDERLYING[upper] ?? upper;
|
|
41
|
+
}
|
|
42
|
+
exports.collateralUnderlying = collateralUnderlying;
|
|
43
|
+
/** The asset a perp market trades: `ETHRUSDPERP` → `ETH`. */
|
|
44
|
+
function marketUnderlying(symbol) {
|
|
45
|
+
return symbol.replace(/RUSDPERP$/i, '').toUpperCase();
|
|
46
|
+
}
|
|
47
|
+
exports.marketUnderlying = marketUnderlying;
|
|
48
|
+
function sameAccount(a, b) {
|
|
49
|
+
return String(a) === String(b);
|
|
50
|
+
}
|
|
51
|
+
function marginRate(definition) {
|
|
52
|
+
const rate = (0, decimal_1.parseDecimal)(definition.liquidationMarginParameter, `${definition.symbol} liquidation margin parameter`);
|
|
53
|
+
if (!(rate.gt(0) && rate.lt(1))) {
|
|
54
|
+
throw new errors_1.AccountFiguresInputError(`${definition.symbol} liquidation margin parameter must be between 0 and 1`);
|
|
55
|
+
}
|
|
56
|
+
return rate;
|
|
57
|
+
}
|
|
58
|
+
function imMultiplierD(definitions) {
|
|
59
|
+
let best = null;
|
|
60
|
+
for (const md of definitions) {
|
|
61
|
+
let lm;
|
|
62
|
+
let im;
|
|
63
|
+
try {
|
|
64
|
+
lm = (0, decimal_1.parseDecimal)(md.liquidationMarginParameter, `${md.symbol} liquidation margin parameter`);
|
|
65
|
+
im = (0, decimal_1.parseDecimal)(md.initialMarginParameter, `${md.symbol} initial margin parameter`);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof errors_1.AccountFiguresInputError) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
if (lm.gt(0) && im.gt(0)) {
|
|
74
|
+
const ratio = im.div(lm).decimalPlaces(12, bignumber_js_1.default.ROUND_HALF_EVEN);
|
|
75
|
+
if (best === null || ratio.gt(best)) {
|
|
76
|
+
best = ratio;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return best;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The collateral pool's IM multiplier, as V2 publishes it per market
|
|
84
|
+
* (IM = LM rate × multiplier). V2 computes both parameters in floating point,
|
|
85
|
+
* so the ratio is rounded to 12 places; markets that disagree resolve to the
|
|
86
|
+
* largest. A market without positive parameters is skipped here; one an
|
|
87
|
+
* account holds fails on its own rate instead.
|
|
88
|
+
*/
|
|
89
|
+
function imMultiplierFrom(definitions) {
|
|
90
|
+
const multiplier = imMultiplierD(definitions);
|
|
91
|
+
return multiplier === null ? null : (0, decimal_1.toText)(multiplier);
|
|
92
|
+
}
|
|
93
|
+
exports.imMultiplierFrom = imMultiplierFrom;
|
|
94
|
+
function buildMarginSnapshotD(inputs) {
|
|
95
|
+
const bySymbol = new Map();
|
|
96
|
+
for (const md of inputs.marketDefinitions) {
|
|
97
|
+
bySymbol.set(md.symbol, md);
|
|
98
|
+
}
|
|
99
|
+
const summaries = new Map();
|
|
100
|
+
for (const summary of inputs.marketSummaries) {
|
|
101
|
+
summaries.set(summary.symbol, summary);
|
|
102
|
+
}
|
|
103
|
+
const assets = new Map();
|
|
104
|
+
for (const asset of inputs.assetDefinitions ?? []) {
|
|
105
|
+
assets.set(asset.asset, asset);
|
|
106
|
+
}
|
|
107
|
+
const assetPrices = new Map();
|
|
108
|
+
for (const price of inputs.assetOraclePrices ?? []) {
|
|
109
|
+
assetPrices.set(price.asset, price.oraclePrice);
|
|
110
|
+
}
|
|
111
|
+
const unit = (0, decimal_1.nativeUnit)(decimal_1.RUSD_DECIMALS);
|
|
112
|
+
const collateral = [];
|
|
113
|
+
let creditTotal = decimal_1.ZERO;
|
|
114
|
+
for (const balance of inputs.balances) {
|
|
115
|
+
if (!sameAccount(balance.accountId, inputs.accountId)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const amount = (0, decimal_1.parseDecimal)(balance.realBalance, `${balance.asset} balance`);
|
|
119
|
+
if (amount.isZero()) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const status = assets.get(balance.asset)?.status ?? null;
|
|
123
|
+
let row;
|
|
124
|
+
let credit;
|
|
125
|
+
if (balance.asset === exports.RUSD) {
|
|
126
|
+
// rUSD is the quote collateral: price 1, no haircut.
|
|
127
|
+
credit = (0, decimal_1.floorTo)(amount, unit);
|
|
128
|
+
row = {
|
|
129
|
+
asset: balance.asset,
|
|
130
|
+
balance: balance.realBalance,
|
|
131
|
+
priceUsd: '1',
|
|
132
|
+
haircut: '0',
|
|
133
|
+
valueUsd: (0, decimal_1.toText)(amount),
|
|
134
|
+
marginCreditUsd: (0, decimal_1.toText)(credit),
|
|
135
|
+
status,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const definition = assets.get(balance.asset);
|
|
140
|
+
if (definition === undefined) {
|
|
141
|
+
throw new errors_1.AccountFiguresInputError(`Asset '${balance.asset}' has a balance but is missing from the asset definitions. ` +
|
|
142
|
+
'This is usually a transient API issue: retry.');
|
|
143
|
+
}
|
|
144
|
+
const price = (0, decimal_1.requirePrice)(assetPrices.get(balance.asset), `${balance.asset} collateral oracle price`);
|
|
145
|
+
const haircut = (0, decimal_1.parseDecimal)(definition.priceHaircut, `${balance.asset} haircut`);
|
|
146
|
+
if (haircut.lt(0) || haircut.gte(1)) {
|
|
147
|
+
throw new errors_1.AccountFiguresInputError(`${balance.asset} haircut must be at least 0 and below 1`);
|
|
148
|
+
}
|
|
149
|
+
const value = amount.times(price);
|
|
150
|
+
// Core's exchangeWithHaircut: a holding is worth less than its price, a
|
|
151
|
+
// debt costs more (PriceHelpers.sol). Both round down, against the account.
|
|
152
|
+
credit = (0, decimal_1.floorTo)(amount.gt(0)
|
|
153
|
+
? value.times(decimal_1.ONE.minus(haircut))
|
|
154
|
+
: value.div(decimal_1.ONE.minus(haircut)), unit);
|
|
155
|
+
row = {
|
|
156
|
+
asset: balance.asset,
|
|
157
|
+
balance: balance.realBalance,
|
|
158
|
+
priceUsd: (0, decimal_1.toText)(price),
|
|
159
|
+
haircut: (0, decimal_1.toText)(haircut),
|
|
160
|
+
valueUsd: (0, decimal_1.toText)(value),
|
|
161
|
+
marginCreditUsd: (0, decimal_1.toText)(credit),
|
|
162
|
+
status,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
creditTotal = creditTotal.plus(credit);
|
|
166
|
+
collateral.push(row);
|
|
167
|
+
}
|
|
168
|
+
const markets = new Map();
|
|
169
|
+
const details = [];
|
|
170
|
+
let upnlTotal = decimal_1.ZERO;
|
|
171
|
+
for (const position of inputs.positions) {
|
|
172
|
+
if (!sameAccount(position.accountId, inputs.accountId)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const unsigned = (0, decimal_1.parseDecimal)(position.qty, `${position.symbol} position qty`);
|
|
176
|
+
const qty = position.side === 'B' ? unsigned : unsigned.negated();
|
|
177
|
+
if (qty.isZero()) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const definition = bySymbol.get(position.symbol);
|
|
181
|
+
if (definition === undefined) {
|
|
182
|
+
throw new errors_1.AccountFiguresInputError(`Position symbol '${position.symbol}' is missing from the perp market definitions.`);
|
|
183
|
+
}
|
|
184
|
+
const summary = summaries.get(position.symbol);
|
|
185
|
+
if (summary === undefined) {
|
|
186
|
+
throw new errors_1.AccountFiguresInputError(`Position symbol '${position.symbol}' is missing from the perp market summaries.`);
|
|
187
|
+
}
|
|
188
|
+
const mark = (0, decimal_1.requirePrice)(summary.markPrice, `${position.symbol} mark price`);
|
|
189
|
+
const entry = (0, decimal_1.parseDecimal)(position.avgEntryPrice, `${position.symbol} entry price`);
|
|
190
|
+
const rate = marginRate(definition);
|
|
191
|
+
const upnl = (0, decimal_1.floorTo)((0, decimal_1.floorTo)(qty.times(mark.minus(entry))), unit);
|
|
192
|
+
upnlTotal = upnlTotal.plus(upnl);
|
|
193
|
+
markets.set(position.symbol, { base: qty, mark, marginRate: rate, entry });
|
|
194
|
+
details.push({
|
|
195
|
+
symbol: position.symbol,
|
|
196
|
+
side: qty.gt(0) ? 'LONG' : 'SHORT',
|
|
197
|
+
qty: position.qty,
|
|
198
|
+
signedQty: (0, decimal_1.toText)(qty),
|
|
199
|
+
avgEntryPrice: position.avgEntryPrice,
|
|
200
|
+
markPrice: summary.markPrice,
|
|
201
|
+
unrealizedPnl: (0, decimal_1.toText)(upnl),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
for (const symbol of inputs.symbols ?? []) {
|
|
205
|
+
if (markets.has(symbol)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const definition = bySymbol.get(symbol);
|
|
209
|
+
if (definition === undefined) {
|
|
210
|
+
throw new errors_1.AccountFiguresInputError(`Unknown symbol '${symbol}'.`);
|
|
211
|
+
}
|
|
212
|
+
const summary = summaries.get(symbol);
|
|
213
|
+
if (summary === undefined) {
|
|
214
|
+
throw new errors_1.AccountFiguresInputError(`'${symbol}' is missing from the market summaries.`);
|
|
215
|
+
}
|
|
216
|
+
const mark = (0, decimal_1.requirePrice)(summary.markPrice, `${symbol} mark price`);
|
|
217
|
+
markets.set(symbol, {
|
|
218
|
+
base: decimal_1.ZERO,
|
|
219
|
+
mark,
|
|
220
|
+
marginRate: marginRate(definition),
|
|
221
|
+
entry: mark,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const multiplier = imMultiplierD(inputs.marketDefinitions);
|
|
225
|
+
if (multiplier === null && markets.size > 0) {
|
|
226
|
+
throw new errors_1.AccountFiguresInputError('No perp market definition publishes margin parameters; cannot compute initial margin');
|
|
227
|
+
}
|
|
228
|
+
// Without markets there is no margin requirement for a multiplier to scale.
|
|
229
|
+
const imMultiplier = multiplier ?? decimal_1.ONE;
|
|
230
|
+
const realBalance = creditTotal;
|
|
231
|
+
const margin = {
|
|
232
|
+
realBalance,
|
|
233
|
+
marginBalance: realBalance.plus(upnlTotal),
|
|
234
|
+
markets,
|
|
235
|
+
};
|
|
236
|
+
const definitions = {};
|
|
237
|
+
for (const [symbol, md] of bySymbol) {
|
|
238
|
+
definitions[symbol] = md;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
margin,
|
|
242
|
+
imMultiplier,
|
|
243
|
+
snapshot: {
|
|
244
|
+
accountId: inputs.accountId,
|
|
245
|
+
margin: (0, engine_1.marginText)(margin),
|
|
246
|
+
imMultiplier: (0, decimal_1.toText)(imMultiplier),
|
|
247
|
+
definitions,
|
|
248
|
+
collateral,
|
|
249
|
+
positions: details,
|
|
250
|
+
unrealizedPnl: (0, decimal_1.toText)(upnlTotal),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
exports.buildMarginSnapshotD = buildMarginSnapshotD;
|
|
255
|
+
/** One account's margin inputs from V2 reads, valued as Core values them. */
|
|
256
|
+
function buildMarginSnapshot(inputs) {
|
|
257
|
+
return buildMarginSnapshotD(inputs).snapshot;
|
|
258
|
+
}
|
|
259
|
+
exports.buildMarginSnapshot = buildMarginSnapshot;
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reyaxyz/account-figures",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Reya account figures from public V2 data: equity, margin requirements, margin usage and liquidation prices",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public",
|
|
7
|
+
"registry": "https://registry.npmjs.org"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/Reya-Labs/reya-off-chain-monorepo.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Reya Network",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.18.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
27
|
+
"ts:check-types": "tsc --noEmit",
|
|
28
|
+
"ts:circular-check": "dpdm --no-output --no-tree --no-warning --exit-code circular:1 -T src/index.ts",
|
|
29
|
+
"ts:check": "pnpm ts:check-types && pnpm ts:circular-check",
|
|
30
|
+
"prettier:check": "prettier --log-level silent --check './src/**/*.ts' './tests/**/*.ts'",
|
|
31
|
+
"prettier:fix": "prettier --log-level silent --write './src/**/*.ts' './tests/**/*.ts'",
|
|
32
|
+
"lint:check": "eslint src tests --ext .ts",
|
|
33
|
+
"lint:fix": "eslint src tests --ext .ts --fix",
|
|
34
|
+
"code-quality:check": "concurrently 'pnpm ts:check' 'pnpm prettier:check' 'pnpm lint:check --quiet'",
|
|
35
|
+
"test": "jest --runInBand"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"bignumber.js": "9.1.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@reyaxyz/api-v2-sdk": "0.316.0",
|
|
42
|
+
"typescript": "5.3.3"
|
|
43
|
+
},
|
|
44
|
+
"gitHead": "c79e1c00f842259ddcb7c435ed799ea0b49e9939"
|
|
45
|
+
}
|