mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18: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:
@@ -21,6 +21,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"oxlint": "^1.54.0",
|
"oxlint": "^1.54.0",
|
||||||
"vitest": "^4.0.18",
|
"vitest": "^4.0.18",
|
||||||
},
|
},
|
||||||
@@ -208,6 +210,10 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||||
|
|
||||||
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
|
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
|
||||||
@@ -260,6 +266,8 @@
|
|||||||
|
|
||||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"start": "bun run index.ts",
|
"start": "bun run index.ts",
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"lint:fix": "oxlint --fix",
|
"lint:fix": "oxlint --fix",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "bun x vitest run",
|
"test": "bun x vitest run",
|
||||||
"test:exchange-contract": "bun x vitest run tests/exchange-contract-suite.test.ts tests/exchange-factory.test.ts tests/config.test.ts",
|
"test:exchange-contract": "bun x vitest run tests/exchange-contract-suite.test.ts tests/exchange-factory.test.ts tests/config.test.ts",
|
||||||
"test:watch": "bun x vitest",
|
"test:watch": "bun x vitest",
|
||||||
@@ -29,6 +30,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"oxlint": "^1.54.0",
|
"oxlint": "^1.54.0",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { LighterPrivateKey } from "../src/exchanges/lighter/crypto/schnorr";
|
|
||||||
import { bytesToHex } from "../src/exchanges/lighter/bytes";
|
|
||||||
|
|
||||||
function derive(keyHex: string): string {
|
|
||||||
const normalized = keyHex.startsWith("0x") ? keyHex.slice(2) : keyHex;
|
|
||||||
const key = LighterPrivateKey.fromHex(normalized);
|
|
||||||
return bytesToHex(key.publicKey().toBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = process.argv[2];
|
|
||||||
if (!input) {
|
|
||||||
console.error("usage: bun run scripts/derive-public.ts <hex>");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(derive(input));
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import { LighterPrivateKey } from "../src/exchanges/lighter/crypto/schnorr";
|
|
||||||
import { bytesToHex } from "../src/exchanges/lighter/bytes";
|
|
||||||
|
|
||||||
function main(): void {
|
|
||||||
const raw = process.env.LIGHTER_API_PRIVATE_KEY;
|
|
||||||
if (!raw) {
|
|
||||||
throw new Error("LIGHTER_API_PRIVATE_KEY env var is required");
|
|
||||||
}
|
|
||||||
const normalized = raw.startsWith("0x") ? raw.slice(2) : raw;
|
|
||||||
const key = LighterPrivateKey.fromHex(normalized);
|
|
||||||
const publicKeyHex = bytesToHex(key.publicKey().toBytes());
|
|
||||||
const apiKeyIndex = process.env.LIGHTER_API_KEY_INDEX ?? "(not set)";
|
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
|
||||||
apiKeyIndex,
|
|
||||||
publicKeyHex,
|
|
||||||
}, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -122,6 +122,31 @@ interface Pollers {
|
|||||||
klines: Map<string, ReturnType<typeof setInterval>>;
|
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 KLINE_DEFAULT_COUNT = 120;
|
||||||
const DEFAULT_TICKER_POLL_MS = 3000;
|
const DEFAULT_TICKER_POLL_MS = 3000;
|
||||||
const DEFAULT_KLINE_POLL_MS = 15000;
|
const DEFAULT_KLINE_POLL_MS = 15000;
|
||||||
@@ -917,7 +942,7 @@ export class LighterGateway {
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
return candidates[0];
|
return candidates[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
@@ -1756,8 +1781,9 @@ export class LighterGateway {
|
|||||||
(matchSymbol ?? asset.symbol ?? (asset.asset_id != null ? String(asset.asset_id) : "ASSET")).toUpperCase();
|
(matchSymbol ?? asset.symbol ?? (asset.asset_id != null ? String(asset.asset_id) : "ASSET")).toUpperCase();
|
||||||
list.push({
|
list.push({
|
||||||
asset: assetSymbol,
|
asset: assetSymbol,
|
||||||
walletBalance: asset.balance ?? "0",
|
walletBalance: String(asset.balance ?? "0"),
|
||||||
availableBalance: available != null && Number.isFinite(available) ? available.toString() : asset.balance ?? "0",
|
availableBalance:
|
||||||
|
available != null && Number.isFinite(available) ? available.toString() : String(asset.balance ?? "0"),
|
||||||
updateTime: now,
|
updateTime: now,
|
||||||
assetId: Number.isFinite(assetId) ? assetId : undefined,
|
assetId: Number.isFinite(assetId) ? assetId : undefined,
|
||||||
});
|
});
|
||||||
@@ -1796,7 +1822,7 @@ export class LighterGateway {
|
|||||||
return (hasBase && hasQuote) || idMatch;
|
return (hasBase && hasQuote) || idMatch;
|
||||||
});
|
});
|
||||||
if (!matches.length) return null;
|
if (!matches.length) return null;
|
||||||
return matches[0];
|
return matches[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrecision(): Promise<{
|
async getPrecision(): Promise<{
|
||||||
@@ -1846,8 +1872,7 @@ export class LighterGateway {
|
|||||||
return qty;
|
return qty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAvailableAssetAmount(assetId?: number | null, symbol?: string | null): { available: number | null; wallet: number | null } {
|
private getSpotAssetBalance(assetId?: number | null, symbol?: string | null): SpotAssetBalance {
|
||||||
if (!this.assets.size) return null;
|
|
||||||
const normalizedSymbol = symbol ? symbol.toUpperCase() : null;
|
const normalizedSymbol = symbol ? symbol.toUpperCase() : null;
|
||||||
for (const asset of this.assets.values()) {
|
for (const asset of this.assets.values()) {
|
||||||
const idMatches = assetId != null && Number.isFinite(Number(asset.asset_id)) && Number(asset.asset_id) === assetId;
|
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);
|
const locked = parseNumber(asset.locked_balance ?? 0);
|
||||||
if (balance == null) continue;
|
if (balance == null) continue;
|
||||||
const available = locked != null ? balance - locked : balance;
|
const available = locked != null ? balance - locked : balance;
|
||||||
return {
|
return makeSpotAssetBalance(
|
||||||
available: Number.isFinite(available) ? available : null,
|
Number.isFinite(available) ? available : null,
|
||||||
wallet: Number.isFinite(balance) ? balance : 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 {
|
private assertSpotBalance(params: { isAsk: boolean; quantity: number | null | undefined; price: number | null }): void {
|
||||||
const qty = Number(params.quantity);
|
const qty = Number(params.quantity);
|
||||||
if (!Number.isFinite(qty) || qty <= 0) return;
|
if (!Number.isFinite(qty) || qty <= 0) return;
|
||||||
if (params.isAsk) {
|
if (params.isAsk) {
|
||||||
const baseAmounts = this.getAvailableAssetAmount(this.baseAssetId, this.baseAssetSymbol);
|
const base = this.getSpotAssetBalance(this.baseAssetId, this.baseAssetSymbol);
|
||||||
const availableBase = baseAmounts?.available ?? null;
|
if (base.effective + BALANCE_EPSILON < qty) {
|
||||||
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) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Insufficient base asset (${this.baseAssetSymbol ?? "BASE"} available ${availableBase ?? 0}${
|
`Insufficient base asset (${this.baseAssetSymbol ?? "BASE"} available ${base.available ?? 0}${
|
||||||
walletBase != null ? ` wallet ${walletBase}` : ""
|
base.wallet != null ? ` wallet ${base.wallet}` : ""
|
||||||
}) for spot sell ${qty}`
|
}) for spot sell ${qty}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1888,10 +1907,12 @@ export class LighterGateway {
|
|||||||
const price = Number(params.price);
|
const price = Number(params.price);
|
||||||
if (!Number.isFinite(price) || price <= 0) return;
|
if (!Number.isFinite(price) || price <= 0) return;
|
||||||
const requiredQuote = qty * price;
|
const requiredQuote = qty * price;
|
||||||
const availableQuote = this.getAvailableAssetAmount(this.quoteAssetId, this.quoteAssetSymbol);
|
const quote = this.getSpotAssetBalance(this.quoteAssetId, this.quoteAssetSymbol);
|
||||||
if (availableQuote != null && availableQuote + 1e-9 < requiredQuote) {
|
if (quote.effective + BALANCE_EPSILON < requiredQuote) {
|
||||||
throw new Error(
|
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 isAsk = side === "SELL" ? 1 : 0;
|
||||||
const enforcedQty = this.enforceMinimums(params.quantity, params.price ?? null);
|
const enforcedQty = this.enforceMinimums(params.quantity, params.price ?? null);
|
||||||
if (this.isSpotMarket() && isAsk === 1) {
|
if (this.isSpotMarket() && isAsk === 1) {
|
||||||
const availableBase = this.getAvailableAssetAmount(this.baseAssetId, this.baseAssetSymbol);
|
const base = this.getSpotAssetBalance(this.baseAssetId, this.baseAssetSymbol);
|
||||||
if (availableBase != null && availableBase + 1e-9 < enforcedQty) {
|
if (base.effective + BALANCE_EPSILON < enforcedQty) {
|
||||||
throw new Error(
|
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,
|
symbol,
|
||||||
side,
|
side,
|
||||||
type: mapOrderType(order.type),
|
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",
|
price: order.price ?? "0",
|
||||||
origQty: order.initial_base_amount ?? "0",
|
origQty: order.initial_base_amount ?? "0",
|
||||||
executedQty: computeExecutedQty(order),
|
executedQty: computeExecutedQty(order),
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ export type LighterOrderType =
|
|||||||
|
|
||||||
type StrOrNum = string | number;
|
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 {
|
export interface LighterOrder {
|
||||||
order_index: StrOrNum;
|
order_index: StrOrNum;
|
||||||
client_order_index: StrOrNum;
|
client_order_index: StrOrNum;
|
||||||
@@ -23,12 +29,12 @@ export interface LighterOrder {
|
|||||||
filled_quote_amount?: string;
|
filled_quote_amount?: string;
|
||||||
price: string;
|
price: string;
|
||||||
nonce?: number;
|
nonce?: number;
|
||||||
is_ask?: boolean;
|
is_ask?: BooleanFlag;
|
||||||
side?: LighterSide;
|
side?: LighterSide;
|
||||||
type?: LighterOrderType;
|
type?: LighterOrderType;
|
||||||
time_in_force?: string;
|
time_in_force?: string;
|
||||||
trigger_price?: string;
|
trigger_price?: string;
|
||||||
reduce_only?: boolean;
|
reduce_only?: BooleanFlag;
|
||||||
status?: string | number;
|
status?: string | number;
|
||||||
trigger_status?: string | number;
|
trigger_status?: string | number;
|
||||||
trigger_time?: number;
|
trigger_time?: number;
|
||||||
@@ -68,7 +74,8 @@ export interface LighterAccountDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LighterAccountAsset {
|
export interface LighterAccountAsset {
|
||||||
symbol: string;
|
/** Optional: the account endpoint omits it for assets it only knows by id. */
|
||||||
|
symbol?: string;
|
||||||
asset_id?: number;
|
asset_id?: number;
|
||||||
balance: string | number;
|
balance: string | number;
|
||||||
locked_balance?: string | number;
|
locked_balance?: string | number;
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ export class ParadexGateway {
|
|||||||
const isClosePosition = (extraParams as any).closePosition === true;
|
const isClosePosition = (extraParams as any).closePosition === true;
|
||||||
if (isClosePosition) {
|
if (isClosePosition) {
|
||||||
const posAbs = this.getCurrentPositionAbs();
|
const posAbs = this.getCurrentPositionAbs();
|
||||||
if (Number.isFinite(posAbs) && posAbs > 0) {
|
if (posAbs != null && Number.isFinite(posAbs) && posAbs > 0) {
|
||||||
amount = posAbs;
|
amount = posAbs;
|
||||||
}
|
}
|
||||||
const current = Number(amount);
|
const current = Number(amount);
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ interface DesiredOrder {
|
|||||||
reduceOnly: boolean;
|
reduceOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Spot wallet view the quoting logic reads; `baseWallet` may lag `baseAvailable` after a fill. */
|
||||||
|
export interface SpotBalances {
|
||||||
|
baseAvailable: number;
|
||||||
|
quoteAvailable: number;
|
||||||
|
baseWallet: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OffsetMakerEngineSnapshot extends MakerEngineSnapshot {
|
export interface OffsetMakerEngineSnapshot extends MakerEngineSnapshot {
|
||||||
buyDepthSum10: number;
|
buyDepthSum10: number;
|
||||||
sellDepthSum10: number;
|
sellDepthSum10: number;
|
||||||
@@ -269,6 +276,7 @@ export class OffsetMakerEngine {
|
|||||||
(klines) => {
|
(klines) => {
|
||||||
if (!Array.isArray(klines) || !klines.length) return;
|
if (!Array.isArray(klines) || !klines.length) return;
|
||||||
const latest = klines[klines.length - 1];
|
const latest = klines[klines.length - 1];
|
||||||
|
if (!latest) return;
|
||||||
this.lastKline = latest;
|
this.lastKline = latest;
|
||||||
const open = Number(latest.open);
|
const open = Number(latest.open);
|
||||||
const close = Number(latest.close);
|
const close = Number(latest.close);
|
||||||
@@ -340,7 +348,9 @@ export class OffsetMakerEngine {
|
|||||||
const position = this.getPositionSnapshot();
|
const position = this.getPositionSnapshot();
|
||||||
const isSpotMarket = this.marketType === "spot";
|
const isSpotMarket = this.marketType === "spot";
|
||||||
const spotBalances = isSpotMarket ? this.getSpotBalances() : null;
|
const spotBalances = isSpotMarket ? this.getSpotBalances() : null;
|
||||||
const balancesForSpot = isSpotMarket ? spotBalances ?? { baseAvailable: 0, quoteAvailable: 0 } : spotBalances;
|
const balancesForSpot = isSpotMarket
|
||||||
|
? spotBalances ?? { baseAvailable: 0, quoteAvailable: 0, baseWallet: 0 }
|
||||||
|
: spotBalances;
|
||||||
this.updateLiveCandle();
|
this.updateLiveCandle();
|
||||||
const handledImbalance = await this.handleImbalanceExit(position, buySum, sellSum);
|
const handledImbalance = await this.handleImbalanceExit(position, buySum, sellSum);
|
||||||
if (handledImbalance) {
|
if (handledImbalance) {
|
||||||
@@ -392,9 +402,7 @@ export class OffsetMakerEngine {
|
|||||||
|
|
||||||
if (absPosition < EPS && isSpotMarket) {
|
if (absPosition < EPS && isSpotMarket) {
|
||||||
this.entryPricePendingLogged = false;
|
this.entryPricePendingLogged = false;
|
||||||
const baseAvail = balancesForSpot?.baseAvailable ?? 0;
|
const maxBase = this.sellableBase(balancesForSpot);
|
||||||
const baseWallet = balancesForSpot?.baseWallet ?? baseAvail;
|
|
||||||
const maxBase = Math.max(baseAvail, baseWallet);
|
|
||||||
if (isSpotMarket && minSell > 0 && maxBase + EPS < minSell) {
|
if (isSpotMarket && minSell > 0 && maxBase + EPS < minSell) {
|
||||||
// 无法卖出,跳过卖单,允许买单累计
|
// 无法卖出,跳过卖单,允许买单累计
|
||||||
this.lastSellPriceViable = false;
|
this.lastSellPriceViable = false;
|
||||||
@@ -429,9 +437,7 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!skipSellSide && canEnter) {
|
if (!skipSellSide && canEnter) {
|
||||||
const baseAvail = balancesForSpot?.baseAvailable ?? 0;
|
const maxBase = this.sellableBase(balancesForSpot);
|
||||||
const baseWallet = balancesForSpot?.baseWallet ?? baseAvail;
|
|
||||||
const maxBase = Math.max(baseAvail, baseWallet);
|
|
||||||
if (isSpotMarket && minSell > 0 && maxBase + EPS < minSell) {
|
if (isSpotMarket && minSell > 0 && maxBase + EPS < minSell) {
|
||||||
// 持仓低于最小卖单量,跳过卖单,等待累积
|
// 持仓低于最小卖单量,跳过卖单,等待累积
|
||||||
if (this.lastSellPriceViable) {
|
if (this.lastSellPriceViable) {
|
||||||
@@ -468,20 +474,22 @@ export class OffsetMakerEngine {
|
|||||||
this.tradeLog.push("info", "现货买入仅在1m阳线,当前跳过买单");
|
this.tradeLog.push("info", "现货买入仅在1m阳线,当前跳过买单");
|
||||||
this.lastBuyPriceViable = false;
|
this.lastBuyPriceViable = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (bidPrice != null) {
|
||||||
desired.push({ side: "BUY", price: bidPrice, amount: this.config.tradeAmount, reduceOnly: false });
|
desired.push({ side: "BUY", price: bidPrice, amount: this.config.tradeAmount, reduceOnly: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!skipSellSide && canEnter) {
|
if (!skipSellSide && canEnter) {
|
||||||
if (isSpotMarket && minSell > 0 && this.minBaseAmount != null) {
|
const belowMinSell =
|
||||||
const baseAvail = balancesForSpot?.baseAvailable ?? 0;
|
isSpotMarket &&
|
||||||
const baseWallet = balancesForSpot?.baseWallet ?? baseAvail;
|
minSell > 0 &&
|
||||||
if (Math.max(baseAvail, baseWallet) + EPS < minSell) {
|
this.minBaseAmount != null &&
|
||||||
this.lastSellPriceViable = false;
|
this.sellableBase(balancesForSpot) + EPS < minSell;
|
||||||
this.tradeLog.push("info", "现货持仓低于最小卖单量,跳过卖单");
|
if (belowMinSell) {
|
||||||
}
|
this.lastSellPriceViable = false;
|
||||||
|
this.tradeLog.push("info", "现货持仓低于最小卖单量,跳过卖单");
|
||||||
|
} else if (askPrice != null) {
|
||||||
|
desired.push({ side: "SELL", price: askPrice, amount: this.config.tradeAmount, reduceOnly: false });
|
||||||
}
|
}
|
||||||
desired.push({ side: "SELL", price: askPrice, amount: this.config.tradeAmount, reduceOnly: false });
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
|
const closeSide: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
|
||||||
@@ -1012,6 +1020,16 @@ export class OffsetMakerEngine {
|
|||||||
return this.spotKlineUp === true || this.isLiveCandleUp();
|
return this.spotKlineUp === true || this.isLiveCandleUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base asset the venue will actually let us sell. Wallet balance can exceed the
|
||||||
|
* available figure right after a fill settles, so the larger of the two wins.
|
||||||
|
*/
|
||||||
|
private sellableBase(balances: SpotBalances | null): number {
|
||||||
|
const available = balances?.baseAvailable ?? 0;
|
||||||
|
const wallet = balances?.baseWallet ?? available;
|
||||||
|
return Math.max(available, wallet);
|
||||||
|
}
|
||||||
|
|
||||||
private isLiveCandleUp(): boolean {
|
private isLiveCandleUp(): boolean {
|
||||||
if (!this.liveCandle) return false;
|
if (!this.liveCandle) return false;
|
||||||
return this.liveCandle.close > this.liveCandle.open;
|
return this.liveCandle.close > this.liveCandle.open;
|
||||||
|
|||||||
@@ -50,6 +50,6 @@ describe("LighterSigner", () => {
|
|||||||
expect(signed.txHash.length).toBeGreaterThan(0);
|
expect(signed.txHash.length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
expect(typeof signed.signature).toBe("string");
|
expect(typeof signed.signature).toBe("string");
|
||||||
expect(signed.signature.length).toBeGreaterThan(0);
|
expect(signed.signature?.length ?? 0).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,10 +60,11 @@ describe("MakerPointsEngine defense-mode REST polling", () => {
|
|||||||
price: "100",
|
price: "100",
|
||||||
origQty: "1",
|
origQty: "1",
|
||||||
executedQty: "0",
|
executedQty: "0",
|
||||||
|
stopPrice: "0",
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
updateTime: Date.now(),
|
updateTime: Date.now(),
|
||||||
reduceOnly: "false",
|
reduceOnly: false,
|
||||||
closePosition: "false",
|
closePosition: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ describe("order-coordinator", () => {
|
|||||||
timers,
|
timers,
|
||||||
pending,
|
pending,
|
||||||
"BUY",
|
"BUY",
|
||||||
100,
|
"100",
|
||||||
1,
|
1,
|
||||||
log,
|
log,
|
||||||
false
|
false
|
||||||
|
|||||||
+6
-1
@@ -25,5 +25,10 @@
|
|||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// docs/ holds vendored third-party samples (ccxt examples); they are reference
|
||||||
|
// material, not compilation units, and their errors mask real ones in src/.
|
||||||
|
"include": ["index.ts", "src/**/*", "tests/**/*", "scripts/**/*"],
|
||||||
|
"exclude": ["node_modules", "docs"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user