import {
buildMarketPriceContext,
buildTradeOpeningPriceImpactContext,
ContractsVersion,
getCurrentMarketPrice,
getFixedSpreadP,
getTradeOpeningPriceImpact,
transformGlobalTradingVariables,
} from "@gainsnetwork/sdk";
type VobLevel = {
price: number;
sizeUsd: number;
cumulativeUsd: number;
impactPct: number;
};
type VirtualOrderBook = {
marketPrice: number;
asks: VobLevel[];
bids: VobLevel[];
};
const TARGET_IMPACTS_PCT = [
0.0005, 0.001, 0.0025, 0.005, 0.01, 0.02, 0.03, 0.05, 0.075, 0.1,
0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0,
];
const TARGET_SIZES_USD = [
1_000, 5_000, 10_000, 25_000, 50_000, 75_000, 100_000, 150_000,
250_000, 500_000, 750_000, 1_000_000, 2_500_000, 5_000_000, 10_000_000,
];
const MIN_LEVEL_SIZE_USD = 2_000;
const MAX_LEVELS = 15;
const MAX_IMPACT_PCT = 2;
const IMPACT_SIMILARITY_THRESHOLD = 0.0001;
async function buildVirtualOrderBook(
chainBackendUrl: string,
pairIndex: number,
collateralIndex: number
): Promise<VirtualOrderBook | null> {
const [rawTradingVariables, charts] = await Promise.all([
fetch(`${chainBackendUrl}/trading-variables/all`).then((res) => res.json()),
fetch("https://backend-pricing.eu.gains.trade/charts").then((res) => res.json()),
]);
const { globalTradingVariables, blockNumber } =
transformGlobalTradingVariables(rawTradingVariables);
const oraclePrice = charts.closes[pairIndex];
const pair = globalTradingVariables.pairs?.[pairIndex];
const collateral = globalTradingVariables.collaterals?.[collateralIndex - 1];
const fee = pair ? globalTradingVariables.fees?.[pair.feeIndex] : undefined;
if (!oraclePrice || !pair || !fee || !collateral) {
return null;
}
const marketPriceContext = buildMarketPriceContext(collateral, pairIndex);
const currentMarketPrice = getCurrentMarketPrice(
pairIndex,
oraclePrice,
marketPriceContext
);
const marketPrice =
marketPriceContext.skewDepth > 0 ? currentMarketPrice.marketPrice : oraclePrice;
const pairDepthBands = globalTradingVariables.pairDepthBands?.[pairIndex];
const hasDepth =
(pairDepthBands?.above?.totalDepthUsd ?? 0) > 0 ||
(pairDepthBands?.below?.totalDepthUsd ?? 0) > 0;
const hasSkew = (collateral.pairSkewDepths?.[pairIndex] ?? 0) > 0;
if (!hasDepth && !hasSkew) {
const halfSpreadPct = getFixedSpreadP(pair.spreadP, true, true);
return {
marketPrice,
asks: [
{
price: marketPrice * (1 + halfSpreadPct / 100),
sizeUsd: 0,
cumulativeUsd: 0,
impactPct: halfSpreadPct,
},
],
bids: [
{
price: marketPrice * (1 - halfSpreadPct / 100),
sizeUsd: 0,
cumulativeUsd: 0,
impactPct: -halfSpreadPct,
},
],
};
}
const impactContext = buildTradeOpeningPriceImpactContext(
globalTradingVariables,
collateralIndex,
pairIndex,
{
currentBlock: blockNumber ?? 0,
contractsVersion: ContractsVersion.V10,
protectionCloseFactorWhitelist: false,
}
);
if (!impactContext) {
return null;
}
const impactForSizeUsd = (sizeUsd: number, long: boolean): number | null => {
try {
const result = getTradeOpeningPriceImpact(
{
collateralIndex,
pairIndex,
long,
collateralAmount: sizeUsd / impactContext.collateralPriceUsd,
leverage: 1,
openPrice: oraclePrice,
pairSpreadP: pair.spreadP,
fee,
contractsVersion: ContractsVersion.V10,
isCounterTrade: false,
},
impactContext
);
return result.totalPriceImpactPFromMarketPrice;
} catch {
return null;
}
};
const sizeForImpactUsd = (targetImpactPct: number, long: boolean): number => {
let minSizeUsd = 100;
let maxSizeUsd = 50_000_000;
const tolerance = Math.max(targetImpactPct * 0.1, 0.0001);
for (let i = 0; i < 20 && maxSizeUsd - minSizeUsd > 100; i++) {
const midSizeUsd = (minSizeUsd + maxSizeUsd) / 2;
const impactPct = impactForSizeUsd(midSizeUsd, long);
if (impactPct === null) {
maxSizeUsd = midSizeUsd;
continue;
}
const absImpactPct = Math.abs(impactPct);
if (Math.abs(absImpactPct - targetImpactPct) < tolerance) {
return midSizeUsd;
}
if (absImpactPct < targetImpactPct) {
minSizeUsd = midSizeUsd;
} else {
maxSizeUsd = midSizeUsd;
}
}
return (minSizeUsd + maxSizeUsd) / 2;
};
const buildSide = (long: boolean): VobLevel[] => {
const bands: Array<{ cumulativeUsd: number; impactPct: number }> = [];
const addBand = (cumulativeUsd: number) => {
if (bands.length >= MAX_LEVELS || cumulativeUsd < MIN_LEVEL_SIZE_USD) {
return;
}
const impactPct = impactForSizeUsd(cumulativeUsd, long);
if (impactPct === null || Math.abs(impactPct) > MAX_IMPACT_PCT) {
return;
}
if (
bands.some(
(band) => Math.abs(band.impactPct - impactPct) < IMPACT_SIMILARITY_THRESHOLD
)
) {
return;
}
bands.push({ cumulativeUsd, impactPct });
};
for (const targetImpactPct of TARGET_IMPACTS_PCT) {
addBand(sizeForImpactUsd(targetImpactPct, long));
}
for (const targetSizeUsd of TARGET_SIZES_USD) {
addBand(targetSizeUsd);
}
return bands
.sort((a, b) => a.cumulativeUsd - b.cumulativeUsd)
.slice(0, MAX_LEVELS)
.map((band, index, sortedBands) => ({
price: marketPrice * (1 + band.impactPct / 100),
sizeUsd:
band.cumulativeUsd - (index > 0 ? sortedBands[index - 1].cumulativeUsd : 0),
cumulativeUsd: band.cumulativeUsd,
impactPct: band.impactPct,
}));
};
return {
marketPrice,
asks: buildSide(true),
bids: buildSide(false),
};
}
const btcUsdcVob = await buildVirtualOrderBook(
"https://backend-arbitrum.gains.trade",
0, // BTC/USD
3 // USDC collateral on Arbitrum; verify the index from collaterals for your chain
);
console.log(btcUsdcVob);