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)
}
export type GridDirection = "both" | "long" | "short";
export interface GridConfig {
symbol: string;
lowerPrice: number;
upperPrice: number;
gridLevels: number;
orderSize: number;
maxPositionSize: number;
tradeAmount: number;
refreshIntervalMs: number;
maxLogEntries: number;
priceTick: number;
qtyStep: number;
direction: GridDirection;
stopLossPct: number;
restartTriggerPct: number;
autoRestart: boolean;
gridMode: "geometric";
maxCloseSlippagePct: number;
levelsPerSide: number;
spacingPct: number;
stopLossBufferPct: number;
maxLogEntries: number;
maxPositionSize: number;
}
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)),
};
const resolveGridDirection = (raw: string | undefined, fallback: GridDirection): GridDirection => {
if (!raw) return fallback;
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 resolveGridMaxPosition = (tradeAmount: number): number => {
const fallback = Math.max(tradeAmount * 10, tradeAmount);
const raw = process.env.GRID_MAX_POSITION_SIZE ?? process.env.GRID_MAX_POSITION ?? process.env.GRID_POSITION_CAP;
const parsed = parseNumber(raw, fallback);
return parsed > 0 ? parsed : fallback;
@@ -180,30 +163,18 @@ const resolveGridMaxPosition = (orderSize: number, levels: number): number => {
export const gridConfig: GridConfig = {
symbol: resolveSymbolFromEnv(),
lowerPrice: parseNumber(process.env.GRID_LOWER_PRICE ?? process.env.GRID_LOWER_BOUND, 0),
upperPrice: parseNumber(process.env.GRID_UPPER_PRICE ?? process.env.GRID_UPPER_BOUND, 0),
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),
tradeAmount: parseNumber(process.env.TRADE_AMOUNT, 0.001),
refreshIntervalMs: parseNumber(process.env.GRID_REFRESH_INTERVAL_MS, 800),
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),
direction: resolveGridDirection(process.env.GRID_DIRECTION, "both"),
stopLossPct: Math.max(0, parseNumber(process.env.GRID_STOP_LOSS_PCT, 0.01)),
restartTriggerPct: Math.max(0, parseNumber(process.env.GRID_RESTART_TRIGGER_PCT, 0.01)),
autoRestart: parseBoolean(process.env.GRID_AUTO_RESTART_ENABLED ?? process.env.GRID_ENABLE_AUTO_RESTART, true),
gridMode: "geometric",
maxCloseSlippagePct: Math.max(
0,
parseNumber(
process.env.GRID_MAX_CLOSE_SLIPPAGE_PCT ?? process.env.MAX_CLOSE_SLIPPAGE_PCT,
0.05
)
),
levelsPerSide: Math.max(5, Math.floor(parseNumber(process.env.GRID_LEVELS_PER_SIDE, 15))),
spacingPct: Math.max(0.0001, parseNumber(process.env.GRID_SPACING_PCT, 0.00025)),
stopLossBufferPct: Math.max(0.001, parseNumber(process.env.GRID_STOP_BUFFER_PCT, 0.003)),
maxLogEntries: parseNumber(process.env.GRID_MAX_LOG_ENTRIES, 200),
maxPositionSize: 0, // placeholder updated below
};
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.orderSize, gridConfig.gridLevels);
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.tradeAmount);
export function isBasisStrategyEnabled(): boolean {
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;
return {
...order,
client_order_id: order.metadata?.client_order_id,
time_in_force: toApiTimeInForce(order.time_in_force),
metadata,
};
@@ -1300,7 +1301,7 @@ function buildUnsignedOrder(params: {
const trigger = buildTriggerMetadata(orderParams);
const metadata = {
client_order_id: generateClientOrderId(),
client_order_id: orderParams.clientOrderId ?? generateClientOrderId(),
...(trigger ? { trigger } : {}),
};
+4 -1
View File
@@ -22,6 +22,9 @@ function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent):
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
if (intent.clientOrderId) {
params.clientOrderId = intent.clientOrderId;
}
return params;
}
@@ -34,6 +37,7 @@ export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterO
quantity: intent.quantity,
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
clientOrderId: intent.clientOrderId,
},
intent
);
@@ -89,4 +93,3 @@ export async function createClosePositionOrder(intent: ClosePositionIntent): Pro
);
return intent.adapter.createOrder(params);
}
+1 -1
View File
@@ -9,6 +9,7 @@ export interface BaseOrderIntent {
reduceOnly?: boolean;
closePosition?: boolean;
timeInForce?: TimeInForce | "GTX";
clientOrderId?: string;
}
export interface LimitOrderIntent extends BaseOrderIntent {
@@ -39,4 +40,3 @@ export function toStringBoolean(value: boolean | undefined): "true" | "false" |
if (value === undefined) return undefined;
return value ? "true" : "false";
}
+2 -2
View File
@@ -599,8 +599,8 @@ export class ParadexGateway {
try {
const isClosePosition = (extraParams as any).closePosition === true;
// Only omit amount for MARKET close-position orders; STOP requires explicit size
const shouldOmitAmount = isClosePosition && type === "market";
// Paradex requires explicit size for close-position MARKET orders; always send amount
const shouldOmitAmount = false;
const amountArg: any = shouldOmitAmount ? undefined : amount;
if (!shouldOmitAmount && amountArg != null && extraParams.size === undefined) {
extraParams.size = amountArg.toString();
+1
View File
@@ -25,6 +25,7 @@ export interface CreateOrderParams {
reduceOnly?: StringBoolean;
closePosition?: StringBoolean;
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
clientOrderId?: string;
}
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}>
<Text color="cyanBright">Grid Strategy Dashboard</Text>
<Text>
: {exchangeName} : {snapshot.symbol} : {snapshot.running ? "运行中" : "暂停"} : {snapshot.direction}
: {exchangeName} : {snapshot.symbol} : {snapshot.running ? "运行中" : "暂停"}
</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 color="gray">:
{feedEntries.map((entry, index) => (
@@ -139,10 +139,10 @@ export function GridApp({ onExit }: GridAppProps) {
<Box flexDirection="column" marginRight={4}>
<Text color="greenBright"></Text>
<Text>
: {formatNumber(gridConfig.orderSize, 6)} : {formatNumber(gridConfig.maxPositionSize, 6)}
: {formatNumber(gridConfig.tradeAmount, 6)} : {gridConfig.levelsPerSide} : {formatNumber(gridConfig.maxPositionSize, 6)}
</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>
: {gridConfig.refreshIntervalMs} ms