mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
feat: 更新订单处理逻辑,将价格类型改为字符串以避免精度问题,并添加价格格式化函数
This commit is contained in:
@@ -2,7 +2,7 @@ import type { AsterOrder } from "../../exchanges/types";
|
|||||||
|
|
||||||
export interface OrderTarget {
|
export interface OrderTarget {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
price: number;
|
price: string; // 改为字符串避免精度问题
|
||||||
amount: number;
|
amount: number;
|
||||||
reduceOnly: boolean;
|
reduceOnly: boolean;
|
||||||
}
|
}
|
||||||
@@ -15,18 +15,14 @@ export function makeOrderPlan(
|
|||||||
const toCancel: AsterOrder[] = [];
|
const toCancel: AsterOrder[] = [];
|
||||||
|
|
||||||
for (const order of openOrders) {
|
for (const order of openOrders) {
|
||||||
const price = Number(order.price);
|
const orderPrice = String(order.price);
|
||||||
if (!Number.isFinite(price)) {
|
|
||||||
toCancel.push(order);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const reduceOnly = order.reduceOnly === true;
|
const reduceOnly = order.reduceOnly === true;
|
||||||
const matchedIndex = targets.findIndex((target, index) => {
|
const matchedIndex = targets.findIndex((target, index) => {
|
||||||
return (
|
return (
|
||||||
unmatched.has(index) &&
|
unmatched.has(index) &&
|
||||||
target.side === order.side &&
|
target.side === order.side &&
|
||||||
target.reduceOnly === reduceOnly &&
|
target.reduceOnly === reduceOnly &&
|
||||||
price === target.price
|
orderPrice === target.price // 直接使用字符串比较
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (matchedIndex >= 0) {
|
if (matchedIndex >= 0) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../exchanges/types";
|
import type { AsterOrder, CreateOrderParams } from "../exchanges/types";
|
||||||
import { roundDownToTick, roundQtyDownToStep } from "../utils/math";
|
import { roundDownToTick, roundQtyDownToStep, formatPriceToString } from "../utils/math";
|
||||||
import { isUnknownOrderError } from "../utils/errors";
|
import { isUnknownOrderError } from "../utils/errors";
|
||||||
import { isOrderPriceAllowedByMark } from "../utils/strategy";
|
import { isOrderPriceAllowedByMark } from "../utils/strategy";
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ export async function placeOrder(
|
|||||||
timers: OrderTimerMap,
|
timers: OrderTimerMap,
|
||||||
pendings: OrderPendingMap,
|
pendings: OrderPendingMap,
|
||||||
side: "BUY" | "SELL",
|
side: "BUY" | "SELL",
|
||||||
price: number,
|
price: string, // 改为字符串价格
|
||||||
amount: number,
|
amount: number,
|
||||||
log: LogHandler,
|
log: LogHandler,
|
||||||
reduceOnly = false,
|
reduceOnly = false,
|
||||||
@@ -131,7 +131,8 @@ export async function placeOrder(
|
|||||||
): Promise<AsterOrder | undefined> {
|
): Promise<AsterOrder | undefined> {
|
||||||
const type = "LIMIT";
|
const type = "LIMIT";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, price, guard, log, "限价单")) return;
|
const priceNum = Number(price);
|
||||||
|
if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return;
|
||||||
const priceTick = opts?.priceTick ?? 0.1;
|
const priceTick = opts?.priceTick ?? 0.1;
|
||||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||||
const params: CreateOrderParams = {
|
const params: CreateOrderParams = {
|
||||||
@@ -139,7 +140,7 @@ export async function placeOrder(
|
|||||||
side,
|
side,
|
||||||
type,
|
type,
|
||||||
quantity: roundQtyDownToStep(amount, qtyStep),
|
quantity: roundQtyDownToStep(amount, qtyStep),
|
||||||
price: roundDownToTick(price, priceTick),
|
price: priceNum, // 直接使用字符串转换的数字,不再格式化
|
||||||
timeInForce: "GTX",
|
timeInForce: "GTX",
|
||||||
};
|
};
|
||||||
if (reduceOnly) params.reduceOnly = "true";
|
if (reduceOnly) params.reduceOnly = "true";
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import type {
|
|||||||
|
|
||||||
const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000;
|
const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000;
|
||||||
const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500;
|
const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500;
|
||||||
const DEFAULT_DEPTH_POLL_INTERVAL_MS = 750;
|
const DEFAULT_DEPTH_POLL_INTERVAL_MS = 400;
|
||||||
const DEFAULT_TICKER_POLL_INTERVAL_MS = 1000;
|
const DEFAULT_TICKER_POLL_INTERVAL_MS = 1000;
|
||||||
const DEFAULT_KLINE_POLL_INTERVAL_MS = 15000;
|
const DEFAULT_KLINE_POLL_INTERVAL_MS = 15000;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
AsterOrder,
|
AsterOrder,
|
||||||
AsterTicker,
|
AsterTicker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { roundDownToTick } from "../utils/math";
|
import { formatPriceToString } 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 } from "../utils/strategy";
|
import { getPosition } from "../utils/strategy";
|
||||||
@@ -30,7 +30,7 @@ import { SessionVolumeTracker } from "./common/session-volume";
|
|||||||
|
|
||||||
interface DesiredOrder {
|
interface DesiredOrder {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
price: number;
|
price: string; // 改为字符串价格
|
||||||
amount: number;
|
amount: number;
|
||||||
reduceOnly: boolean;
|
reduceOnly: boolean;
|
||||||
}
|
}
|
||||||
@@ -244,10 +244,12 @@ export class MakerEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeBidPrice = roundDownToTick(topBid, this.config.priceTick);
|
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
||||||
const closeAskPrice = roundDownToTick(topAsk, this.config.priceTick);
|
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
||||||
const bidPrice = roundDownToTick(topBid - this.config.bidOffset, this.config.priceTick);
|
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
||||||
const askPrice = roundDownToTick(topAsk + this.config.askOffset, this.config.priceTick);
|
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
||||||
|
const bidPrice = formatPriceToString(topBid - this.config.bidOffset, priceDecimals);
|
||||||
|
const askPrice = formatPriceToString(topAsk + this.config.askOffset, priceDecimals);
|
||||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||||
const absPosition = Math.abs(position.positionAmt);
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
const desired: DesiredOrder[] = [];
|
const desired: DesiredOrder[] = [];
|
||||||
@@ -268,7 +270,7 @@ export class MakerEngine {
|
|||||||
this.desiredOrders = desired;
|
this.desiredOrders = desired;
|
||||||
this.sessionVolume.update(position, this.getReferencePrice());
|
this.sessionVolume.update(position, this.getReferencePrice());
|
||||||
await this.syncOrders(desired);
|
await this.syncOrders(desired);
|
||||||
await this.checkRisk(position, closeBidPrice, closeAskPrice);
|
await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice));
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRateLimitError(error)) {
|
if (isRateLimitError(error)) {
|
||||||
@@ -291,9 +293,10 @@ export class MakerEngine {
|
|||||||
if (Math.abs(position.positionAmt) < EPS) return;
|
if (Math.abs(position.positionAmt) < EPS) return;
|
||||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
if (topBid == null || topAsk == null) return;
|
if (topBid == null || topAsk == null) return;
|
||||||
const closeBidPrice = roundDownToTick(topBid, this.config.priceTick);
|
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
||||||
const closeAskPrice = roundDownToTick(topAsk, this.config.priceTick);
|
const closeBidPrice = formatPriceToString(topBid, priceDecimals);
|
||||||
await this.checkRisk(position, closeBidPrice, closeAskPrice);
|
const closeAskPrice = formatPriceToString(topAsk, priceDecimals);
|
||||||
|
await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice));
|
||||||
await this.flushOrders();
|
await this.flushOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,13 +371,17 @@ export class MakerEngine {
|
|||||||
this.timers,
|
this.timers,
|
||||||
this.pending,
|
this.pending,
|
||||||
target.side,
|
target.side,
|
||||||
target.price,
|
target.price, // 已经是字符串价格
|
||||||
target.amount,
|
target.amount,
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
(type, detail) => this.tradeLog.push(type, detail),
|
||||||
target.reduceOnly,
|
target.reduceOnly,
|
||||||
{
|
{
|
||||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priceTick: this.config.priceTick,
|
||||||
|
qtyStep: 0.001, // 默认数量步长
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
AsterOrder,
|
AsterOrder,
|
||||||
AsterTicker,
|
AsterTicker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { roundDownToTick } from "../utils/math";
|
import { formatPriceToString } 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 } from "../utils/strategy";
|
import { getPosition } from "../utils/strategy";
|
||||||
@@ -32,7 +32,7 @@ import { SessionVolumeTracker } from "./common/session-volume";
|
|||||||
|
|
||||||
interface DesiredOrder {
|
interface DesiredOrder {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
price: number;
|
price: string; // 改为字符串价格
|
||||||
amount: number;
|
amount: number;
|
||||||
reduceOnly: boolean;
|
reduceOnly: boolean;
|
||||||
}
|
}
|
||||||
@@ -236,6 +236,7 @@ export class OffsetMakerEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保使用最新的深度数据
|
||||||
const depth = this.depthSnapshot!;
|
const depth = this.depthSnapshot!;
|
||||||
const { topBid, topAsk } = getTopPrices(depth);
|
const { topBid, topAsk } = getTopPrices(depth);
|
||||||
if (topBid == null || topAsk == null) {
|
if (topBid == null || topAsk == null) {
|
||||||
@@ -257,10 +258,18 @@ export class OffsetMakerEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeBidPrice = roundDownToTick(topBid!, this.config.priceTick);
|
// 在计算挂单价格前,重新获取最新的深度数据以确保价格同步
|
||||||
const closeAskPrice = roundDownToTick(topAsk!, this.config.priceTick);
|
const latestDepth = this.depthSnapshot!;
|
||||||
const bidPrice = roundDownToTick(topBid! - this.config.bidOffset, this.config.priceTick);
|
const { topBid: latestBid, topAsk: latestAsk } = getTopPrices(latestDepth);
|
||||||
const askPrice = roundDownToTick(topAsk! + this.config.askOffset, this.config.priceTick);
|
const finalBid = latestBid ?? topBid!;
|
||||||
|
const finalAsk = latestAsk ?? topAsk!;
|
||||||
|
|
||||||
|
// 直接使用orderbook价格,格式化为字符串避免精度问题
|
||||||
|
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
||||||
|
const closeBidPrice = formatPriceToString(finalBid, priceDecimals);
|
||||||
|
const closeAskPrice = formatPriceToString(finalAsk, priceDecimals);
|
||||||
|
const bidPrice = formatPriceToString(finalBid - this.config.bidOffset, priceDecimals);
|
||||||
|
const askPrice = formatPriceToString(finalAsk + this.config.askOffset, priceDecimals);
|
||||||
const absPosition = Math.abs(position.positionAmt);
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
const desired: DesiredOrder[] = [];
|
const desired: DesiredOrder[] = [];
|
||||||
const canEnter = !this.rateLimit.shouldBlockEntries();
|
const canEnter = !this.rateLimit.shouldBlockEntries();
|
||||||
@@ -282,7 +291,7 @@ export class OffsetMakerEngine {
|
|||||||
this.desiredOrders = desired;
|
this.desiredOrders = desired;
|
||||||
this.sessionVolume.update(position, this.getReferencePrice());
|
this.sessionVolume.update(position, this.getReferencePrice());
|
||||||
await this.syncOrders(desired);
|
await this.syncOrders(desired);
|
||||||
await this.checkRisk(position, closeBidPrice, closeAskPrice);
|
await this.checkRisk(position, Number(closeBidPrice), Number(closeAskPrice));
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRateLimitError(error)) {
|
if (isRateLimitError(error)) {
|
||||||
@@ -307,8 +316,9 @@ export class OffsetMakerEngine {
|
|||||||
const absPosition = Math.abs(position.positionAmt);
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
|
const side: "BUY" | "SELL" = position.positionAmt > 0 ? "SELL" : "BUY";
|
||||||
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
const closeBidPrice = topBid != null ? roundDownToTick(topBid, this.config.priceTick) : null;
|
const priceDecimals = Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)));
|
||||||
const closeAskPrice = topAsk != null ? roundDownToTick(topAsk, this.config.priceTick) : null;
|
const closeBidPrice = topBid != null ? formatPriceToString(topBid, priceDecimals) : null;
|
||||||
|
const closeAskPrice = topAsk != null ? formatPriceToString(topAsk, priceDecimals) : null;
|
||||||
try {
|
try {
|
||||||
await marketClose(
|
await marketClose(
|
||||||
this.exchange,
|
this.exchange,
|
||||||
@@ -471,13 +481,17 @@ export class OffsetMakerEngine {
|
|||||||
this.timers,
|
this.timers,
|
||||||
this.pending,
|
this.pending,
|
||||||
target.side,
|
target.side,
|
||||||
target.price,
|
target.price, // 已经是字符串价格
|
||||||
target.amount,
|
target.amount,
|
||||||
(type, detail) => this.tradeLog.push(type, detail),
|
(type, detail) => this.tradeLog.push(type, detail),
|
||||||
target.reduceOnly,
|
target.reduceOnly,
|
||||||
{
|
{
|
||||||
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
markPrice: getPosition(this.accountSnapshot, this.config.symbol).markPrice,
|
||||||
maxPct: this.config.maxCloseSlippagePct,
|
maxPct: this.config.maxCloseSlippagePct,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priceTick: this.config.priceTick,
|
||||||
|
qtyStep: 0.001, // 默认数量步长
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
} from "../core/order-coordinator";
|
} from "../core/order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||||
import { extractMessage, isUnknownOrderError } from "../utils/errors";
|
import { extractMessage, isUnknownOrderError } from "../utils/errors";
|
||||||
import { roundDownToTick } from "../utils/math";
|
import { formatPriceToString } from "../utils/math";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
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";
|
||||||
@@ -557,7 +557,7 @@ export class TrendEngine {
|
|||||||
const rawTarget = direction === "long"
|
const rawTarget = direction === "long"
|
||||||
? position.entryPrice + steps * stepPx
|
? position.entryPrice + steps * stepPx
|
||||||
: position.entryPrice - steps * stepPx;
|
: position.entryPrice - steps * stepPx;
|
||||||
let targetStop = roundDownToTick(rawTarget, this.config.priceTick);
|
let targetStop = Number(formatPriceToString(rawTarget, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)))));
|
||||||
|
|
||||||
// 不允许下一次移动超过动态止盈订单的激活价
|
// 不允许下一次移动超过动态止盈订单的激活价
|
||||||
if (Number.isFinite(trailingActivate)) {
|
if (Number.isFinite(trailingActivate)) {
|
||||||
@@ -639,13 +639,13 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!currentStop) {
|
if (!currentStop) {
|
||||||
await this.tryPlaceStopLoss(stopSide, roundDownToTick(stopPrice, this.config.priceTick), price);
|
await this.tryPlaceStopLoss(stopSide, Number(formatPriceToString(stopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))), price);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentTrailing && this.exchange.supportsTrailingStops()) {
|
if (!currentTrailing && this.exchange.supportsTrailingStops()) {
|
||||||
await this.tryPlaceTrailingStop(
|
await this.tryPlaceTrailingStop(
|
||||||
stopSide,
|
stopSide,
|
||||||
roundDownToTick(activationPrice, this.config.priceTick),
|
Number(formatPriceToString(activationPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))),
|
||||||
Math.abs(position.positionAmt)
|
Math.abs(position.positionAmt)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -815,7 +815,7 @@ export class TrendEngine {
|
|||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
||||||
);
|
);
|
||||||
if (order) {
|
if (order) {
|
||||||
this.tradeLog.push("stop", `移动止损到 ${roundDownToTick(nextStopPrice, this.config.priceTick)}`);
|
this.tradeLog.push("stop", `移动止损到 ${formatPriceToString(nextStopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.tradeLog.push("error", `移动止损失败: ${String(err)}`);
|
this.tradeLog.push("error", `移动止损失败: ${String(err)}`);
|
||||||
@@ -846,7 +846,7 @@ export class TrendEngine {
|
|||||||
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
|
||||||
);
|
);
|
||||||
if (restored) {
|
if (restored) {
|
||||||
this.tradeLog.push("order", `恢复原止损 @ ${roundDownToTick(existingStopPrice, this.config.priceTick)}`);
|
this.tradeLog.push("order", `恢复原止损 @ ${formatPriceToString(existingStopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (recoverErr) {
|
} catch (recoverErr) {
|
||||||
|
|||||||
@@ -21,3 +21,14 @@ export function decimalsOf(step: number): number {
|
|||||||
export function isNearlyZero(value: number, epsilon = 1e-5): boolean {
|
export function isNearlyZero(value: number, epsilon = 1e-5): boolean {
|
||||||
return Math.abs(value) < epsilon;
|
return Math.abs(value) < epsilon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将价格格式化为指定小数位数的字符串
|
||||||
|
* @param price 原始价格
|
||||||
|
* @param decimals 小数位数
|
||||||
|
* @returns 格式化后的价格字符串
|
||||||
|
*/
|
||||||
|
export function formatPriceToString(price: number, decimals: number): string {
|
||||||
|
if (!Number.isFinite(price)) return "0";
|
||||||
|
return price.toFixed(decimals);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user