From e94cf1bda2c14e1d63987b71c6cc8e37f6281de3 Mon Sep 17 00:00:00 2001 From: discountry Date: Sat, 8 Nov 2025 17:14:01 +0800 Subject: [PATCH] feat: enhance toAccountSnapshot function with improved market ID and symbol matching logic, and add corresponding unit tests --- src/exchanges/lighter/mappers.ts | 45 ++++++++++++++++++++----- tests/lighter/mappers.test.ts | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 tests/lighter/mappers.test.ts diff --git a/src/exchanges/lighter/mappers.ts b/src/exchanges/lighter/mappers.ts index 5e4c21c..acf572f 100644 --- a/src/exchanges/lighter/mappers.ts +++ b/src/exchanges/lighter/mappers.ts @@ -144,16 +144,23 @@ export function toAccountSnapshot( assets: AsterAccountAsset[] = [], options?: { marketSymbol?: string | null; marketId?: number | null } ): AsterAccountSnapshot { - const targetSymbol = options?.marketSymbol?.toUpperCase(); - const targetMarketId = options?.marketId; + const targetSymbol = options?.marketSymbol ?? null; + const targetMarketId = + options?.marketId != null && Number.isFinite(Number(options.marketId)) + ? Number(options.marketId) + : null; const filteredPositions = positions.filter((position) => { - const marketMatches = - targetMarketId == null || - (Number.isFinite(Number(position.market_id)) && Number(position.market_id) === Number(targetMarketId)); - const symbolMatches = - !targetSymbol || - (typeof position.symbol === "string" && position.symbol.toUpperCase() === targetSymbol); - return marketMatches && symbolMatches; + if (targetMarketId != null) { + const positionMarketId = Number(position.market_id); + if (Number.isFinite(positionMarketId)) { + return positionMarketId === targetMarketId; + } + return targetSymbol ? symbolsMatch(position.symbol, targetSymbol) : false; + } + if (targetSymbol) { + return symbolsMatch(position.symbol, targetSymbol); + } + return true; }); const transformedPositions = filteredPositions.map((position) => lighterPositionToAster(symbol, position)); const aggregateUnrealized = transformedPositions.reduce((acc, pos) => acc + Number(pos.unrealizedProfit ?? 0), 0); @@ -201,3 +208,23 @@ function lighterPositionToAster(symbol: string, position: LighterPosition): Aste markPrice: undefined, }; } + +function symbolsMatch(source: string | null | undefined, target: string | null | undefined): boolean { + if (!source || !target) return false; + const sourceForms = normalizeSymbolForms(source); + const targetForms = normalizeSymbolForms(target); + if (!sourceForms.length || !targetForms.length) return false; + return sourceForms.some((value) => targetForms.includes(value)); +} + +function normalizeSymbolForms(value: string): string[] { + const upper = value.toUpperCase(); + const sanitized = upper.replace(/[^A-Z0-9]/g, ""); + const parts = upper.split(/[-:/]/).filter(Boolean); + const base = parts.length ? parts[0] : ""; + const forms = new Set(); + if (upper) forms.add(upper); + if (sanitized) forms.add(sanitized); + if (base) forms.add(base); + return Array.from(forms); +} diff --git a/tests/lighter/mappers.test.ts b/tests/lighter/mappers.test.ts new file mode 100644 index 0000000..7417397 --- /dev/null +++ b/tests/lighter/mappers.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { toAccountSnapshot } from "../../src/exchanges/lighter/mappers"; +import type { LighterAccountDetails, LighterPosition } from "../../src/exchanges/lighter/types"; + +const baseDetails: LighterAccountDetails = { + account_index: 1, + collateral: "1000", +}; + +function createPosition(overrides: Partial = {}): LighterPosition { + return { + market_id: 101, + symbol: "BTC/USDC:USDC", + sign: 1, + position: "0.5", + avg_entry_price: "100", + position_value: "50", + unrealized_pnl: "0", + realized_pnl: "0", + ...overrides, + } as LighterPosition; +} + +describe("toAccountSnapshot", () => { + it("includes positions that match the configured market id regardless of reported symbol format", () => { + const snapshot = toAccountSnapshot( + "BTC", + baseDetails, + [ + createPosition({ market_id: 101, symbol: "BTC/USDC:USDC" }), + createPosition({ market_id: 202, symbol: "ETH/USDC:USDC", sign: -1 }), + ], + [], + { marketSymbol: "BTC", marketId: 101 } + ); + + expect(snapshot.positions).toHaveLength(1); + expect(snapshot.positions[0]).toMatchObject({ symbol: "BTC", positionAmt: "0.5" }); + }); + + it("falls back to fuzzy symbol matching when market id is unavailable", () => { + const snapshot = toAccountSnapshot( + "BTC", + baseDetails, + [ + createPosition({ market_id: Number.NaN as number, symbol: "BTC/USDC:USDC" }), + createPosition({ market_id: Number.NaN as number, symbol: "ETH/USDC:USDC" }), + ], + [], + { marketSymbol: "BTC" } + ); + + expect(snapshot.positions).toHaveLength(1); + expect(snapshot.positions[0]).toMatchObject({ symbol: "BTC", positionAmt: "0.5" }); + }); +});