mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-10 08:48:07 +00:00
fix(exchanges): restore type-check safety net and repair dead balance guards
tsconfig compiled docs/ (vendored ccxt samples) and @types/react was missing,
so 269 tsc errors buried the real ones. Scoping the project and adding the
missing type packages left 42 genuine errors in src/, which exposed two bugs:
- lighter: assertSpotBalance's buy branch and createOrder's spot-sell guard
compared the {available, wallet} object against a number, so both guards
were dead. Introduce SpotAssetBalance with a precomputed 'effective' field
(Extract Class) and route all three call sites through it.
- offset-maker: the below-min-sell branch logged 'skip sell' but pushed the
SELL order anyway, and pushed a possibly-null price. Both branches now
match their working sibling.
Also: widen LighterOrder's is_ask/reduce_only to BooleanFlag (the wire format
flags.ts already parses), extract sellableBase (Extract Function, 3 copies),
delete two scripts importing a module that does not exist, add a typecheck
script. tsc --noEmit: 269 -> 0 errors; 218 tests still pass.
This commit is contained in:
@@ -122,6 +122,31 @@ interface Pollers {
|
||||
klines: Map<string, ReturnType<typeof setInterval>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spot balance of a single asset. `effective` is what every balance guard compares
|
||||
* against: an unknown or unparseable asset collapses to 0 so guards fail closed
|
||||
* instead of waving an order through.
|
||||
*/
|
||||
interface SpotAssetBalance {
|
||||
available: number | null;
|
||||
wallet: number | null;
|
||||
effective: number;
|
||||
}
|
||||
|
||||
function makeSpotAssetBalance(available: number | null, wallet: number | null): SpotAssetBalance {
|
||||
return {
|
||||
available,
|
||||
wallet,
|
||||
effective: Math.max(
|
||||
available != null && Number.isFinite(available) ? available : 0,
|
||||
wallet != null && Number.isFinite(wallet) ? wallet : 0
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** Tolerance that absorbs float drift when comparing a balance against an order size. */
|
||||
const BALANCE_EPSILON = 1e-9;
|
||||
|
||||
const KLINE_DEFAULT_COUNT = 120;
|
||||
const DEFAULT_TICKER_POLL_MS = 3000;
|
||||
const DEFAULT_KLINE_POLL_MS = 15000;
|
||||
@@ -917,7 +942,7 @@ export class LighterGateway {
|
||||
return 0;
|
||||
});
|
||||
|
||||
return candidates[0];
|
||||
return candidates[0] ?? null;
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
@@ -1756,8 +1781,9 @@ export class LighterGateway {
|
||||
(matchSymbol ?? asset.symbol ?? (asset.asset_id != null ? String(asset.asset_id) : "ASSET")).toUpperCase();
|
||||
list.push({
|
||||
asset: assetSymbol,
|
||||
walletBalance: asset.balance ?? "0",
|
||||
availableBalance: available != null && Number.isFinite(available) ? available.toString() : asset.balance ?? "0",
|
||||
walletBalance: String(asset.balance ?? "0"),
|
||||
availableBalance:
|
||||
available != null && Number.isFinite(available) ? available.toString() : String(asset.balance ?? "0"),
|
||||
updateTime: now,
|
||||
assetId: Number.isFinite(assetId) ? assetId : undefined,
|
||||
});
|
||||
@@ -1796,7 +1822,7 @@ export class LighterGateway {
|
||||
return (hasBase && hasQuote) || idMatch;
|
||||
});
|
||||
if (!matches.length) return null;
|
||||
return matches[0];
|
||||
return matches[0] ?? null;
|
||||
}
|
||||
|
||||
async getPrecision(): Promise<{
|
||||
@@ -1846,8 +1872,7 @@ export class LighterGateway {
|
||||
return qty;
|
||||
}
|
||||
|
||||
private getAvailableAssetAmount(assetId?: number | null, symbol?: string | null): { available: number | null; wallet: number | null } {
|
||||
if (!this.assets.size) return null;
|
||||
private getSpotAssetBalance(assetId?: number | null, symbol?: string | null): SpotAssetBalance {
|
||||
const normalizedSymbol = symbol ? symbol.toUpperCase() : null;
|
||||
for (const asset of this.assets.values()) {
|
||||
const idMatches = assetId != null && Number.isFinite(Number(asset.asset_id)) && Number(asset.asset_id) === assetId;
|
||||
@@ -1857,29 +1882,23 @@ export class LighterGateway {
|
||||
const locked = parseNumber(asset.locked_balance ?? 0);
|
||||
if (balance == null) continue;
|
||||
const available = locked != null ? balance - locked : balance;
|
||||
return {
|
||||
available: Number.isFinite(available) ? available : null,
|
||||
wallet: Number.isFinite(balance) ? balance : null,
|
||||
};
|
||||
return makeSpotAssetBalance(
|
||||
Number.isFinite(available) ? available : null,
|
||||
Number.isFinite(balance) ? balance : null
|
||||
);
|
||||
}
|
||||
return { available: null, wallet: null };
|
||||
return makeSpotAssetBalance(null, null);
|
||||
}
|
||||
|
||||
private assertSpotBalance(params: { isAsk: boolean; quantity: number | null | undefined; price: number | null }): void {
|
||||
const qty = Number(params.quantity);
|
||||
if (!Number.isFinite(qty) || qty <= 0) return;
|
||||
if (params.isAsk) {
|
||||
const baseAmounts = this.getAvailableAssetAmount(this.baseAssetId, this.baseAssetSymbol);
|
||||
const availableBase = baseAmounts?.available ?? null;
|
||||
const walletBase = baseAmounts?.wallet ?? null;
|
||||
const effective = Math.max(
|
||||
availableBase != null && Number.isFinite(availableBase) ? availableBase : 0,
|
||||
walletBase != null && Number.isFinite(walletBase) ? walletBase : 0
|
||||
);
|
||||
if (effective + 1e-9 < qty) {
|
||||
const base = this.getSpotAssetBalance(this.baseAssetId, this.baseAssetSymbol);
|
||||
if (base.effective + BALANCE_EPSILON < qty) {
|
||||
throw new Error(
|
||||
`Insufficient base asset (${this.baseAssetSymbol ?? "BASE"} available ${availableBase ?? 0}${
|
||||
walletBase != null ? ` wallet ${walletBase}` : ""
|
||||
`Insufficient base asset (${this.baseAssetSymbol ?? "BASE"} available ${base.available ?? 0}${
|
||||
base.wallet != null ? ` wallet ${base.wallet}` : ""
|
||||
}) for spot sell ${qty}`
|
||||
);
|
||||
}
|
||||
@@ -1888,10 +1907,12 @@ export class LighterGateway {
|
||||
const price = Number(params.price);
|
||||
if (!Number.isFinite(price) || price <= 0) return;
|
||||
const requiredQuote = qty * price;
|
||||
const availableQuote = this.getAvailableAssetAmount(this.quoteAssetId, this.quoteAssetSymbol);
|
||||
if (availableQuote != null && availableQuote + 1e-9 < requiredQuote) {
|
||||
const quote = this.getSpotAssetBalance(this.quoteAssetId, this.quoteAssetSymbol);
|
||||
if (quote.effective + BALANCE_EPSILON < requiredQuote) {
|
||||
throw new Error(
|
||||
`Insufficient quote asset (${this.quoteAssetSymbol ?? "QUOTE"} available ${availableQuote}) for spot buy requiring ${requiredQuote}`
|
||||
`Insufficient quote asset (${this.quoteAssetSymbol ?? "QUOTE"} available ${
|
||||
quote.available ?? 0
|
||||
}) for spot buy requiring ${requiredQuote}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1932,10 +1953,10 @@ export class LighterGateway {
|
||||
const isAsk = side === "SELL" ? 1 : 0;
|
||||
const enforcedQty = this.enforceMinimums(params.quantity, params.price ?? null);
|
||||
if (this.isSpotMarket() && isAsk === 1) {
|
||||
const availableBase = this.getAvailableAssetAmount(this.baseAssetId, this.baseAssetSymbol);
|
||||
if (availableBase != null && availableBase + 1e-9 < enforcedQty) {
|
||||
const base = this.getSpotAssetBalance(this.baseAssetId, this.baseAssetSymbol);
|
||||
if (base.effective + BALANCE_EPSILON < enforcedQty) {
|
||||
throw new Error(
|
||||
`Spot sell quantity ${enforcedQty} exceeds available base ${availableBase} (min trade size may be higher than balance)`
|
||||
`Spot sell quantity ${enforcedQty} exceeds available base ${base.effective} (min trade size may be higher than balance)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function lighterOrderToAster(symbol: string, order: LighterOrder): Order
|
||||
symbol,
|
||||
side,
|
||||
type: mapOrderType(order.type),
|
||||
status: normalizeOrderStatus(order.status ?? order.trigger_status ?? "UNKNOWN"),
|
||||
status: normalizeOrderStatus(String(order.status ?? order.trigger_status ?? "UNKNOWN")),
|
||||
price: order.price ?? "0",
|
||||
origQty: order.initial_base_amount ?? "0",
|
||||
executedQty: computeExecutedQty(order),
|
||||
|
||||
@@ -10,6 +10,12 @@ export type LighterOrderType =
|
||||
|
||||
type StrOrNum = string | number;
|
||||
|
||||
/**
|
||||
* Lighter reports boolean fields inconsistently across endpoints — `true`, `1`, `"Yes"`.
|
||||
* `normalizeBooleanFlag` in ./flags is what turns any of these into a real boolean.
|
||||
*/
|
||||
type BooleanFlag = boolean | string | number | bigint;
|
||||
|
||||
export interface LighterOrder {
|
||||
order_index: StrOrNum;
|
||||
client_order_index: StrOrNum;
|
||||
@@ -23,12 +29,12 @@ export interface LighterOrder {
|
||||
filled_quote_amount?: string;
|
||||
price: string;
|
||||
nonce?: number;
|
||||
is_ask?: boolean;
|
||||
is_ask?: BooleanFlag;
|
||||
side?: LighterSide;
|
||||
type?: LighterOrderType;
|
||||
time_in_force?: string;
|
||||
trigger_price?: string;
|
||||
reduce_only?: boolean;
|
||||
reduce_only?: BooleanFlag;
|
||||
status?: string | number;
|
||||
trigger_status?: string | number;
|
||||
trigger_time?: number;
|
||||
@@ -68,7 +74,8 @@ export interface LighterAccountDetails {
|
||||
}
|
||||
|
||||
export interface LighterAccountAsset {
|
||||
symbol: string;
|
||||
/** Optional: the account endpoint omits it for assets it only knows by id. */
|
||||
symbol?: string;
|
||||
asset_id?: number;
|
||||
balance: string | number;
|
||||
locked_balance?: string | number;
|
||||
|
||||
Reference in New Issue
Block a user