feat: enhance quantity and price rounding functions for improved precision in trading calculations

This commit is contained in:
discountry
2025-11-06 21:06:03 +08:00
parent 0f70c6b6aa
commit d0d1afa0ea
5 changed files with 92 additions and 18 deletions
+2 -1
View File
@@ -495,7 +495,8 @@ export class MakerEngine {
markPrice: position.markPrice,
expectedPrice: Number(closeSidePrice) || null,
maxPct: this.config.maxCloseSlippagePct,
}
},
{ qtyStep: this.qtyStep }
);
} catch (error) {
if (isUnknownOrderError(error)) {
+6 -3
View File
@@ -353,7 +353,8 @@ export class OffsetMakerEngine {
? (closeAskPrice != null ? Number(closeAskPrice) : null)
: (closeBidPrice != null ? Number(closeBidPrice) : null),
maxPct: this.config.maxCloseSlippagePct,
}
},
{ qtyStep: this.qtyStep }
);
} catch (error) {
if (isUnknownOrderError(error)) {
@@ -441,7 +442,8 @@ export class OffsetMakerEngine {
markPrice: position.markPrice,
expectedPrice: Number(closeSidePrice) || null,
maxPct: this.config.maxCloseSlippagePct,
}
},
{ qtyStep: this.qtyStep }
);
} catch (error) {
if (isUnknownOrderError(error)) {
@@ -587,7 +589,8 @@ export class OffsetMakerEngine {
markPrice: position.markPrice,
expectedPrice: Number(position.positionAmt > 0 ? bidPrice : askPrice) || null,
maxPct: this.config.maxCloseSlippagePct,
}
},
{ qtyStep: this.qtyStep }
);
} catch (error) {
if (isUnknownOrderError(error)) {
+17 -5
View File
@@ -743,8 +743,8 @@ export class TrendEngine {
: this.depthSnapshot?.asks?.[0]?.[0]
) || null,
maxPct: this.config.maxCloseSlippagePct,
},
{ qtyStep: this.config.qtyStep }
},
{ qtyStep: this.config.qtyStep }
);
result.closed = true;
this.tradeLog.push("close", `止损平仓: ${direction === "long" ? "SELL" : "BUY"}`);
@@ -783,7 +783,11 @@ export class TrendEngine {
}
try {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const quantity = Math.abs(position.positionAmt) || this.config.tradeAmount;
const quantity = Math.abs(position.positionAmt);
const minQty = this.config.qtyStep > 0 ? this.config.qtyStep / 2 : 1e-12;
if (quantity <= minQty) {
return;
}
await placeStopLossOrder(
this.exchange,
this.config.symbol,
@@ -839,7 +843,11 @@ export class TrendEngine {
// 仅在成功创建新止损单后记录“移动止损”日志
try {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const quantity = Math.abs(position.positionAmt) || this.config.tradeAmount;
const quantity = Math.abs(position.positionAmt);
const minQty = this.config.qtyStep > 0 ? this.config.qtyStep / 2 : 1e-12;
if (quantity <= minQty) {
return;
}
const order = await placeStopLossOrder(
this.exchange,
this.config.symbol,
@@ -866,7 +874,11 @@ export class TrendEngine {
// 回滚策略:尝试用原价恢复止损,以避免出现短时间内无止损保护
try {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const quantity = Math.abs(position.positionAmt) || this.config.tradeAmount;
const quantity = Math.abs(position.positionAmt);
const minQty = this.config.qtyStep > 0 ? this.config.qtyStep / 2 : 1e-12;
if (quantity <= minQty) {
return;
}
const restoreInvalid =
(side === "SELL" && existingStopPrice >= lastPrice) ||
(side === "BUY" && existingStopPrice <= lastPrice);
+42 -9
View File
@@ -1,21 +1,54 @@
const MAX_SCALE_DECIMALS = 12;
const STEP_TOLERANCE = 1e-9;
function resolveStepScale(step: number): { scale: number; stepInt: number; decimals: number } {
if (!Number.isFinite(step) || step <= 0) {
return { scale: 1, stepInt: 1, decimals: 0 };
}
let decimals = Math.min(MAX_SCALE_DECIMALS, Math.max(0, decimalsOf(step)));
let scale = Math.pow(10, decimals);
let scaledStep = step * scale;
while (decimals < MAX_SCALE_DECIMALS && Math.abs(Math.round(scaledStep) - scaledStep) > STEP_TOLERANCE) {
decimals += 1;
scale *= 10;
scaledStep = step * scale;
}
const stepInt = Math.max(1, Math.round(scaledStep));
return { scale, stepInt, decimals };
}
export function roundDownToTick(value: number, tick: number): number {
if (!Number.isFinite(value) || !Number.isFinite(tick) || tick <= 0) return value;
const scaled = Math.floor(value / tick) * tick;
// Avoid floating residuals
return Number(scaled.toFixed(Math.max(0, decimalsOf(tick))));
const sign = value < 0 ? -1 : 1;
const absValue = Math.abs(value);
const { scale, stepInt, decimals } = resolveStepScale(tick);
const scaledValue = Math.floor(absValue * scale + STEP_TOLERANCE);
const resultInt = Math.floor(scaledValue / stepInt) * stepInt;
const rounded = resultInt / scale;
return sign * Number(rounded.toFixed(decimals));
}
export function roundQtyDownToStep(value: number, step: number): number {
if (!Number.isFinite(value) || !Number.isFinite(step) || step <= 0) return value;
const scaled = Math.floor(value / step) * step;
return Number(scaled.toFixed(Math.max(0, decimalsOf(step))));
const sign = value < 0 ? -1 : 1;
const absValue = Math.abs(value);
const { scale, stepInt, decimals } = resolveStepScale(step);
const scaledValue = Math.floor(absValue * scale + STEP_TOLERANCE);
const resultInt = Math.floor(scaledValue / stepInt) * stepInt;
const rounded = resultInt / scale;
return sign * Number(rounded.toFixed(decimals));
}
export function decimalsOf(step: number): number {
const s = step.toString();
if (!s.includes(".")) return 0;
const fraction = s.split(".")[1];
return fraction ? fraction.length : 0;
if (!Number.isFinite(step)) return 0;
if (Number.isInteger(step)) return 0;
let decimals = 0;
let scaled = step;
while (decimals < MAX_SCALE_DECIMALS && Math.abs(Math.round(scaled) - scaled) > STEP_TOLERANCE) {
scaled *= 10;
decimals += 1;
}
return decimals;
}
export function isNearlyZero(value: number, epsilon = 1e-5): boolean {
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { roundQtyDownToStep, roundDownToTick, decimalsOf } from "../../src/utils/math";
describe("utils/math precision", () => {
it("keeps quantity intact when it matches the step exactly", () => {
expect(roundQtyDownToStep(0.01, 0.00001)).toBe(0.01);
expect(roundQtyDownToStep(0.01, 0.001)).toBe(0.01);
});
it("floors quantity to the nearest valid step without precision loss", () => {
expect(roundQtyDownToStep(1.23456789, 0.001)).toBe(1.234);
expect(roundQtyDownToStep(0.00009, 0.00005)).toBe(0.00005);
});
it("rounds prices down respecting tick size", () => {
expect(roundDownToTick(20345.123456, 0.001)).toBe(20345.123);
expect(roundDownToTick(1.00000009, 0.00001)).toBe(1);
});
it("detects decimal places for powers of ten", () => {
expect(decimalsOf(0.00000001)).toBe(8);
expect(decimalsOf(0.25)).toBe(2);
expect(decimalsOf(1)).toBe(0);
});
});