mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
feat: enhance quantity and price rounding functions for improved precision in trading calculations
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user