mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 16:28:06 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37476ea4bc | ||
|
|
ffd62fd481 | ||
|
|
511cf8669d | ||
|
|
968b585688 |
+16
-45
@@ -116,25 +116,17 @@ export interface BasisArbConfig {
|
|||||||
arbAmount: number; // base asset amount to arb (e.g., ASTER amount when ASTERUSDT)
|
arbAmount: number; // base asset amount to arb (e.g., ASTER amount when ASTERUSDT)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GridDirection = "both" | "long" | "short";
|
|
||||||
|
|
||||||
export interface GridConfig {
|
export interface GridConfig {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
lowerPrice: number;
|
tradeAmount: number;
|
||||||
upperPrice: number;
|
|
||||||
gridLevels: number;
|
|
||||||
orderSize: number;
|
|
||||||
maxPositionSize: number;
|
|
||||||
refreshIntervalMs: number;
|
refreshIntervalMs: number;
|
||||||
maxLogEntries: number;
|
|
||||||
priceTick: number;
|
priceTick: number;
|
||||||
qtyStep: number;
|
qtyStep: number;
|
||||||
direction: GridDirection;
|
levelsPerSide: number;
|
||||||
stopLossPct: number;
|
spacingPct: number;
|
||||||
restartTriggerPct: number;
|
stopLossBufferPct: number;
|
||||||
autoRestart: boolean;
|
maxLogEntries: number;
|
||||||
gridMode: "geometric";
|
maxPositionSize: number;
|
||||||
maxCloseSlippagePct: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveBasisSymbol = (envKeys: string[], fallback: string): string => {
|
const resolveBasisSymbol = (envKeys: string[], fallback: string): string => {
|
||||||
@@ -162,17 +154,8 @@ export const basisConfig: BasisArbConfig = {
|
|||||||
arbAmount: parseNumber(process.env.ARB_AMOUNT, parseNumber(process.env.TRADE_AMOUNT, 0)),
|
arbAmount: parseNumber(process.env.ARB_AMOUNT, parseNumber(process.env.TRADE_AMOUNT, 0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveGridDirection = (raw: string | undefined, fallback: GridDirection): GridDirection => {
|
const resolveGridMaxPosition = (tradeAmount: number): number => {
|
||||||
if (!raw) return fallback;
|
const fallback = Math.max(tradeAmount * 10, tradeAmount);
|
||||||
const normalized = raw.trim().toLowerCase();
|
|
||||||
if (normalized === "long" || normalized === "long-only") return "long";
|
|
||||||
if (normalized === "short" || normalized === "short-only") return "short";
|
|
||||||
if (normalized === "both" || normalized === "dual" || normalized === "bi" || normalized === "two-way") return "both";
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveGridMaxPosition = (orderSize: number, levels: number): number => {
|
|
||||||
const fallback = Math.max(orderSize * Math.max(levels - 1, 1), orderSize);
|
|
||||||
const raw = process.env.GRID_MAX_POSITION_SIZE ?? process.env.GRID_MAX_POSITION ?? process.env.GRID_POSITION_CAP;
|
const raw = process.env.GRID_MAX_POSITION_SIZE ?? process.env.GRID_MAX_POSITION ?? process.env.GRID_POSITION_CAP;
|
||||||
const parsed = parseNumber(raw, fallback);
|
const parsed = parseNumber(raw, fallback);
|
||||||
return parsed > 0 ? parsed : fallback;
|
return parsed > 0 ? parsed : fallback;
|
||||||
@@ -180,30 +163,18 @@ const resolveGridMaxPosition = (orderSize: number, levels: number): number => {
|
|||||||
|
|
||||||
export const gridConfig: GridConfig = {
|
export const gridConfig: GridConfig = {
|
||||||
symbol: resolveSymbolFromEnv(),
|
symbol: resolveSymbolFromEnv(),
|
||||||
lowerPrice: parseNumber(process.env.GRID_LOWER_PRICE ?? process.env.GRID_LOWER_BOUND, 0),
|
tradeAmount: parseNumber(process.env.TRADE_AMOUNT, 0.001),
|
||||||
upperPrice: parseNumber(process.env.GRID_UPPER_PRICE ?? process.env.GRID_UPPER_BOUND, 0),
|
refreshIntervalMs: parseNumber(process.env.GRID_REFRESH_INTERVAL_MS, 800),
|
||||||
gridLevels: Math.max(2, Math.floor(parseNumber(process.env.GRID_LEVELS, 10))),
|
|
||||||
orderSize: parseNumber(process.env.GRID_ORDER_SIZE, parseNumber(process.env.TRADE_AMOUNT, 0.001)),
|
|
||||||
maxPositionSize: 0, // placeholder, replaced below
|
|
||||||
refreshIntervalMs: parseNumber(process.env.GRID_REFRESH_INTERVAL_MS, 1_000),
|
|
||||||
maxLogEntries: parseNumber(process.env.GRID_MAX_LOG_ENTRIES, 200),
|
|
||||||
priceTick: parseNumber(process.env.GRID_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
|
priceTick: parseNumber(process.env.GRID_PRICE_TICK ?? process.env.PRICE_TICK, 0.1),
|
||||||
qtyStep: parseNumber(process.env.GRID_QTY_STEP ?? process.env.QTY_STEP, 0.001),
|
qtyStep: parseNumber(process.env.GRID_QTY_STEP ?? process.env.QTY_STEP, 0.001),
|
||||||
direction: resolveGridDirection(process.env.GRID_DIRECTION, "both"),
|
levelsPerSide: Math.max(5, Math.floor(parseNumber(process.env.GRID_LEVELS_PER_SIDE, 15))),
|
||||||
stopLossPct: Math.max(0, parseNumber(process.env.GRID_STOP_LOSS_PCT, 0.01)),
|
spacingPct: Math.max(0.0001, parseNumber(process.env.GRID_SPACING_PCT, 0.00025)),
|
||||||
restartTriggerPct: Math.max(0, parseNumber(process.env.GRID_RESTART_TRIGGER_PCT, 0.01)),
|
stopLossBufferPct: Math.max(0.001, parseNumber(process.env.GRID_STOP_BUFFER_PCT, 0.003)),
|
||||||
autoRestart: parseBoolean(process.env.GRID_AUTO_RESTART_ENABLED ?? process.env.GRID_ENABLE_AUTO_RESTART, true),
|
maxLogEntries: parseNumber(process.env.GRID_MAX_LOG_ENTRIES, 200),
|
||||||
gridMode: "geometric",
|
maxPositionSize: 0, // placeholder updated below
|
||||||
maxCloseSlippagePct: Math.max(
|
|
||||||
0,
|
|
||||||
parseNumber(
|
|
||||||
process.env.GRID_MAX_CLOSE_SLIPPAGE_PCT ?? process.env.MAX_CLOSE_SLIPPAGE_PCT,
|
|
||||||
0.05
|
|
||||||
)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.orderSize, gridConfig.gridLevels);
|
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.tradeAmount);
|
||||||
|
|
||||||
export function isBasisStrategyEnabled(): boolean {
|
export function isBasisStrategyEnabled(): boolean {
|
||||||
const raw = process.env.ENABLE_BASIS_STRATEGY;
|
const raw = process.env.ENABLE_BASIS_STRATEGY;
|
||||||
|
|||||||
@@ -1201,6 +1201,7 @@ function toApiOrderPayload(order: GrvtSignedOrder): IOrder {
|
|||||||
const metadata = order.metadata ? toApiOrderMetadata(order.metadata) : undefined;
|
const metadata = order.metadata ? toApiOrderMetadata(order.metadata) : undefined;
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
|
client_order_id: order.metadata?.client_order_id,
|
||||||
time_in_force: toApiTimeInForce(order.time_in_force),
|
time_in_force: toApiTimeInForce(order.time_in_force),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
@@ -1300,7 +1301,7 @@ function buildUnsignedOrder(params: {
|
|||||||
|
|
||||||
const trigger = buildTriggerMetadata(orderParams);
|
const trigger = buildTriggerMetadata(orderParams);
|
||||||
const metadata = {
|
const metadata = {
|
||||||
client_order_id: generateClientOrderId(),
|
client_order_id: orderParams.clientOrderId ?? generateClientOrderId(),
|
||||||
...(trigger ? { trigger } : {}),
|
...(trigger ? { trigger } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent):
|
|||||||
if (intent.closePosition !== undefined) {
|
if (intent.closePosition !== undefined) {
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
params.closePosition = toStringBoolean(intent.closePosition);
|
||||||
}
|
}
|
||||||
|
if (intent.clientOrderId) {
|
||||||
|
params.clientOrderId = intent.clientOrderId;
|
||||||
|
}
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterO
|
|||||||
quantity: intent.quantity,
|
quantity: intent.quantity,
|
||||||
price: intent.price,
|
price: intent.price,
|
||||||
timeInForce: intent.timeInForce ?? "GTX",
|
timeInForce: intent.timeInForce ?? "GTX",
|
||||||
|
clientOrderId: intent.clientOrderId,
|
||||||
},
|
},
|
||||||
intent
|
intent
|
||||||
);
|
);
|
||||||
@@ -89,4 +93,3 @@ export async function createClosePositionOrder(intent: ClosePositionIntent): Pro
|
|||||||
);
|
);
|
||||||
return intent.adapter.createOrder(params);
|
return intent.adapter.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface BaseOrderIntent {
|
|||||||
reduceOnly?: boolean;
|
reduceOnly?: boolean;
|
||||||
closePosition?: boolean;
|
closePosition?: boolean;
|
||||||
timeInForce?: TimeInForce | "GTX";
|
timeInForce?: TimeInForce | "GTX";
|
||||||
|
clientOrderId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LimitOrderIntent extends BaseOrderIntent {
|
export interface LimitOrderIntent extends BaseOrderIntent {
|
||||||
@@ -39,4 +40,3 @@ export function toStringBoolean(value: boolean | undefined): "true" | "false" |
|
|||||||
if (value === undefined) return undefined;
|
if (value === undefined) return undefined;
|
||||||
return value ? "true" : "false";
|
return value ? "true" : "false";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -599,8 +599,8 @@ export class ParadexGateway {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const isClosePosition = (extraParams as any).closePosition === true;
|
const isClosePosition = (extraParams as any).closePosition === true;
|
||||||
// Only omit amount for MARKET close-position orders; STOP requires explicit size
|
// Paradex requires explicit size for close-position MARKET orders; always send amount
|
||||||
const shouldOmitAmount = isClosePosition && type === "market";
|
const shouldOmitAmount = false;
|
||||||
const amountArg: any = shouldOmitAmount ? undefined : amount;
|
const amountArg: any = shouldOmitAmount ? undefined : amount;
|
||||||
if (!shouldOmitAmount && amountArg != null && extraParams.size === undefined) {
|
if (!shouldOmitAmount && amountArg != null && extraParams.size === undefined) {
|
||||||
extraParams.size = amountArg.toString();
|
extraParams.size = amountArg.toString();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface CreateOrderParams {
|
|||||||
reduceOnly?: StringBoolean;
|
reduceOnly?: StringBoolean;
|
||||||
closePosition?: StringBoolean;
|
closePosition?: StringBoolean;
|
||||||
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||||
|
clientOrderId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterAccountPosition {
|
export interface AsterAccountPosition {
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { promises as fs } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import type { GridDirection } from "../../config";
|
|
||||||
|
|
||||||
const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
|
|
||||||
const GRID_FILE = path.resolve(DATA_DIR, "grid-record.json");
|
|
||||||
|
|
||||||
export interface StoredGridState {
|
|
||||||
symbol: string;
|
|
||||||
lowerPrice: number;
|
|
||||||
upperPrice: number;
|
|
||||||
gridLevels: number;
|
|
||||||
orderSize: number;
|
|
||||||
maxPositionSize: number;
|
|
||||||
direction: GridDirection;
|
|
||||||
longExposure: Record<string, number>;
|
|
||||||
shortExposure: Record<string, number>;
|
|
||||||
updatedAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type GridStateMap = Record<string, StoredGridState>;
|
|
||||||
|
|
||||||
async function ensureDataDir(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readStateFile(): Promise<GridStateMap> {
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(GRID_FILE, "utf8");
|
|
||||||
const parsed = JSON.parse(content);
|
|
||||||
if (parsed && typeof parsed === "object") {
|
|
||||||
return parsed as GridStateMap;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadGridState(symbol: string): Promise<StoredGridState | null> {
|
|
||||||
const map = await readStateFile();
|
|
||||||
const snapshot = map[symbol];
|
|
||||||
return snapshot ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveGridState(snapshot: StoredGridState): Promise<void> {
|
|
||||||
await ensureDataDir();
|
|
||||||
const map = await readStateFile();
|
|
||||||
map[snapshot.symbol] = snapshot;
|
|
||||||
await fs.writeFile(GRID_FILE, JSON.stringify(map, null, 2), "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clearGridState(symbol: string): Promise<void> {
|
|
||||||
const map = await readStateFile();
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(map, symbol)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete map[symbol];
|
|
||||||
const entries = Object.keys(map);
|
|
||||||
if (!entries.length) {
|
|
||||||
try {
|
|
||||||
await fs.unlink(GRID_FILE);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (!error || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ensureDataDir();
|
|
||||||
await fs.writeFile(GRID_FILE, JSON.stringify(map, null, 2), "utf8");
|
|
||||||
}
|
|
||||||
+484
-1106
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -118,10 +118,10 @@ export function GridApp({ onExit }: GridAppProps) {
|
|||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text color="cyanBright">Grid Strategy Dashboard</Text>
|
<Text color="cyanBright">Grid Strategy Dashboard</Text>
|
||||||
<Text>
|
<Text>
|
||||||
交易所: {exchangeName} | 交易对: {snapshot.symbol} | 状态: {snapshot.running ? "运行中" : "暂停"} | 方向: {snapshot.direction}
|
交易所: {exchangeName} | 交易对: {snapshot.symbol} | 状态: {snapshot.running ? "运行中" : "暂停"}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
实时价格: {formatNumber(snapshot.lastPrice, 4)} | 下界: {formatNumber(snapshot.lowerPrice, 4)} | 上界: {formatNumber(snapshot.upperPrice, 4)} | 网格数量: {snapshot.gridLines.length}
|
实时价格: {formatNumber(snapshot.lastPrice, 4)} | 中心价: {formatNumber(snapshot.centerPrice, 4)} | 下界: {formatNumber(snapshot.lowerPrice, 4)} | 上界: {formatNumber(snapshot.upperPrice, 4)} | 网格数量: {snapshot.gridLines.length}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color="gray">数据状态:
|
<Text color="gray">数据状态:
|
||||||
{feedEntries.map((entry, index) => (
|
{feedEntries.map((entry, index) => (
|
||||||
@@ -139,10 +139,10 @@ export function GridApp({ onExit }: GridAppProps) {
|
|||||||
<Box flexDirection="column" marginRight={4}>
|
<Box flexDirection="column" marginRight={4}>
|
||||||
<Text color="greenBright">网格配置</Text>
|
<Text color="greenBright">网格配置</Text>
|
||||||
<Text>
|
<Text>
|
||||||
单笔数量: {formatNumber(gridConfig.orderSize, 6)} | 最大仓位: {formatNumber(gridConfig.maxPositionSize, 6)}
|
单笔数量: {formatNumber(gridConfig.tradeAmount, 6)} | 每侧格子数: {gridConfig.levelsPerSide} | 最大仓位: {formatNumber(gridConfig.maxPositionSize, 6)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
止损阈值: {(gridConfig.stopLossPct * 100).toFixed(2)}% | 重启阈值: {(gridConfig.restartTriggerPct * 100).toFixed(2)}% | 自动重启: {gridConfig.autoRestart ? "启用" : "关闭"}
|
网格步长: {(gridConfig.spacingPct * 100).toFixed(3)}% | 止损缓冲: {(gridConfig.stopLossBufferPct * 100).toFixed(3)}%
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
刷新间隔: {gridConfig.refreshIntervalMs} ms
|
刷新间隔: {gridConfig.refreshIntervalMs} ms
|
||||||
|
|||||||
Reference in New Issue
Block a user