mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-10 16:58:08 +00:00
feat: 引入事件发射器和会话交易量跟踪器,重构策略引擎以支持更灵活的事件处理和交易量计算
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
export class StrategyEventEmitter<TEvent extends string, TPayload> {
|
||||||
|
private readonly listeners = new Map<TEvent, Set<(payload: TPayload) => void>>();
|
||||||
|
|
||||||
|
on(event: TEvent, handler: (payload: TPayload) => void): void {
|
||||||
|
const handlers = this.listeners.get(event) ?? new Set<(payload: TPayload) => void>();
|
||||||
|
handlers.add(handler);
|
||||||
|
this.listeners.set(event, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: TEvent, handler: (payload: TPayload) => void): void {
|
||||||
|
const handlers = this.listeners.get(event);
|
||||||
|
if (!handlers) return;
|
||||||
|
handlers.delete(handler);
|
||||||
|
if (handlers.size === 0) {
|
||||||
|
this.listeners.delete(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: TEvent, payload: TPayload, onError?: (error: unknown) => void): void {
|
||||||
|
const handlers = this.listeners.get(event);
|
||||||
|
if (!handlers) return;
|
||||||
|
for (const handler of handlers) {
|
||||||
|
try {
|
||||||
|
handler(payload);
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { PositionSnapshot } from "../../utils/strategy";
|
||||||
|
|
||||||
|
export class SessionVolumeTracker {
|
||||||
|
private initialized = false;
|
||||||
|
private previousPositionAmt = 0;
|
||||||
|
private total = 0;
|
||||||
|
|
||||||
|
update(position: PositionSnapshot, referencePrice: number | null): void {
|
||||||
|
if (!this.initialized) {
|
||||||
|
this.previousPositionAmt = position.positionAmt;
|
||||||
|
this.initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (referencePrice == null) {
|
||||||
|
this.previousPositionAmt = position.positionAmt;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delta = Math.abs(position.positionAmt - this.previousPositionAmt);
|
||||||
|
if (delta > 0) {
|
||||||
|
this.total += delta * referencePrice;
|
||||||
|
}
|
||||||
|
this.previousPositionAmt = position.positionAmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): number {
|
||||||
|
return this.total;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export type LogHandler = (type: string, detail: string) => void;
|
||||||
|
|
||||||
|
interface SubscriptionMessages {
|
||||||
|
subscribeFail: (error: unknown) => string;
|
||||||
|
processFail: (error: unknown) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeSubscribe<T>(
|
||||||
|
subscribe: (cb: (payload: T) => void) => void,
|
||||||
|
handler: (payload: T) => void,
|
||||||
|
log: LogHandler,
|
||||||
|
messages: SubscriptionMessages
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
subscribe((payload) => {
|
||||||
|
try {
|
||||||
|
handler(payload);
|
||||||
|
} catch (error) {
|
||||||
|
log("error", messages.processFail(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log("error", messages.subscribeFail(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
-118
@@ -3,13 +3,15 @@ import type { ExchangeAdapter } from "../exchanges/adapter";
|
|||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AsterAccountSnapshot,
|
||||||
AsterDepth,
|
AsterDepth,
|
||||||
|
AsterKline,
|
||||||
AsterOrder,
|
AsterOrder,
|
||||||
AsterTicker,
|
AsterTicker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { roundDownToTick } from "../utils/math";
|
import { roundDownToTick } from "../utils/math";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
import { isUnknownOrderError, isRateLimitError } from "../utils/errors";
|
import { isUnknownOrderError, isRateLimitError } from "../utils/errors";
|
||||||
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
import { getPosition } from "../utils/strategy";
|
||||||
|
import type { PositionSnapshot } from "../utils/strategy";
|
||||||
import { computePositionPnl } from "../utils/pnl";
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
import { getTopPrices, getMidOrLast } from "../utils/price";
|
import { getTopPrices, getMidOrLast } from "../utils/price";
|
||||||
import { shouldStopLoss } from "../utils/risk";
|
import { shouldStopLoss } from "../utils/risk";
|
||||||
@@ -22,6 +24,9 @@ import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order
|
|||||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||||
import { safeCancelOrder } from "../core/lib/orders";
|
import { safeCancelOrder } from "../core/lib/orders";
|
||||||
import { RateLimitController } from "../core/lib/rate-limit";
|
import { RateLimitController } from "../core/lib/rate-limit";
|
||||||
|
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||||
|
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||||
|
import { SessionVolumeTracker } from "./common/session-volume";
|
||||||
|
|
||||||
interface DesiredOrder {
|
interface DesiredOrder {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
@@ -63,15 +68,13 @@ export class MakerEngine {
|
|||||||
private readonly pendingCancelOrders = new Set<string>();
|
private readonly pendingCancelOrders = new Set<string>();
|
||||||
|
|
||||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||||
private readonly listeners = new Map<MakerEvent, Set<MakerListener>>();
|
private readonly events = new StrategyEventEmitter<MakerEvent, MakerEngineSnapshot>();
|
||||||
|
private readonly sessionVolume = new SessionVolumeTracker();
|
||||||
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null;
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
private processing = false;
|
private processing = false;
|
||||||
private desiredOrders: DesiredOrder[] = [];
|
private desiredOrders: DesiredOrder[] = [];
|
||||||
private accountUnrealized = 0;
|
private accountUnrealized = 0;
|
||||||
private sessionQuoteVolume = 0;
|
|
||||||
private prevPositionAmt = 0;
|
|
||||||
private initializedPosition = false;
|
|
||||||
private initialOrderSnapshotReady = false;
|
private initialOrderSnapshotReady = false;
|
||||||
private initialOrderResetDone = false;
|
private initialOrderResetDone = false;
|
||||||
private entryPricePendingLogged = false;
|
private entryPricePendingLogged = false;
|
||||||
@@ -100,18 +103,11 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
on(event: MakerEvent, handler: MakerListener): void {
|
on(event: MakerEvent, handler: MakerListener): void {
|
||||||
const handlers = this.listeners.get(event) ?? new Set<MakerListener>();
|
this.events.on(event, handler);
|
||||||
handlers.add(handler);
|
|
||||||
this.listeners.set(event, handlers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
off(event: MakerEvent, handler: MakerListener): void {
|
off(event: MakerEvent, handler: MakerListener): void {
|
||||||
const handlers = this.listeners.get(event);
|
this.events.off(event, handler);
|
||||||
if (!handlers) return;
|
|
||||||
handlers.delete(handler);
|
|
||||||
if (handlers.size === 0) {
|
|
||||||
this.listeners.delete(event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnapshot(): MakerEngineSnapshot {
|
getSnapshot(): MakerEngineSnapshot {
|
||||||
@@ -119,86 +115,88 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
try {
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
this.exchange.watchAccount((snapshot) => {
|
|
||||||
try {
|
safeSubscribe<AsterAccountSnapshot>(
|
||||||
this.accountSnapshot = snapshot;
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0");
|
(snapshot) => {
|
||||||
if (Number.isFinite(totalUnrealized)) {
|
this.accountSnapshot = snapshot;
|
||||||
this.accountUnrealized = totalUnrealized;
|
const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0");
|
||||||
|
if (Number.isFinite(totalUnrealized)) {
|
||||||
|
this.accountUnrealized = totalUnrealized;
|
||||||
|
}
|
||||||
|
const position = getPosition(snapshot, this.config.symbol);
|
||||||
|
this.sessionVolume.update(position, this.getReferencePrice());
|
||||||
|
this.emitUpdate();
|
||||||
|
},
|
||||||
|
log,
|
||||||
|
{
|
||||||
|
subscribeFail: (error) => `订阅账户失败: ${String(error)}`,
|
||||||
|
processFail: (error) => `账户推送处理异常: ${String(error)}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
safeSubscribe<AsterOrder[]>(
|
||||||
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
|
(orders) => {
|
||||||
|
this.syncLocksWithOrders(orders);
|
||||||
|
this.openOrders = Array.isArray(orders)
|
||||||
|
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
|
||||||
|
: [];
|
||||||
|
const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
|
||||||
|
for (const id of Array.from(this.pendingCancelOrders)) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
this.pendingCancelOrders.delete(id);
|
||||||
}
|
}
|
||||||
const position = getPosition(snapshot, this.config.symbol);
|
|
||||||
this.updateSessionVolume(position);
|
|
||||||
this.emitUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `账户推送处理异常: ${String(err)}`);
|
|
||||||
}
|
}
|
||||||
});
|
this.initialOrderSnapshotReady = true;
|
||||||
} catch (err) {
|
this.emitUpdate();
|
||||||
this.tradeLog.push("error", `订阅账户失败: ${String(err)}`);
|
},
|
||||||
}
|
log,
|
||||||
|
{
|
||||||
|
subscribeFail: (error) => `订阅订单失败: ${String(error)}`,
|
||||||
|
processFail: (error) => `订单推送处理异常: ${String(error)}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
safeSubscribe<AsterDepth>(
|
||||||
this.exchange.watchOrders((orders) => {
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
try {
|
(depth) => {
|
||||||
this.syncLocksWithOrders(orders);
|
this.depthSnapshot = depth;
|
||||||
this.openOrders = Array.isArray(orders)
|
this.emitUpdate();
|
||||||
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
|
},
|
||||||
: [];
|
log,
|
||||||
const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
|
{
|
||||||
for (const id of Array.from(this.pendingCancelOrders)) {
|
subscribeFail: (error) => `订阅深度失败: ${String(error)}`,
|
||||||
if (!currentIds.has(id)) {
|
processFail: (error) => `深度推送处理异常: ${String(error)}`,
|
||||||
this.pendingCancelOrders.delete(id);
|
}
|
||||||
}
|
);
|
||||||
}
|
|
||||||
this.initialOrderSnapshotReady = true;
|
|
||||||
this.emitUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `订单推送处理异常: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `订阅订单失败: ${String(err)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
safeSubscribe<AsterTicker>(
|
||||||
this.exchange.watchDepth(this.config.symbol, (depth) => {
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
try {
|
(ticker) => {
|
||||||
this.depthSnapshot = depth;
|
this.tickerSnapshot = ticker;
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
} catch (err) {
|
},
|
||||||
this.tradeLog.push("error", `深度推送处理异常: ${String(err)}`);
|
log,
|
||||||
}
|
{
|
||||||
});
|
subscribeFail: (error) => `订阅Ticker失败: ${String(error)}`,
|
||||||
} catch (err) {
|
processFail: (error) => `价格推送处理异常: ${String(error)}`,
|
||||||
this.tradeLog.push("error", `订阅深度失败: ${String(err)}`);
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
try {
|
|
||||||
this.exchange.watchTicker(this.config.symbol, (ticker) => {
|
|
||||||
try {
|
|
||||||
this.tickerSnapshot = ticker;
|
|
||||||
this.emitUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `价格推送处理异常: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `订阅Ticker失败: ${String(err)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maker strategy does not consume klines, but subscribe to keep parity with other modules
|
// Maker strategy does not consume klines, but subscribe to keep parity with other modules
|
||||||
try {
|
safeSubscribe<AsterKline[]>(
|
||||||
this.exchange.watchKlines(this.config.symbol, "1m", () => {
|
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
||||||
try {
|
(_klines) => {
|
||||||
/* no-op */
|
/* no-op */
|
||||||
} catch (err) {
|
},
|
||||||
this.tradeLog.push("error", `K线推送处理异常: ${String(err)}`);
|
log,
|
||||||
}
|
{
|
||||||
});
|
subscribeFail: (error) => `订阅K线失败: ${String(error)}`,
|
||||||
} catch (err) {
|
processFail: (error) => `K线推送处理异常: ${String(error)}`,
|
||||||
this.tradeLog.push("error", `订阅K线失败: ${String(err)}`);
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
||||||
@@ -268,7 +266,7 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.desiredOrders = desired;
|
this.desiredOrders = desired;
|
||||||
this.updateSessionVolume(position);
|
this.sessionVolume.update(position, this.getReferencePrice());
|
||||||
await this.syncOrders(desired);
|
await this.syncOrders(desired);
|
||||||
await this.checkRisk(position, closeBidPrice, closeAskPrice);
|
await this.checkRisk(position, closeBidPrice, closeAskPrice);
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
@@ -468,16 +466,9 @@ export class MakerEngine {
|
|||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
try {
|
try {
|
||||||
const snapshot = this.buildSnapshot();
|
const snapshot = this.buildSnapshot();
|
||||||
const handlers = this.listeners.get("update");
|
this.events.emit("update", snapshot, (error) => {
|
||||||
if (handlers) {
|
this.tradeLog.push("error", `更新回调处理异常: ${String(error)}`);
|
||||||
handlers.forEach((handler) => {
|
});
|
||||||
try {
|
|
||||||
handler(snapshot);
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `更新回调处理异常: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
|
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
|
||||||
}
|
}
|
||||||
@@ -498,7 +489,7 @@ export class MakerEngine {
|
|||||||
position,
|
position,
|
||||||
pnl,
|
pnl,
|
||||||
accountUnrealized: this.accountUnrealized,
|
accountUnrealized: this.accountUnrealized,
|
||||||
sessionVolume: this.sessionQuoteVolume,
|
sessionVolume: this.sessionVolume.value,
|
||||||
openOrders: this.openOrders,
|
openOrders: this.openOrders,
|
||||||
desiredOrders: this.desiredOrders,
|
desiredOrders: this.desiredOrders,
|
||||||
tradeLog: this.tradeLog.all(),
|
tradeLog: this.tradeLog.all(),
|
||||||
@@ -506,24 +497,6 @@ export class MakerEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSessionVolume(position: PositionSnapshot): void {
|
|
||||||
const price = this.getReferencePrice();
|
|
||||||
if (!this.initializedPosition) {
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
this.initializedPosition = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (price == null) {
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delta = Math.abs(position.positionAmt - this.prevPositionAmt);
|
|
||||||
if (delta > 0) {
|
|
||||||
this.sessionQuoteVolume += delta * price;
|
|
||||||
}
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getReferencePrice(): number | null {
|
private getReferencePrice(): number | null {
|
||||||
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
|
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import type { ExchangeAdapter } from "../exchanges/adapter";
|
|||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AsterAccountSnapshot,
|
||||||
AsterDepth,
|
AsterDepth,
|
||||||
|
AsterKline,
|
||||||
AsterOrder,
|
AsterOrder,
|
||||||
AsterTicker,
|
AsterTicker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { roundDownToTick } from "../utils/math";
|
import { roundDownToTick } from "../utils/math";
|
||||||
import { createTradeLog } from "../logging/trade-log";
|
import { createTradeLog } from "../logging/trade-log";
|
||||||
import { isUnknownOrderError, isRateLimitError } from "../utils/errors";
|
import { isUnknownOrderError, isRateLimitError } from "../utils/errors";
|
||||||
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
import { getPosition } from "../utils/strategy";
|
||||||
|
import type { PositionSnapshot } from "../utils/strategy";
|
||||||
import { computeDepthStats } from "../utils/depth";
|
import { computeDepthStats } from "../utils/depth";
|
||||||
import { computePositionPnl } from "../utils/pnl";
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
import { getTopPrices, getMidOrLast } from "../utils/price";
|
import { getTopPrices, getMidOrLast } from "../utils/price";
|
||||||
@@ -24,6 +26,9 @@ import type { MakerEngineSnapshot } from "./maker-engine";
|
|||||||
import { makeOrderPlan } from "../core/lib/order-plan";
|
import { makeOrderPlan } from "../core/lib/order-plan";
|
||||||
import { safeCancelOrder } from "../core/lib/orders";
|
import { safeCancelOrder } from "../core/lib/orders";
|
||||||
import { RateLimitController } from "../core/lib/rate-limit";
|
import { RateLimitController } from "../core/lib/rate-limit";
|
||||||
|
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||||
|
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||||
|
import { SessionVolumeTracker } from "./common/session-volume";
|
||||||
|
|
||||||
interface DesiredOrder {
|
interface DesiredOrder {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
@@ -57,15 +62,13 @@ export class OffsetMakerEngine {
|
|||||||
private readonly pendingCancelOrders = new Set<string>();
|
private readonly pendingCancelOrders = new Set<string>();
|
||||||
|
|
||||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||||
private readonly listeners = new Map<MakerEvent, Set<MakerListener>>();
|
private readonly events = new StrategyEventEmitter<MakerEvent, OffsetMakerEngineSnapshot>();
|
||||||
|
private readonly sessionVolume = new SessionVolumeTracker();
|
||||||
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null;
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
private processing = false;
|
private processing = false;
|
||||||
private desiredOrders: DesiredOrder[] = [];
|
private desiredOrders: DesiredOrder[] = [];
|
||||||
private accountUnrealized = 0;
|
private accountUnrealized = 0;
|
||||||
private sessionQuoteVolume = 0;
|
|
||||||
private prevPositionAmt = 0;
|
|
||||||
private initializedPosition = false;
|
|
||||||
private initialOrderSnapshotReady = false;
|
private initialOrderSnapshotReady = false;
|
||||||
private initialOrderResetDone = false;
|
private initialOrderResetDone = false;
|
||||||
private entryPricePendingLogged = false;
|
private entryPricePendingLogged = false;
|
||||||
@@ -100,18 +103,11 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
on(event: MakerEvent, handler: MakerListener): void {
|
on(event: MakerEvent, handler: MakerListener): void {
|
||||||
const handlers = this.listeners.get(event) ?? new Set<MakerListener>();
|
this.events.on(event, handler);
|
||||||
handlers.add(handler);
|
|
||||||
this.listeners.set(event, handlers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
off(event: MakerEvent, handler: MakerListener): void {
|
off(event: MakerEvent, handler: MakerListener): void {
|
||||||
const handlers = this.listeners.get(event);
|
this.events.off(event, handler);
|
||||||
if (!handlers) return;
|
|
||||||
handlers.delete(handler);
|
|
||||||
if (handlers.size === 0) {
|
|
||||||
this.listeners.delete(event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnapshot(): OffsetMakerEngineSnapshot {
|
getSnapshot(): OffsetMakerEngineSnapshot {
|
||||||
@@ -119,85 +115,87 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
try {
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
this.exchange.watchAccount((snapshot) => {
|
|
||||||
try {
|
safeSubscribe<AsterAccountSnapshot>(
|
||||||
this.accountSnapshot = snapshot;
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0");
|
(snapshot) => {
|
||||||
if (Number.isFinite(totalUnrealized)) {
|
this.accountSnapshot = snapshot;
|
||||||
this.accountUnrealized = totalUnrealized;
|
const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0");
|
||||||
|
if (Number.isFinite(totalUnrealized)) {
|
||||||
|
this.accountUnrealized = totalUnrealized;
|
||||||
|
}
|
||||||
|
const position = getPosition(snapshot, this.config.symbol);
|
||||||
|
this.sessionVolume.update(position, this.getReferencePrice());
|
||||||
|
this.emitUpdate();
|
||||||
|
},
|
||||||
|
log,
|
||||||
|
{
|
||||||
|
subscribeFail: (error) => `订阅账户失败: ${String(error)}`,
|
||||||
|
processFail: (error) => `账户推送处理异常: ${String(error)}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
safeSubscribe<AsterOrder[]>(
|
||||||
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
|
(orders) => {
|
||||||
|
this.syncLocksWithOrders(orders);
|
||||||
|
this.openOrders = Array.isArray(orders)
|
||||||
|
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
|
||||||
|
: [];
|
||||||
|
const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
|
||||||
|
for (const id of Array.from(this.pendingCancelOrders)) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
this.pendingCancelOrders.delete(id);
|
||||||
}
|
}
|
||||||
const position = getPosition(snapshot, this.config.symbol);
|
|
||||||
this.updateSessionVolume(position);
|
|
||||||
this.emitUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `账户推送处理异常: ${String(err)}`);
|
|
||||||
}
|
}
|
||||||
});
|
this.initialOrderSnapshotReady = true;
|
||||||
} catch (err) {
|
this.emitUpdate();
|
||||||
this.tradeLog.push("error", `订阅账户失败: ${String(err)}`);
|
},
|
||||||
}
|
log,
|
||||||
|
{
|
||||||
|
subscribeFail: (error) => `订阅订单失败: ${String(error)}`,
|
||||||
|
processFail: (error) => `订单推送处理异常: ${String(error)}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
safeSubscribe<AsterDepth>(
|
||||||
this.exchange.watchOrders((orders) => {
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
try {
|
(depth) => {
|
||||||
this.syncLocksWithOrders(orders);
|
this.depthSnapshot = depth;
|
||||||
this.openOrders = Array.isArray(orders)
|
this.emitUpdate();
|
||||||
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
|
},
|
||||||
: [];
|
log,
|
||||||
const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
|
{
|
||||||
for (const id of Array.from(this.pendingCancelOrders)) {
|
subscribeFail: (error) => `订阅深度失败: ${String(error)}`,
|
||||||
if (!currentIds.has(id)) {
|
processFail: (error) => `深度推送处理异常: ${String(error)}`,
|
||||||
this.pendingCancelOrders.delete(id);
|
}
|
||||||
}
|
);
|
||||||
}
|
|
||||||
this.initialOrderSnapshotReady = true;
|
|
||||||
this.emitUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `订单推送处理异常: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `订阅订单失败: ${String(err)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
safeSubscribe<AsterTicker>(
|
||||||
this.exchange.watchDepth(this.config.symbol, (depth) => {
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
try {
|
(ticker) => {
|
||||||
this.depthSnapshot = depth;
|
this.tickerSnapshot = ticker;
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
} catch (err) {
|
},
|
||||||
this.tradeLog.push("error", `深度推送处理异常: ${String(err)}`);
|
log,
|
||||||
}
|
{
|
||||||
});
|
subscribeFail: (error) => `订阅Ticker失败: ${String(error)}`,
|
||||||
} catch (err) {
|
processFail: (error) => `价格推送处理异常: ${String(error)}`,
|
||||||
this.tradeLog.push("error", `订阅深度失败: ${String(err)}`);
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
try {
|
safeSubscribe<AsterKline[]>(
|
||||||
this.exchange.watchTicker(this.config.symbol, (ticker) => {
|
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
||||||
try {
|
(_klines) => {
|
||||||
this.tickerSnapshot = ticker;
|
/* no-op */
|
||||||
this.emitUpdate();
|
},
|
||||||
} catch (err) {
|
log,
|
||||||
this.tradeLog.push("error", `价格推送处理异常: ${String(err)}`);
|
{
|
||||||
}
|
subscribeFail: (error) => `订阅K线失败: ${String(error)}`,
|
||||||
});
|
processFail: (error) => `K线推送处理异常: ${String(error)}`,
|
||||||
} catch (err) {
|
}
|
||||||
this.tradeLog.push("error", `订阅Ticker失败: ${String(err)}`);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.exchange.watchKlines(this.config.symbol, "1m", () => {
|
|
||||||
try {
|
|
||||||
/* no-op */
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `K线推送处理异常: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `订阅K线失败: ${String(err)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
||||||
@@ -282,7 +280,7 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.desiredOrders = desired;
|
this.desiredOrders = desired;
|
||||||
this.updateSessionVolume(position);
|
this.sessionVolume.update(position, this.getReferencePrice());
|
||||||
await this.syncOrders(desired);
|
await this.syncOrders(desired);
|
||||||
await this.checkRisk(position, closeBidPrice, closeAskPrice);
|
await this.checkRisk(position, closeBidPrice, closeAskPrice);
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
@@ -569,16 +567,9 @@ export class OffsetMakerEngine {
|
|||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
try {
|
try {
|
||||||
const snapshot = this.buildSnapshot();
|
const snapshot = this.buildSnapshot();
|
||||||
const handlers = this.listeners.get("update");
|
this.events.emit("update", snapshot, (error) => {
|
||||||
if (handlers) {
|
this.tradeLog.push("error", `更新回调处理异常: ${String(error)}`);
|
||||||
handlers.forEach((handler) => {
|
});
|
||||||
try {
|
|
||||||
handler(snapshot);
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `更新回调处理异常: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
|
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
|
||||||
}
|
}
|
||||||
@@ -599,7 +590,7 @@ export class OffsetMakerEngine {
|
|||||||
position,
|
position,
|
||||||
pnl,
|
pnl,
|
||||||
accountUnrealized: this.accountUnrealized,
|
accountUnrealized: this.accountUnrealized,
|
||||||
sessionVolume: this.sessionQuoteVolume,
|
sessionVolume: this.sessionVolume.value,
|
||||||
openOrders: this.openOrders,
|
openOrders: this.openOrders,
|
||||||
desiredOrders: this.desiredOrders,
|
desiredOrders: this.desiredOrders,
|
||||||
tradeLog: this.tradeLog.all(),
|
tradeLog: this.tradeLog.all(),
|
||||||
@@ -612,24 +603,6 @@ export class OffsetMakerEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSessionVolume(position: PositionSnapshot): void {
|
|
||||||
const price = this.getReferencePrice();
|
|
||||||
if (!this.initializedPosition) {
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
this.initializedPosition = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (price == null) {
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delta = Math.abs(position.positionAmt - this.prevPositionAmt);
|
|
||||||
if (delta > 0) {
|
|
||||||
this.sessionQuoteVolume += delta * price;
|
|
||||||
}
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getReferencePrice(): number | null {
|
private getReferencePrice(): number | null {
|
||||||
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
|
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-115
@@ -32,6 +32,9 @@ import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
|||||||
import { decryptCopyright } from "../utils/copyright";
|
import { decryptCopyright } from "../utils/copyright";
|
||||||
import { isRateLimitError } from "../utils/errors";
|
import { isRateLimitError } from "../utils/errors";
|
||||||
import { RateLimitController } from "../core/lib/rate-limit";
|
import { RateLimitController } from "../core/lib/rate-limit";
|
||||||
|
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||||
|
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||||
|
import { SessionVolumeTracker } from "./common/session-volume";
|
||||||
|
|
||||||
export interface TrendEngineSnapshot {
|
export interface TrendEngineSnapshot {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
@@ -75,6 +78,8 @@ export class TrendEngine {
|
|||||||
private readonly pending: OrderPendingMap = {};
|
private readonly pending: OrderPendingMap = {};
|
||||||
|
|
||||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||||
|
private readonly events = new StrategyEventEmitter<TrendEngineEvent, TrendEngineSnapshot>();
|
||||||
|
private readonly sessionVolume = new SessionVolumeTracker();
|
||||||
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null;
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
private processing = false;
|
private processing = false;
|
||||||
@@ -84,9 +89,6 @@ export class TrendEngine {
|
|||||||
private totalProfit = 0;
|
private totalProfit = 0;
|
||||||
private totalTrades = 0;
|
private totalTrades = 0;
|
||||||
private lastOpenPlan: OpenOrderPlan = { side: null, price: null };
|
private lastOpenPlan: OpenOrderPlan = { side: null, price: null };
|
||||||
private sessionQuoteVolume = 0;
|
|
||||||
private prevPositionAmt = 0;
|
|
||||||
private initializedPosition = false;
|
|
||||||
private cancelAllRequested = false;
|
private cancelAllRequested = false;
|
||||||
private readonly pendingCancelOrders = new Set<string>();
|
private readonly pendingCancelOrders = new Set<string>();
|
||||||
private readonly rateLimit: RateLimitController;
|
private readonly rateLimit: RateLimitController;
|
||||||
@@ -137,18 +139,11 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
on(event: TrendEngineEvent, handler: TrendEngineListener): void {
|
on(event: TrendEngineEvent, handler: TrendEngineListener): void {
|
||||||
const handlers = this.listeners.get(event) ?? new Set<TrendEngineListener>();
|
this.events.on(event, handler);
|
||||||
handlers.add(handler);
|
|
||||||
this.listeners.set(event, handlers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
off(event: TrendEngineEvent, handler: TrendEngineListener): void {
|
off(event: TrendEngineEvent, handler: TrendEngineListener): void {
|
||||||
const handlers = this.listeners.get(event);
|
this.events.off(event, handler);
|
||||||
if (!handlers) return;
|
|
||||||
handlers.delete(handler);
|
|
||||||
if (handlers.size === 0) {
|
|
||||||
this.listeners.delete(event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnapshot(): TrendEngineSnapshot {
|
getSnapshot(): TrendEngineSnapshot {
|
||||||
@@ -156,82 +151,89 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
try {
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
this.exchange.watchAccount((snapshot) => {
|
|
||||||
try {
|
safeSubscribe<AsterAccountSnapshot>(
|
||||||
this.accountSnapshot = snapshot;
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
const position = getPosition(snapshot, this.config.symbol);
|
(snapshot) => {
|
||||||
this.updateSessionVolume(position);
|
this.accountSnapshot = snapshot;
|
||||||
this.trackPositionLifecycle(position, this.getReferencePrice());
|
const position = getPosition(snapshot, this.config.symbol);
|
||||||
this.emitUpdate();
|
const reference = this.getReferencePrice();
|
||||||
} catch (err) {
|
this.sessionVolume.update(position, reference);
|
||||||
this.tradeLog.push("error", `账户推送处理异常: ${extractMessage(err)}`);
|
this.trackPositionLifecycle(position, reference);
|
||||||
}
|
this.emitUpdate();
|
||||||
});
|
},
|
||||||
} catch (err) {
|
log,
|
||||||
this.tradeLog.push("error", `订阅账户失败: ${String(err)}`);
|
{
|
||||||
}
|
subscribeFail: (error) => `订阅账户失败: ${String(error)}`,
|
||||||
try {
|
processFail: (error) => `账户推送处理异常: ${extractMessage(error)}`,
|
||||||
this.exchange.watchOrders((orders) => {
|
}
|
||||||
try {
|
);
|
||||||
this.synchronizeLocks(orders);
|
|
||||||
this.openOrders = Array.isArray(orders)
|
safeSubscribe<AsterOrder[]>(
|
||||||
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
: [];
|
(orders) => {
|
||||||
const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
|
this.synchronizeLocks(orders);
|
||||||
for (const id of Array.from(this.pendingCancelOrders)) {
|
this.openOrders = Array.isArray(orders)
|
||||||
if (!currentIds.has(id)) {
|
? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol)
|
||||||
this.pendingCancelOrders.delete(id);
|
: [];
|
||||||
}
|
const currentIds = new Set(this.openOrders.map((order) => String(order.orderId)));
|
||||||
|
for (const id of Array.from(this.pendingCancelOrders)) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
this.pendingCancelOrders.delete(id);
|
||||||
}
|
}
|
||||||
if (this.openOrders.length === 0 || this.pendingCancelOrders.size === 0) {
|
|
||||||
this.cancelAllRequested = false;
|
|
||||||
}
|
|
||||||
this.ordersSnapshotReady = true;
|
|
||||||
this.emitUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `订单推送处理异常: ${extractMessage(err)}`);
|
|
||||||
}
|
}
|
||||||
});
|
if (this.openOrders.length === 0 || this.pendingCancelOrders.size === 0) {
|
||||||
} catch (err) {
|
this.cancelAllRequested = false;
|
||||||
this.tradeLog.push("error", `订阅订单失败: ${String(err)}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.exchange.watchDepth(this.config.symbol, (depth) => {
|
|
||||||
try {
|
|
||||||
this.depthSnapshot = depth;
|
|
||||||
this.emitUpdate();
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `深度推送处理异常: ${extractMessage(err)}`);
|
|
||||||
}
|
}
|
||||||
});
|
this.ordersSnapshotReady = true;
|
||||||
} catch (err) {
|
this.emitUpdate();
|
||||||
this.tradeLog.push("error", `订阅深度失败: ${String(err)}`);
|
},
|
||||||
}
|
log,
|
||||||
try {
|
{
|
||||||
this.exchange.watchTicker(this.config.symbol, (ticker) => {
|
subscribeFail: (error) => `订阅订单失败: ${String(error)}`,
|
||||||
try {
|
processFail: (error) => `订单推送处理异常: ${extractMessage(error)}`,
|
||||||
this.tickerSnapshot = ticker;
|
}
|
||||||
this.emitUpdate();
|
);
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `价格推送处理异常: ${extractMessage(err)}`);
|
safeSubscribe<AsterDepth>(
|
||||||
}
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
});
|
(depth) => {
|
||||||
} catch (err) {
|
this.depthSnapshot = depth;
|
||||||
this.tradeLog.push("error", `订阅Ticker失败: ${String(err)}`);
|
this.emitUpdate();
|
||||||
}
|
},
|
||||||
try {
|
log,
|
||||||
this.exchange.watchKlines(this.config.symbol, this.config.klineInterval, (klines) => {
|
{
|
||||||
try {
|
subscribeFail: (error) => `订阅深度失败: ${String(error)}`,
|
||||||
this.klineSnapshot = Array.isArray(klines) ? klines : [];
|
processFail: (error) => `深度推送处理异常: ${extractMessage(error)}`,
|
||||||
this.emitUpdate();
|
}
|
||||||
} catch (err) {
|
);
|
||||||
this.tradeLog.push("error", `K线推送处理异常: ${extractMessage(err)}`);
|
|
||||||
}
|
safeSubscribe<AsterTicker>(
|
||||||
});
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
} catch (err) {
|
(ticker) => {
|
||||||
this.tradeLog.push("error", `订阅K线失败: ${String(err)}`);
|
this.tickerSnapshot = ticker;
|
||||||
}
|
this.emitUpdate();
|
||||||
|
},
|
||||||
|
log,
|
||||||
|
{
|
||||||
|
subscribeFail: (error) => `订阅Ticker失败: ${String(error)}`,
|
||||||
|
processFail: (error) => `价格推送处理异常: ${extractMessage(error)}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
safeSubscribe<AsterKline[]>(
|
||||||
|
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, this.config.klineInterval),
|
||||||
|
(klines) => {
|
||||||
|
this.klineSnapshot = Array.isArray(klines) ? klines : [];
|
||||||
|
this.emitUpdate();
|
||||||
|
},
|
||||||
|
log,
|
||||||
|
{
|
||||||
|
subscribeFail: (error) => `订阅K线失败: ${String(error)}`,
|
||||||
|
processFail: (error) => `K线推送处理异常: ${extractMessage(error)}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
||||||
@@ -303,7 +305,7 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSessionVolume(position);
|
this.sessionVolume.update(position, price);
|
||||||
this.trackPositionLifecycle(position, price);
|
this.trackPositionLifecycle(position, price);
|
||||||
this.lastSma30 = sma30;
|
this.lastSma30 = sma30;
|
||||||
this.lastPrice = price;
|
this.lastPrice = price;
|
||||||
@@ -849,16 +851,9 @@ export class TrendEngine {
|
|||||||
private emitUpdate(): void {
|
private emitUpdate(): void {
|
||||||
try {
|
try {
|
||||||
const snapshot = this.buildSnapshot();
|
const snapshot = this.buildSnapshot();
|
||||||
const handlers = this.listeners.get("update");
|
this.events.emit("update", snapshot, (error) => {
|
||||||
if (handlers) {
|
this.tradeLog.push("error", `更新回调处理异常: ${String(error)}`);
|
||||||
handlers.forEach((handler) => {
|
});
|
||||||
try {
|
|
||||||
handler(snapshot);
|
|
||||||
} catch (err) {
|
|
||||||
this.tradeLog.push("error", `更新回调处理异常: ${String(err)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
|
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
|
||||||
}
|
}
|
||||||
@@ -888,7 +883,7 @@ export class TrendEngine {
|
|||||||
unrealized: position.unrealizedProfit,
|
unrealized: position.unrealizedProfit,
|
||||||
totalProfit: this.totalProfit,
|
totalProfit: this.totalProfit,
|
||||||
totalTrades: this.totalTrades,
|
totalTrades: this.totalTrades,
|
||||||
sessionVolume: this.sessionQuoteVolume,
|
sessionVolume: this.sessionVolume.value,
|
||||||
tradeLog: this.tradeLog.all(),
|
tradeLog: this.tradeLog.all(),
|
||||||
openOrders: this.openOrders,
|
openOrders: this.openOrders,
|
||||||
depth: this.depthSnapshot,
|
depth: this.depthSnapshot,
|
||||||
@@ -898,24 +893,6 @@ export class TrendEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSessionVolume(position: PositionSnapshot): void {
|
|
||||||
const price = this.getReferencePrice();
|
|
||||||
if (!this.initializedPosition) {
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
this.initializedPosition = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (price == null) {
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delta = Math.abs(position.positionAmt - this.prevPositionAmt);
|
|
||||||
if (delta > 0) {
|
|
||||||
this.sessionQuoteVolume += delta * price;
|
|
||||||
}
|
|
||||||
this.prevPositionAmt = position.positionAmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getReferencePrice(): number | null {
|
private getReferencePrice(): number | null {
|
||||||
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot) ?? (this.lastPrice != null && Number.isFinite(this.lastPrice) ? this.lastPrice : null);
|
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot) ?? (this.lastPrice != null && Number.isFinite(this.lastPrice) ? this.lastPrice : null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user