更新 MakerEngine、OffsetMakerEngine 和 TrendEngine,重构订单同步逻辑,优化撤单和止损判断,新增订单计划生成和安全撤单功能,提升代码可读性和维护性。

This commit is contained in:
discountry
2025-09-24 04:33:08 +08:00
parent 6f05ee0da3
commit 7eddc3b88c
9 changed files with 272 additions and 199 deletions
+47
View File
@@ -0,0 +1,47 @@
import type { AsterOrder } from "../../exchanges/types";
export interface OrderTarget {
side: "BUY" | "SELL";
price: number;
amount: number;
reduceOnly: boolean;
}
export function makeOrderPlan(
openOrders: AsterOrder[],
targets: OrderTarget[],
tolerance: number
): { toCancel: AsterOrder[]; toPlace: OrderTarget[] } {
const unmatched = new Set(targets.map((_, idx) => idx));
const toCancel: AsterOrder[] = [];
for (const order of openOrders) {
const price = Number(order.price);
if (!Number.isFinite(price)) {
toCancel.push(order);
continue;
}
const reduceOnly = order.reduceOnly === true;
const matchedIndex = targets.findIndex((target, index) => {
return (
unmatched.has(index) &&
target.side === order.side &&
target.reduceOnly === reduceOnly &&
Math.abs(price - target.price) <= tolerance
);
});
if (matchedIndex >= 0) {
unmatched.delete(matchedIndex);
} else {
toCancel.push(order);
}
}
const toPlace = [...unmatched]
.map((idx) => targets[idx])
.filter((t): t is OrderTarget => t !== undefined && t.amount > 1e-5);
return { toCancel, toPlace };
}
+22
View File
@@ -0,0 +1,22 @@
import type { ExchangeAdapter } from "../../exchanges/adapter";
import type { AsterOrder } from "../../exchanges/types";
import { isUnknownOrderError } from "../../utils/errors";
export async function safeCancelOrder(
exchange: ExchangeAdapter,
symbol: string,
order: AsterOrder,
onResolved: (orderId: number) => void,
onUnknown: () => void,
onError: (err: unknown) => void
): Promise<void> {
try {
await exchange.cancelOrder({ symbol, orderId: order.orderId });
onResolved(order.orderId);
} catch (error) {
if (isUnknownOrderError(error)) onUnknown();
else onError(error);
}
}
+47 -83
View File
@@ -10,12 +10,17 @@ import { toPrice1Decimal } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../state/trade-log";
import { isUnknownOrderError } from "../utils/errors";
import { getPosition, type PositionSnapshot } from "../utils/strategy";
import { computePositionPnl } from "../utils/pnl";
import { getTopPrices, getMidOrLast } from "../utils/price";
import { shouldStopLoss } from "../utils/risk";
import {
marketClose,
placeOrder,
unlockOperating,
} from "./order-coordinator";
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
import { makeOrderPlan } from "./lib/order-plan";
import { safeCancelOrder } from "./lib/orders";
interface DesiredOrder {
side: "BUY" | "SELL";
@@ -221,17 +226,14 @@ export class MakerEngine {
}
const depth = this.depthSnapshot!;
const bidLevel = depth.bids?.[0];
const askLevel = depth.asks?.[0];
const topBid = bidLevel ? Number(bidLevel[0]) : undefined;
const topAsk = askLevel ? Number(askLevel[0]) : undefined;
if (!Number.isFinite(topBid) || !Number.isFinite(topAsk)) {
const { topBid, topAsk } = getTopPrices(depth);
if (topBid == null || topAsk == null) {
this.emitUpdate();
return;
}
const bidPrice = toPrice1Decimal(topBid! - this.config.bidOffset);
const askPrice = toPrice1Decimal(topAsk! + this.config.askOffset);
const bidPrice = toPrice1Decimal(topBid - this.config.bidOffset);
const askPrice = toPrice1Decimal(topAsk + this.config.askOffset);
const position = getPosition(this.accountSnapshot, this.config.symbol);
const absPosition = Math.abs(position.positionAmt);
const desired: DesiredOrder[] = [];
@@ -290,52 +292,36 @@ export class MakerEngine {
private async syncOrders(targets: DesiredOrder[]): Promise<void> {
const tolerance = this.config.priceChaseThreshold;
const unmatched = new Set(targets.map((_, idx) => idx));
const toCancel: AsterOrder[] = [];
for (const order of this.openOrders) {
const price = Number(order.price);
if (!Number.isFinite(price)) {
toCancel.push(order);
continue;
}
const reduceOnly = order.reduceOnly === true;
if (this.pendingCancelOrders.has(order.orderId)) {
continue;
}
const matchedIndex = targets.findIndex((target, index) => {
if (!unmatched.has(index)) return false;
if (target.side !== order.side) return false;
if (target.reduceOnly !== reduceOnly) return false;
return Math.abs(price - target.price) <= tolerance;
});
if (matchedIndex >= 0) {
unmatched.delete(matchedIndex);
continue;
}
toCancel.push(order);
}
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(o.orderId));
const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance);
for (const order of toCancel) {
if (this.pendingCancelOrders.has(order.orderId)) continue;
this.pendingCancelOrders.add(order.orderId);
try {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
this.tradeLog.push("order", `撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`);
} catch (error) {
if (isUnknownOrderError(error)) {
await safeCancelOrder(
this.exchange,
this.config.symbol,
order,
() => {
this.tradeLog.push(
"order",
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
);
},
() => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.pendingCancelOrders.delete(order.orderId);
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} else {
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId);
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
}
);
}
for (const index of unmatched) {
const target = targets[index];
for (const target of toPlace) {
if (!target) continue;
if (target.amount < EPS) continue;
try {
@@ -376,20 +362,10 @@ export class MakerEngine {
}
this.entryPricePendingLogged = false;
const pnl = position.positionAmt > 0
? (bidPrice - position.entryPrice) * absPosition
: (position.entryPrice - askPrice) * absPosition;
const unrealized = Number.isFinite(position.unrealizedProfit)
? position.unrealizedProfit
: null;
const derivedLoss = pnl < -this.config.lossLimit;
const snapshotLoss = Boolean(
unrealized != null &&
unrealized < -this.config.lossLimit &&
pnl <= 0
);
const pnl = computePositionPnl(position, bidPrice, askPrice);
const triggerStop = shouldStopLoss(position, bidPrice, askPrice, this.config.lossLimit);
if (derivedLoss || snapshotLoss) {
if (triggerStop) {
// 价格操纵保护:只有平仓方向价格与标记价格在阈值内才允许市价平仓
const closeSideIsSell = position.positionAmt > 0;
const closeSidePrice = closeSideIsSell ? bidPrice : askPrice;
@@ -430,20 +406,24 @@ export class MakerEngine {
for (const order of this.openOrders) {
if (this.pendingCancelOrders.has(order.orderId)) continue;
this.pendingCancelOrders.add(order.orderId);
try {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
} catch (error) {
if (isUnknownOrderError(error)) {
await safeCancelOrder(
this.exchange,
this.config.symbol,
order,
() => {
// 成功撤销不记录日志,保持现有行为
},
() => {
this.tradeLog.push("order", "订单已不存在,撤销跳过");
this.pendingCancelOrders.delete(order.orderId);
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} else {
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId);
// 与偏移做市保持一致:移除本地缓存中的异常订单,等待订单流推送重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
}
);
}
}
@@ -467,23 +447,15 @@ export class MakerEngine {
private buildSnapshot(): MakerEngineSnapshot {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const bid = this.depthSnapshot?.bids?.[0]?.[0];
const ask = this.depthSnapshot?.asks?.[0]?.[0];
const bidNum = Number(bid);
const askNum = Number(ask);
const spread = Number.isFinite(bidNum) && Number.isFinite(askNum) ? askNum - bidNum : null;
const priceForPnl = position.positionAmt > 0 ? bidNum : askNum;
const pnl = Number.isFinite(priceForPnl)
? (position.positionAmt > 0
? (priceForPnl! - position.entryPrice) * Math.abs(position.positionAmt)
: (position.entryPrice - priceForPnl!) * Math.abs(position.positionAmt))
: 0;
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
const spread = topBid != null && topAsk != null ? topAsk - topBid : null;
const pnl = computePositionPnl(position, topBid, topAsk);
return {
ready: this.isReady(),
symbol: this.config.symbol,
topBid: Number.isFinite(bidNum) ? bidNum : null,
topAsk: Number.isFinite(askNum) ? askNum : null,
topBid: topBid,
topAsk: topAsk,
spread,
position,
pnl,
@@ -515,14 +487,6 @@ export class MakerEngine {
}
private getReferencePrice(): number | null {
const bid = Number(this.depthSnapshot?.bids?.[0]?.[0]);
const ask = Number(this.depthSnapshot?.asks?.[0]?.[0]);
if (Number.isFinite(bid) && Number.isFinite(ask)) return (bid + ask) / 2;
if (this.tickerSnapshot) {
const last = Number(this.tickerSnapshot.lastPrice);
if (Number.isFinite(last)) return last;
}
return null;
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
}
}
+49 -100
View File
@@ -10,6 +10,10 @@ import { toPrice1Decimal } from "../utils/math";
import { createTradeLog } from "../state/trade-log";
import { isUnknownOrderError } from "../utils/errors";
import { getPosition, type PositionSnapshot } from "../utils/strategy";
import { computeDepthStats } from "../utils/depth";
import { computePositionPnl } from "../utils/pnl";
import { getTopPrices, getMidOrLast } from "../utils/price";
import { shouldStopLoss } from "../utils/risk";
import {
marketClose,
placeOrder,
@@ -17,6 +21,8 @@ import {
} from "./order-coordinator";
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
import type { MakerEngineSnapshot } from "./maker-engine";
import { makeOrderPlan } from "./lib/order-plan";
import { safeCancelOrder } from "./lib/orders";
interface DesiredOrder {
side: "BUY" | "SELL";
@@ -219,11 +225,8 @@ export class OffsetMakerEngine {
}
const depth = this.depthSnapshot!;
const bidLevel = depth.bids?.[0];
const askLevel = depth.asks?.[0];
const topBid = bidLevel ? Number(bidLevel[0]) : undefined;
const topAsk = askLevel ? Number(askLevel[0]) : undefined;
if (!Number.isFinite(topBid) || !Number.isFinite(topAsk)) {
const { topBid, topAsk } = getTopPrices(depth);
if (topBid == null || topAsk == null) {
this.emitUpdate();
return;
}
@@ -310,27 +313,8 @@ export class OffsetMakerEngine {
skipSellSide: boolean;
imbalance: "balanced" | "buy_dominant" | "sell_dominant";
} {
const topBids = (depth.bids ?? []).slice(0, 10);
const topAsks = (depth.asks ?? []).slice(0, 10);
const buySum = topBids.reduce((total, [price, qty]) => {
const size = Number(qty);
return Number.isFinite(size) ? total + size : total;
}, 0);
const sellSum = topAsks.reduce((total, [price, qty]) => {
const size = Number(qty);
return Number.isFinite(size) ? total + size : total;
}, 0);
const skipSellSide = sellSum === 0 || sellSum * 3 < buySum;
const skipBuySide = buySum === 0 || buySum * 3 < sellSum;
let imbalance: "balanced" | "buy_dominant" | "sell_dominant" = "balanced";
if (buySum > sellSum * 3) {
imbalance = "buy_dominant";
} else if (sellSum > buySum * 3) {
imbalance = "sell_dominant";
}
return { buySum, sellSum, skipBuySide, skipSellSide, imbalance };
// Keep existing behavior: 10 levels, ratio threshold 3x
return computeDepthStats(depth, 10, 3);
}
private async handleImbalanceExit(
@@ -384,53 +368,38 @@ export class OffsetMakerEngine {
private async syncOrders(targets: DesiredOrder[]): Promise<void> {
const tolerance = this.config.priceChaseThreshold;
const unmatched = new Set(targets.map((_, idx) => idx));
const toCancel: AsterOrder[] = [];
for (const order of this.openOrders) {
const price = Number(order.price);
if (!Number.isFinite(price)) {
toCancel.push(order);
continue;
}
const reduceOnly = order.reduceOnly === true;
if (this.pendingCancelOrders.has(order.orderId)) {
continue;
}
const matchedIndex = targets.findIndex((target, index) => {
if (!unmatched.has(index)) return false;
if (target.side !== order.side) return false;
if (target.reduceOnly !== reduceOnly) return false;
return Math.abs(price - target.price) <= tolerance;
});
if (matchedIndex >= 0) {
unmatched.delete(matchedIndex);
continue;
}
toCancel.push(order);
}
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(o.orderId));
const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance);
for (const order of toCancel) {
if (this.pendingCancelOrders.has(order.orderId)) continue;
this.pendingCancelOrders.add(order.orderId);
try {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
this.tradeLog.push("order", `撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`);
} catch (error) {
if (isUnknownOrderError(error)) {
await safeCancelOrder(
this.exchange,
this.config.symbol,
order,
() => {
this.tradeLog.push(
"order",
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
);
// 保持与原逻辑一致:成功撤销不立即修改本地 openOrders,等待订单流重建
},
() => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.pendingCancelOrders.delete(order.orderId);
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} else {
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId);
// 避免同一轮内重复操作同一张已出错的本地挂单,直接从本地缓存移除,等待下一次订单推送重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
}
);
}
for (const index of unmatched) {
const target = targets[index];
for (const target of toPlace) {
if (!target) continue;
if (target.amount < EPS) continue;
try {
@@ -471,20 +440,10 @@ export class OffsetMakerEngine {
}
this.entryPricePendingLogged = false;
const pnl = position.positionAmt > 0
? (bidPrice - position.entryPrice) * absPosition
: (position.entryPrice - askPrice) * absPosition;
const unrealized = Number.isFinite(position.unrealizedProfit)
? position.unrealizedProfit
: null;
const derivedLoss = pnl < -this.config.lossLimit;
const snapshotLoss = Boolean(
unrealized != null &&
unrealized < -this.config.lossLimit &&
pnl <= 0
);
const pnl = computePositionPnl(position, bidPrice, askPrice);
const triggerStop = shouldStopLoss(position, bidPrice, askPrice, this.config.lossLimit);
if (derivedLoss || snapshotLoss) {
if (triggerStop) {
this.tradeLog.push(
"stop",
`触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT`
@@ -522,20 +481,25 @@ export class OffsetMakerEngine {
for (const order of this.openOrders) {
if (this.pendingCancelOrders.has(order.orderId)) continue;
this.pendingCancelOrders.add(order.orderId);
try {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
} catch (error) {
if (isUnknownOrderError(error)) {
await safeCancelOrder(
this.exchange,
this.config.symbol,
order,
() => {
// 与原逻辑保持一致:成功撤销不记录日志且不修改本地 openOrders
},
() => {
this.tradeLog.push("order", "订单已不存在,撤销跳过");
this.pendingCancelOrders.delete(order.orderId);
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
} else {
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.pendingCancelOrders.delete(order.orderId);
// 与同步撤单路径保持一致,移除本地异常订单,等待订单流重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
}
);
}
}
@@ -559,23 +523,15 @@ export class OffsetMakerEngine {
private buildSnapshot(): OffsetMakerEngineSnapshot {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const bid = this.depthSnapshot?.bids?.[0]?.[0];
const ask = this.depthSnapshot?.asks?.[0]?.[0];
const bidNum = Number(bid);
const askNum = Number(ask);
const spread = Number.isFinite(bidNum) && Number.isFinite(askNum) ? askNum - bidNum : null;
const priceForPnl = position.positionAmt > 0 ? bidNum : askNum;
const pnl = Number.isFinite(priceForPnl)
? (position.positionAmt > 0
? (priceForPnl! - position.entryPrice) * Math.abs(position.positionAmt)
: (position.entryPrice - priceForPnl!) * Math.abs(position.positionAmt))
: 0;
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
const spread = topBid != null && topAsk != null ? topAsk - topBid : null;
const pnl = computePositionPnl(position, topBid, topAsk);
return {
ready: this.isReady(),
symbol: this.config.symbol,
topBid: Number.isFinite(bidNum) ? bidNum : null,
topAsk: Number.isFinite(askNum) ? askNum : null,
topBid: topBid,
topAsk: topAsk,
spread,
position,
pnl,
@@ -612,13 +568,6 @@ export class OffsetMakerEngine {
}
private getReferencePrice(): number | null {
const bid = Number(this.depthSnapshot?.bids?.[0]?.[0]);
const ask = Number(this.depthSnapshot?.asks?.[0]?.[0]);
if (Number.isFinite(bid) && Number.isFinite(ask)) return (bid + ask) / 2;
if (this.tickerSnapshot) {
const last = Number(this.tickerSnapshot.lastPrice);
if (Number.isFinite(last)) return last;
}
return null;
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
}
}
+4 -16
View File
@@ -14,6 +14,8 @@ import {
getSMA,
type PositionSnapshot,
} from "../utils/strategy";
import { computePositionPnl } from "../utils/pnl";
import { getTopPrices, getMidOrLast } from "../utils/price";
import {
marketClose,
placeMarketOrder,
@@ -601,11 +603,7 @@ export class TrendEngine {
: price < sma30
? "做空"
: "无信号";
const pnl = price != null && position
? (position.positionAmt > 0
? (price - position.entryPrice) * Math.abs(position.positionAmt)
: (position.entryPrice - price) * Math.abs(position.positionAmt))
: 0;
const pnl = price != null ? computePositionPnl(position, price, price) : 0;
return {
ready: this.isReady(),
symbol: this.config.symbol,
@@ -646,17 +644,7 @@ export class TrendEngine {
}
private getReferencePrice(): number | null {
if (this.tickerSnapshot) {
const last = Number(this.tickerSnapshot.lastPrice);
if (Number.isFinite(last)) return last;
}
if (this.depthSnapshot) {
const bid = Number(this.depthSnapshot.bids?.[0]?.[0]);
const ask = Number(this.depthSnapshot.asks?.[0]?.[0]);
if (Number.isFinite(bid) && Number.isFinite(ask)) return (bid + ask) / 2;
}
if (this.lastPrice != null && Number.isFinite(this.lastPrice)) return this.lastPrice;
return null;
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot) ?? (this.lastPrice != null && Number.isFinite(this.lastPrice) ? this.lastPrice : null);
}
}
+42
View File
@@ -0,0 +1,42 @@
import type { AsterDepth } from "../exchanges/types";
export type DepthImbalance = "balanced" | "buy_dominant" | "sell_dominant";
export function computeDepthStats(
depth: AsterDepth,
levels = 10,
ratio = 3
): {
buySum: number;
sellSum: number;
skipBuySide: boolean;
skipSellSide: boolean;
imbalance: DepthImbalance;
} {
const topBids = (depth.bids ?? []).slice(0, levels);
const topAsks = (depth.asks ?? []).slice(0, levels);
const buySum = topBids.reduce((total, level) => {
const qty = Number(level?.[1]);
return Number.isFinite(qty) ? total + qty : total;
}, 0);
const sellSum = topAsks.reduce((total, level) => {
const qty = Number(level?.[1]);
return Number.isFinite(qty) ? total + qty : total;
}, 0);
const skipSellSide = sellSum === 0 || sellSum * ratio < buySum;
const skipBuySide = buySum === 0 || buySum * ratio < sellSum;
let imbalance: DepthImbalance = "balanced";
if (buySum > sellSum * ratio) {
imbalance = "buy_dominant";
} else if (sellSum > buySum * ratio) {
imbalance = "sell_dominant";
}
return { buySum, sellSum, skipBuySide, skipSellSide, imbalance };
}
+16
View File
@@ -0,0 +1,16 @@
import type { PositionSnapshot } from "./strategy";
export function computePositionPnl(
position: PositionSnapshot,
bestBid?: number | null,
bestAsk?: number | null
): number {
const priceForPnl = position.positionAmt > 0 ? bestBid : bestAsk;
if (!Number.isFinite(priceForPnl as number)) return 0;
const absAmt = Math.abs(position.positionAmt);
return position.positionAmt > 0
? ((priceForPnl as number) - position.entryPrice) * absAmt
: (position.entryPrice - (priceForPnl as number)) * absAmt;
}
+19
View File
@@ -0,0 +1,19 @@
import type { AsterDepth, AsterTicker } from "../exchanges/types";
export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null; topAsk: number | null } {
const bid = Number(depth?.bids?.[0]?.[0]);
const ask = Number(depth?.asks?.[0]?.[0]);
return {
topBid: Number.isFinite(bid) ? bid : null,
topAsk: Number.isFinite(ask) ? ask : null,
};
}
export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | null): number | null {
const { topBid, topAsk } = getTopPrices(depth);
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
const last = Number(ticker?.lastPrice);
return Number.isFinite(last) ? last : null;
}
+26
View File
@@ -0,0 +1,26 @@
import type { PositionSnapshot } from "./strategy";
export function shouldStopLoss(
position: PositionSnapshot,
bestBid: number,
bestAsk: number,
lossLimit: number
): boolean {
const absPosition = Math.abs(position.positionAmt);
if (absPosition < 1e-5) return false;
const pnl = position.positionAmt > 0
? (bestBid - position.entryPrice) * absPosition
: (position.entryPrice - bestAsk) * absPosition;
const unrealized = Number.isFinite(position.unrealizedProfit)
? (position.unrealizedProfit as number)
: null;
const derivedLoss = pnl < -lossLimit;
const snapshotLoss = Boolean(unrealized != null && unrealized < -lossLimit && pnl <= 0);
return derivedLoss || snapshotLoss;
}