mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
Add changeMarginMode method to ExchangeAdapter and Standx classes
- Introduced `changeMarginMode` method in `ExchangeAdapter` interface to allow margin mode adjustments. - Implemented the `changeMarginMode` method in `StandxExchangeAdapter` to interact with the gateway for changing margin modes. - Added corresponding `changeMarginMode` method in `StandxGateway` to handle API requests for margin mode changes. - Enhanced `MakerPointsEngine` to ensure isolated margin mode before order placement, with appropriate logging and defense mode activation if the change fails. - Created tests for margin mode functionality to validate behavior under different scenarios.
This commit is contained in:
@@ -84,5 +84,6 @@ export interface ExchangeAdapter {
|
|||||||
offRestHealthEvent?(listener: RestHealthListener): void;
|
offRestHealthEvent?(listener: RestHealthListener): void;
|
||||||
queryOpenOrders?(): Promise<AsterOrder[]>;
|
queryOpenOrders?(): Promise<AsterOrder[]>;
|
||||||
queryAccountSnapshot?(): Promise<AsterAccountSnapshot | null>;
|
queryAccountSnapshot?(): Promise<AsterAccountSnapshot | null>;
|
||||||
|
changeMarginMode?(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void>;
|
||||||
forceCancelAllOrders?(): Promise<boolean>;
|
forceCancelAllOrders?(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,11 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
|||||||
return this.gateway.queryAccountSnapshot();
|
return this.gateway.queryAccountSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||||
|
await this.ensureInitialized("changeMarginMode");
|
||||||
|
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 强制取消所有挂单
|
* 强制取消所有挂单
|
||||||
* 会查询当前挂单然后取消,并验证取消成功
|
* 会查询当前挂单然后取消,并验证取消成功
|
||||||
|
|||||||
@@ -1462,6 +1462,30 @@ export class StandxGateway {
|
|||||||
return await this.refreshAccountSnapshot();
|
return await this.refreshAccountSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeMarginMode(symbol: string, marginMode: "isolated" | "cross"): Promise<void> {
|
||||||
|
if (!this.signer.hasKey()) {
|
||||||
|
throw new Error("StandX change_margin_mode requires STANDX_REQUEST_PRIVATE_KEY for signed requests");
|
||||||
|
}
|
||||||
|
const normalized = normalizeSymbol(symbol);
|
||||||
|
const response = await this.requestJson<{ code?: number; message?: string; request_id?: string }>(
|
||||||
|
"/api/change_margin_mode",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
symbol: normalized,
|
||||||
|
margin_mode: marginMode,
|
||||||
|
},
|
||||||
|
signed: true,
|
||||||
|
extraHeaders: {
|
||||||
|
"x-session-id": this.sessionId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response && typeof response.code === "number" && response.code !== 0) {
|
||||||
|
throw new Error(response.message ?? "StandX change margin mode rejected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async refreshOpenOrders(symbol: string): Promise<void> {
|
private async refreshOpenOrders(symbol: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", {
|
const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", {
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ const DATA_STALE_THRESHOLD_MS = 5_000; // 数据过时阈值(5秒)
|
|||||||
const DEFENSE_MODE_CHECK_INTERVAL_MS = 1000; // 防御模式检查间隔
|
const DEFENSE_MODE_CHECK_INTERVAL_MS = 1000; // 防御模式检查间隔
|
||||||
const ACCOUNT_DATA_STALE_THRESHOLD_MS = 20_000; // 账户数据长期无更新阈值(StandX 有 REST 兜底,长期无更新通常意味着异常)
|
const ACCOUNT_DATA_STALE_THRESHOLD_MS = 20_000; // 账户数据长期无更新阈值(StandX 有 REST 兜底,长期无更新通常意味着异常)
|
||||||
const STANDX_REST_ERROR_DEFENSE_THRESHOLD = 3;
|
const STANDX_REST_ERROR_DEFENSE_THRESHOLD = 3;
|
||||||
|
const STANDX_MARGIN_MODE_CHECK_INTERVAL_MS = 500;
|
||||||
|
const STANDX_MARGIN_MODE_MAX_ATTEMPTS = 10;
|
||||||
|
|
||||||
export class MakerPointsEngine {
|
export class MakerPointsEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||||
@@ -185,6 +187,7 @@ export class MakerPointsEngine {
|
|||||||
private standxRestConsecutiveErrors = 0;
|
private standxRestConsecutiveErrors = 0;
|
||||||
private standxRestUnhealthy = false;
|
private standxRestUnhealthy = false;
|
||||||
private standxRestLastError: string | null = null;
|
private standxRestLastError: string | null = null;
|
||||||
|
private marginModeEnsuring: Promise<boolean> | null = null;
|
||||||
|
|
||||||
constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) {
|
constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) {
|
||||||
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
||||||
@@ -399,6 +402,8 @@ export class MakerPointsEngine {
|
|||||||
standxRestUnhealthy: true,
|
standxRestUnhealthy: true,
|
||||||
standxRestConsecutiveErrors: this.standxRestConsecutiveErrors,
|
standxRestConsecutiveErrors: this.standxRestConsecutiveErrors,
|
||||||
standxRestLastError: this.standxRestLastError,
|
standxRestLastError: this.standxRestLastError,
|
||||||
|
marginModeNotIsolated: false,
|
||||||
|
marginMode: this.getStandxMarginMode(this.accountSnapshot),
|
||||||
standxDepthAge: 0,
|
standxDepthAge: 0,
|
||||||
binanceAge: 0,
|
binanceAge: 0,
|
||||||
standxAccountAge: 0,
|
standxAccountAge: 0,
|
||||||
@@ -535,6 +540,27 @@ export class MakerPointsEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await this.ensureStandxIsolatedMarginMode())) {
|
||||||
|
const current = this.getStandxMarginMode(this.accountSnapshot);
|
||||||
|
this.enterDefenseMode({
|
||||||
|
standxDepthStale: false,
|
||||||
|
binanceStale: false,
|
||||||
|
standxAccountStale: false,
|
||||||
|
accountInvalid: false,
|
||||||
|
standxRestUnhealthy: false,
|
||||||
|
standxRestConsecutiveErrors: this.standxRestConsecutiveErrors,
|
||||||
|
standxRestLastError: this.standxRestLastError,
|
||||||
|
marginModeNotIsolated: true,
|
||||||
|
marginMode: current,
|
||||||
|
standxDepthAge: 0,
|
||||||
|
binanceAge: 0,
|
||||||
|
standxAccountAge: 0,
|
||||||
|
accountIssues: [],
|
||||||
|
});
|
||||||
|
this.emitUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const accountHealth = validateAccountSnapshotForSymbol(this.accountSnapshot, this.config.symbol);
|
const accountHealth = validateAccountSnapshotForSymbol(this.accountSnapshot, this.config.symbol);
|
||||||
if (!accountHealth.ok && !this.defenseMode) {
|
if (!accountHealth.ok && !this.defenseMode) {
|
||||||
this.enterDefenseMode({
|
this.enterDefenseMode({
|
||||||
@@ -545,6 +571,8 @@ export class MakerPointsEngine {
|
|||||||
standxRestUnhealthy: false,
|
standxRestUnhealthy: false,
|
||||||
standxRestConsecutiveErrors: this.standxRestConsecutiveErrors,
|
standxRestConsecutiveErrors: this.standxRestConsecutiveErrors,
|
||||||
standxRestLastError: this.standxRestLastError,
|
standxRestLastError: this.standxRestLastError,
|
||||||
|
marginModeNotIsolated: false,
|
||||||
|
marginMode: this.getStandxMarginMode(this.accountSnapshot),
|
||||||
standxDepthAge: 0,
|
standxDepthAge: 0,
|
||||||
binanceAge: 0,
|
binanceAge: 0,
|
||||||
standxAccountAge: 0,
|
standxAccountAge: 0,
|
||||||
@@ -1594,8 +1622,16 @@ export class MakerPointsEngine {
|
|||||||
const accountInvalid = this.accountSnapshot != null && !accountHealth.ok;
|
const accountInvalid = this.accountSnapshot != null && !accountHealth.ok;
|
||||||
const standxRestUnhealthy =
|
const standxRestUnhealthy =
|
||||||
this.standxRestUnhealthy && this.standxRestConsecutiveErrors >= STANDX_REST_ERROR_DEFENSE_THRESHOLD;
|
this.standxRestUnhealthy && this.standxRestConsecutiveErrors >= STANDX_REST_ERROR_DEFENSE_THRESHOLD;
|
||||||
|
const marginMode = this.getStandxMarginMode(this.accountSnapshot);
|
||||||
|
const marginModeNotIsolated = this.exchange.id === "standx" && marginMode != null && marginMode !== "isolated";
|
||||||
|
|
||||||
const shouldDefend = standxDepthStale || binanceStale || standxAccountStale || accountInvalid || standxRestUnhealthy;
|
const shouldDefend =
|
||||||
|
standxDepthStale ||
|
||||||
|
binanceStale ||
|
||||||
|
standxAccountStale ||
|
||||||
|
accountInvalid ||
|
||||||
|
standxRestUnhealthy ||
|
||||||
|
marginModeNotIsolated;
|
||||||
|
|
||||||
if (shouldDefend && !this.defenseMode) {
|
if (shouldDefend && !this.defenseMode) {
|
||||||
// 进入防御模式
|
// 进入防御模式
|
||||||
@@ -1607,6 +1643,8 @@ export class MakerPointsEngine {
|
|||||||
standxRestUnhealthy,
|
standxRestUnhealthy,
|
||||||
standxRestConsecutiveErrors: this.standxRestConsecutiveErrors,
|
standxRestConsecutiveErrors: this.standxRestConsecutiveErrors,
|
||||||
standxRestLastError: this.standxRestLastError,
|
standxRestLastError: this.standxRestLastError,
|
||||||
|
marginModeNotIsolated,
|
||||||
|
marginMode,
|
||||||
standxDepthAge: this.lastStandxDepthTime > 0 ? now - this.lastStandxDepthTime : 0,
|
standxDepthAge: this.lastStandxDepthTime > 0 ? now - this.lastStandxDepthTime : 0,
|
||||||
binanceAge: this.lastBinanceDepthTime > 0 ? now - this.lastBinanceDepthTime : 0,
|
binanceAge: this.lastBinanceDepthTime > 0 ? now - this.lastBinanceDepthTime : 0,
|
||||||
standxAccountAge: this.lastStandxAccountTime > 0 ? now - this.lastStandxAccountTime : 0,
|
standxAccountAge: this.lastStandxAccountTime > 0 ? now - this.lastStandxAccountTime : 0,
|
||||||
@@ -1630,6 +1668,8 @@ export class MakerPointsEngine {
|
|||||||
standxRestUnhealthy: boolean;
|
standxRestUnhealthy: boolean;
|
||||||
standxRestConsecutiveErrors: number;
|
standxRestConsecutiveErrors: number;
|
||||||
standxRestLastError: string | null;
|
standxRestLastError: string | null;
|
||||||
|
marginModeNotIsolated: boolean;
|
||||||
|
marginMode: string | null;
|
||||||
standxDepthAge: number;
|
standxDepthAge: number;
|
||||||
binanceAge: number;
|
binanceAge: number;
|
||||||
standxAccountAge: number;
|
standxAccountAge: number;
|
||||||
@@ -1651,6 +1691,9 @@ export class MakerPointsEngine {
|
|||||||
if (staleInfo.standxRestUnhealthy) {
|
if (staleInfo.standxRestUnhealthy) {
|
||||||
staleItems.push(`StandX REST错误(${staleInfo.standxRestConsecutiveErrors}次)`);
|
staleItems.push(`StandX REST错误(${staleInfo.standxRestConsecutiveErrors}次)`);
|
||||||
}
|
}
|
||||||
|
if (staleInfo.marginModeNotIsolated) {
|
||||||
|
staleItems.push(`保证金模式(${staleInfo.marginMode ?? "unknown"})`);
|
||||||
|
}
|
||||||
if (staleInfo.binanceStale) {
|
if (staleInfo.binanceStale) {
|
||||||
staleItems.push(`Binance深度(${Math.round(staleInfo.binanceAge / 1000)}s)`);
|
staleItems.push(`Binance深度(${Math.round(staleInfo.binanceAge / 1000)}s)`);
|
||||||
}
|
}
|
||||||
@@ -1766,6 +1809,11 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 防御模式下也尝试修复保证金模式(StandX)
|
||||||
|
if (this.exchange.id === "standx") {
|
||||||
|
await this.ensureStandxIsolatedMarginMode();
|
||||||
|
}
|
||||||
|
|
||||||
// 防御模式下持续通过 REST 刷新挂单,并尽力撤销所有挂单(避免本地状态/WS 丢失导致遗留挂单)
|
// 防御模式下持续通过 REST 刷新挂单,并尽力撤销所有挂单(避免本地状态/WS 丢失导致遗留挂单)
|
||||||
if (this.exchange.queryOpenOrders) {
|
if (this.exchange.queryOpenOrders) {
|
||||||
try {
|
try {
|
||||||
@@ -1809,6 +1857,57 @@ export class MakerPointsEngine {
|
|||||||
void poll();
|
void poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getStandxMarginMode(snapshot: AsterAccountSnapshot | null): string | null {
|
||||||
|
if (this.exchange.id !== "standx") return null;
|
||||||
|
const positions = snapshot?.positions ?? [];
|
||||||
|
const match = positions.find((pos) => pos.symbol === this.config.symbol);
|
||||||
|
const raw = (match as any)?.marginType ?? (match as any)?.margin_mode;
|
||||||
|
const mode = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||||
|
return mode ? mode : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureStandxIsolatedMarginMode(): Promise<boolean> {
|
||||||
|
if (this.exchange.id !== "standx") return true;
|
||||||
|
const currentMode = this.getStandxMarginMode(this.accountSnapshot);
|
||||||
|
if (currentMode === "isolated") return true;
|
||||||
|
const change = this.exchange.changeMarginMode?.bind(this.exchange);
|
||||||
|
const queryAccount = this.exchange.queryAccountSnapshot?.bind(this.exchange);
|
||||||
|
if (!change || !queryAccount) return false;
|
||||||
|
|
||||||
|
if (this.marginModeEnsuring) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.marginModeEnsuring = (async () => {
|
||||||
|
try {
|
||||||
|
await change({ symbol: this.config.symbol, marginMode: "isolated" });
|
||||||
|
for (let attempt = 0; attempt < STANDX_MARGIN_MODE_MAX_ATTEMPTS; attempt++) {
|
||||||
|
const next = await queryAccount();
|
||||||
|
if (next) {
|
||||||
|
this.accountSnapshot = next;
|
||||||
|
this.lastStandxAccountTime = Date.now();
|
||||||
|
this.feedStatus.account = true;
|
||||||
|
}
|
||||||
|
const mode = this.getStandxMarginMode(this.accountSnapshot);
|
||||||
|
if (mode === "isolated") {
|
||||||
|
this.tradeLog.push("info", "已切换为逐仓模式 (isolated),恢复策略运行");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await this.sleep(STANDX_MARGIN_MODE_CHECK_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
this.tradeLog.push("warn", `逐仓模式切换未确认,当前模式: ${this.getStandxMarginMode(this.accountSnapshot) ?? "unknown"}`);
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
this.tradeLog.push("error", `切换逐仓模式失败: ${extractMessage(error)}`);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.marginModeEnsuring = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return await this.marginModeEnsuring;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止防御模式下的 REST 轮询
|
* 停止防御模式下的 REST 轮询
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ describe("MakerPointsEngine defense-mode REST polling", () => {
|
|||||||
standxRestUnhealthy: false,
|
standxRestUnhealthy: false,
|
||||||
standxRestConsecutiveErrors: 0,
|
standxRestConsecutiveErrors: 0,
|
||||||
standxRestLastError: null,
|
standxRestLastError: null,
|
||||||
|
marginModeNotIsolated: false,
|
||||||
|
marginMode: "isolated",
|
||||||
standxDepthAge: 6000,
|
standxDepthAge: 6000,
|
||||||
binanceAge: 0,
|
binanceAge: 0,
|
||||||
standxAccountAge: 0,
|
standxAccountAge: 0,
|
||||||
@@ -149,6 +151,8 @@ describe("MakerPointsEngine defense-mode REST polling", () => {
|
|||||||
standxRestUnhealthy: false,
|
standxRestUnhealthy: false,
|
||||||
standxRestConsecutiveErrors: 0,
|
standxRestConsecutiveErrors: 0,
|
||||||
standxRestLastError: null,
|
standxRestLastError: null,
|
||||||
|
marginModeNotIsolated: false,
|
||||||
|
marginMode: "isolated",
|
||||||
standxDepthAge: 6000,
|
standxDepthAge: 6000,
|
||||||
binanceAge: 0,
|
binanceAge: 0,
|
||||||
standxAccountAge: 0,
|
standxAccountAge: 0,
|
||||||
@@ -162,4 +166,3 @@ describe("MakerPointsEngine defense-mode REST polling", () => {
|
|||||||
engine.stop();
|
engine.stop();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
|
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||||
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
|
class StandxStubAdapter implements ExchangeAdapter {
|
||||||
|
id = "standx";
|
||||||
|
marginMode: "cross" | "isolated" = "cross";
|
||||||
|
changeCalls: Array<{ symbol: string; marginMode: "isolated" | "cross" }> = [];
|
||||||
|
|
||||||
|
supportsTrailingStops(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||||
|
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||||
|
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||||
|
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||||
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||||
|
|
||||||
|
async createOrder(): Promise<AsterOrder> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
async cancelOrder(): Promise<void> {}
|
||||||
|
async cancelOrders(): Promise<void> {}
|
||||||
|
async cancelAllOrders(): Promise<void> {}
|
||||||
|
|
||||||
|
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||||
|
return {
|
||||||
|
canTrade: true,
|
||||||
|
canDeposit: true,
|
||||||
|
canWithdraw: true,
|
||||||
|
updateTime: Date.now(),
|
||||||
|
totalWalletBalance: "0",
|
||||||
|
totalUnrealizedProfit: "0",
|
||||||
|
marketType: "perp",
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
positionAmt: "0",
|
||||||
|
entryPrice: "0",
|
||||||
|
unrealizedProfit: "0",
|
||||||
|
positionSide: "BOTH",
|
||||||
|
updateTime: Date.now(),
|
||||||
|
marginType: this.marginMode,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
assets: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||||
|
this.changeCalls.push(params);
|
||||||
|
this.marginMode = params.marginMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MakerPointsEngine StandX isolated margin guard", () => {
|
||||||
|
it("switches to isolated before placing orders", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const adapter = new StandxStubAdapter();
|
||||||
|
|
||||||
|
const engine = new MakerPointsEngine(
|
||||||
|
{
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
perOrderAmount: 0.01,
|
||||||
|
closeThreshold: 0,
|
||||||
|
stopLossUsd: 1,
|
||||||
|
refreshIntervalMs: 500,
|
||||||
|
maxLogEntries: 10,
|
||||||
|
maxCloseSlippagePct: 0.05,
|
||||||
|
priceTick: 0.1,
|
||||||
|
qtyStep: 0.001,
|
||||||
|
enableBand0To10: true,
|
||||||
|
enableBand10To30: false,
|
||||||
|
enableBand30To100: false,
|
||||||
|
band0To10Amount: 0.01,
|
||||||
|
band10To30Amount: 0.01,
|
||||||
|
band30To100Amount: 0.01,
|
||||||
|
minRepriceBps: 3,
|
||||||
|
enableBinanceDepthCancel: false,
|
||||||
|
filterMinDepth: 0,
|
||||||
|
},
|
||||||
|
adapter
|
||||||
|
);
|
||||||
|
|
||||||
|
// Seed engine state to pass readiness checks without WS.
|
||||||
|
(engine as any).feedStatus = { account: true, depth: true, ticker: true, orders: true, binance: true };
|
||||||
|
(engine as any).initialOrderSnapshotReady = true;
|
||||||
|
(engine as any).accountSnapshot = await adapter.queryAccountSnapshot();
|
||||||
|
(engine as any).depthSnapshot = {
|
||||||
|
lastUpdateId: 1,
|
||||||
|
bids: [["100", "1"]],
|
||||||
|
asks: [["101", "1"]],
|
||||||
|
eventTime: Date.now(),
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
} as AsterDepth;
|
||||||
|
(engine as any).tickerSnapshot = {
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
lastPrice: "100",
|
||||||
|
openPrice: "0",
|
||||||
|
highPrice: "0",
|
||||||
|
lowPrice: "0",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
eventTime: Date.now(),
|
||||||
|
} as AsterTicker;
|
||||||
|
|
||||||
|
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
(engine as any).syncOrders = syncSpy;
|
||||||
|
|
||||||
|
// First tick should force margin mode to isolated and then proceed to sync orders.
|
||||||
|
await (engine as any).tick();
|
||||||
|
|
||||||
|
expect(adapter.changeCalls).toEqual([{ symbol: "BTC-USD", marginMode: "isolated" }]);
|
||||||
|
expect(syncSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enters defense mode if it cannot switch to isolated", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const adapter = new StandxStubAdapter();
|
||||||
|
adapter.changeMarginMode = vi.fn(async () => {
|
||||||
|
throw new Error("change failed");
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
const engine = new MakerPointsEngine(
|
||||||
|
{
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
perOrderAmount: 0.01,
|
||||||
|
closeThreshold: 0,
|
||||||
|
stopLossUsd: 1,
|
||||||
|
refreshIntervalMs: 500,
|
||||||
|
maxLogEntries: 10,
|
||||||
|
maxCloseSlippagePct: 0.05,
|
||||||
|
priceTick: 0.1,
|
||||||
|
qtyStep: 0.001,
|
||||||
|
enableBand0To10: true,
|
||||||
|
enableBand10To30: false,
|
||||||
|
enableBand30To100: false,
|
||||||
|
band0To10Amount: 0.01,
|
||||||
|
band10To30Amount: 0.01,
|
||||||
|
band30To100Amount: 0.01,
|
||||||
|
minRepriceBps: 3,
|
||||||
|
enableBinanceDepthCancel: false,
|
||||||
|
filterMinDepth: 0,
|
||||||
|
},
|
||||||
|
adapter
|
||||||
|
);
|
||||||
|
|
||||||
|
(engine as any).feedStatus = { account: true, depth: true, ticker: true, orders: true, binance: true };
|
||||||
|
(engine as any).initialOrderSnapshotReady = true;
|
||||||
|
(engine as any).accountSnapshot = await adapter.queryAccountSnapshot();
|
||||||
|
(engine as any).depthSnapshot = {
|
||||||
|
lastUpdateId: 1,
|
||||||
|
bids: [["100", "1"]],
|
||||||
|
asks: [["101", "1"]],
|
||||||
|
eventTime: Date.now(),
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
} as AsterDepth;
|
||||||
|
(engine as any).tickerSnapshot = {
|
||||||
|
symbol: "BTC-USD",
|
||||||
|
lastPrice: "100",
|
||||||
|
openPrice: "0",
|
||||||
|
highPrice: "0",
|
||||||
|
lowPrice: "0",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
eventTime: Date.now(),
|
||||||
|
} as AsterTicker;
|
||||||
|
|
||||||
|
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
(engine as any).syncOrders = syncSpy;
|
||||||
|
|
||||||
|
await (engine as any).tick();
|
||||||
|
|
||||||
|
expect(syncSpy).not.toHaveBeenCalled();
|
||||||
|
expect((engine as any).defenseMode).toBe(true);
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user