9 changed files with 514 additions and 1239 deletions
+16 -45
View File
@@ -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;
+2 -1
View File
@@ -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 } : {}),
}; };
+4 -1
View File
@@ -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);
} }
+1 -1
View File
@@ -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";
} }
+2 -2
View File
@@ -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();
+1
View File
@@ -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 {
-79
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -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