95 lines
2.5 KiB
TypeScript
95 lines
2.5 KiB
TypeScript
|
|
export interface AutotradeExecutionCostProfile {
|
||
|
|
buyFeeRate: number;
|
||
|
|
sellFeeRate: number;
|
||
|
|
sellTaxRate: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface AutotradeEstimatedOrderCost {
|
||
|
|
side: "buy" | "sell";
|
||
|
|
price: number;
|
||
|
|
quantity: number;
|
||
|
|
grossAmount: number;
|
||
|
|
feeAmount: number;
|
||
|
|
taxAmount: number;
|
||
|
|
netAmount: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveExecutionCostProfile(): AutotradeExecutionCostProfile {
|
||
|
|
return {
|
||
|
|
buyFeeRate: readRateFromEnv("AUTOTRADE_BUY_FEE_RATE", 0.00015),
|
||
|
|
sellFeeRate: readRateFromEnv("AUTOTRADE_SELL_FEE_RATE", 0.00015),
|
||
|
|
sellTaxRate: readRateFromEnv("AUTOTRADE_SELL_TAX_RATE", 0.0018),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function estimateBuyUnitCost(
|
||
|
|
price: number,
|
||
|
|
profile: AutotradeExecutionCostProfile,
|
||
|
|
) {
|
||
|
|
const safePrice = Math.max(0, price);
|
||
|
|
if (safePrice <= 0) return 0;
|
||
|
|
return safePrice * (1 + Math.max(0, profile.buyFeeRate));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function estimateSellNetUnit(
|
||
|
|
price: number,
|
||
|
|
profile: AutotradeExecutionCostProfile,
|
||
|
|
) {
|
||
|
|
const safePrice = Math.max(0, price);
|
||
|
|
if (safePrice <= 0) return 0;
|
||
|
|
const totalRate = Math.max(0, profile.sellFeeRate) + Math.max(0, profile.sellTaxRate);
|
||
|
|
return safePrice * (1 - totalRate);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function estimateOrderCost(params: {
|
||
|
|
side: "buy" | "sell";
|
||
|
|
price: number;
|
||
|
|
quantity: number;
|
||
|
|
profile: AutotradeExecutionCostProfile;
|
||
|
|
}): AutotradeEstimatedOrderCost {
|
||
|
|
const safePrice = Math.max(0, params.price);
|
||
|
|
const safeQuantity = Math.max(0, Math.floor(params.quantity));
|
||
|
|
const grossAmount = safePrice * safeQuantity;
|
||
|
|
|
||
|
|
if (params.side === "buy") {
|
||
|
|
const feeAmount = grossAmount * Math.max(0, params.profile.buyFeeRate);
|
||
|
|
return {
|
||
|
|
side: params.side,
|
||
|
|
price: safePrice,
|
||
|
|
quantity: safeQuantity,
|
||
|
|
grossAmount,
|
||
|
|
feeAmount,
|
||
|
|
taxAmount: 0,
|
||
|
|
netAmount: grossAmount + feeAmount,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const feeAmount = grossAmount * Math.max(0, params.profile.sellFeeRate);
|
||
|
|
const taxAmount = grossAmount * Math.max(0, params.profile.sellTaxRate);
|
||
|
|
return {
|
||
|
|
side: params.side,
|
||
|
|
price: safePrice,
|
||
|
|
quantity: safeQuantity,
|
||
|
|
grossAmount,
|
||
|
|
feeAmount,
|
||
|
|
taxAmount,
|
||
|
|
netAmount: grossAmount - feeAmount - taxAmount,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function readRateFromEnv(envName: string, fallback: number) {
|
||
|
|
const raw = Number.parseFloat(process.env[envName] ?? "");
|
||
|
|
if (!Number.isFinite(raw)) return fallback;
|
||
|
|
|
||
|
|
if (raw >= 1 && raw <= 100) {
|
||
|
|
return clamp(raw / 100, 0, 0.5);
|
||
|
|
}
|
||
|
|
|
||
|
|
return clamp(raw, 0, 0.5);
|
||
|
|
}
|
||
|
|
|
||
|
|
function clamp(value: number, min: number, max: number) {
|
||
|
|
if (!Number.isFinite(value)) return min;
|
||
|
|
return Math.min(max, Math.max(min, value));
|
||
|
|
}
|