mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
更新环境配置示例,添加 GRVT 相关的 API 凭证和选项,增强文档以支持新的交易所适配器,确保用户能够正确配置和使用 GRVT 交易功能。
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import type { ExchangeAdapter } from "./adapter";
|
||||
import { AsterExchangeAdapter, type AsterCredentials } from "./aster-adapter";
|
||||
import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
|
||||
|
||||
export interface ExchangeFactoryOptions {
|
||||
symbol: string;
|
||||
exchange?: string;
|
||||
aster?: AsterCredentials;
|
||||
grvt?: GrvtCredentials;
|
||||
}
|
||||
|
||||
export type SupportedExchangeId = "aster" | "grvt";
|
||||
|
||||
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
|
||||
.toString()
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (fallback === "grvt") return "grvt";
|
||||
return "aster";
|
||||
}
|
||||
|
||||
export function createExchangeAdapter(options: ExchangeFactoryOptions): ExchangeAdapter {
|
||||
const id = resolveExchangeId(options.exchange);
|
||||
if (id === "grvt") {
|
||||
return new GrvtExchangeAdapter({ ...options.grvt, symbol: options.symbol });
|
||||
}
|
||||
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import path from "path";
|
||||
import { createRequire } from "module";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
ExchangeAdapter,
|
||||
KlineListener,
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import {
|
||||
GrvtGateway,
|
||||
type GrvtEnvironment,
|
||||
type GrvtGatewayOptions,
|
||||
type GrvtHostsOverride,
|
||||
type GrvtSignatureProvider,
|
||||
} from "./gateway";
|
||||
|
||||
export interface GrvtCredentials {
|
||||
cookie?: string;
|
||||
accountId?: string;
|
||||
apiKey?: string;
|
||||
apiSecret?: string;
|
||||
subAccountId?: string;
|
||||
instrument?: string;
|
||||
symbol?: string;
|
||||
env?: GrvtEnvironment;
|
||||
hosts?: GrvtHostsOverride;
|
||||
signatureProvider?: GrvtSignatureProvider;
|
||||
pollIntervals?: GrvtGatewayOptions["pollIntervals"];
|
||||
logger?: GrvtGatewayOptions["logger"];
|
||||
}
|
||||
|
||||
export class GrvtExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id = "grvt";
|
||||
|
||||
private readonly gateway: GrvtGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly instrument: string;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
private klineInterval = "1m";
|
||||
|
||||
constructor(credentials: GrvtCredentials = {}) {
|
||||
const apiKey = credentials.apiKey ?? process.env.GRVT_API_KEY;
|
||||
const apiSecret = credentials.apiSecret ?? process.env.GRVT_API_SECRET;
|
||||
const cookie = credentials.cookie ?? process.env.GRVT_COOKIE;
|
||||
const accountId = credentials.accountId ?? process.env.GRVT_ACCOUNT_ID;
|
||||
if (!cookie || !accountId) {
|
||||
if (!apiKey) {
|
||||
throw new Error("Missing GRVT_API_KEY environment variable for authentication");
|
||||
}
|
||||
}
|
||||
|
||||
const subAccountId = requireValue(
|
||||
credentials.subAccountId ?? process.env.GRVT_SUB_ACCOUNT_ID,
|
||||
"GRVT_SUB_ACCOUNT_ID"
|
||||
);
|
||||
const instrument = requireValue(
|
||||
credentials.instrument ?? process.env.GRVT_INSTRUMENT,
|
||||
"GRVT_INSTRUMENT"
|
||||
);
|
||||
const symbol = normalizeSymbol(credentials.symbol ?? process.env.GRVT_SYMBOL, instrument);
|
||||
this.symbol = symbol;
|
||||
this.instrument = instrument;
|
||||
const signatureProvider =
|
||||
credentials.signatureProvider ?? loadSignatureProviderFromEnv(credentials.logger);
|
||||
if (!signatureProvider && !apiSecret) {
|
||||
throw new Error(
|
||||
"GRVT_API_SECRET is required when no external signature provider is configured"
|
||||
);
|
||||
}
|
||||
|
||||
this.gateway = new GrvtGateway({
|
||||
apiKey: apiKey ?? undefined,
|
||||
apiSecret: apiSecret ?? undefined,
|
||||
cookie: cookie ?? undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
subAccountId,
|
||||
instrument,
|
||||
symbol,
|
||||
env: (credentials.env ?? process.env.GRVT_ENV) as GrvtEnvironment | undefined,
|
||||
hosts: credentials.hosts,
|
||||
signatureProvider,
|
||||
pollIntervals: credentials.pollIntervals,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||
this.klineInterval = interval ?? this.klineInterval;
|
||||
void this.ensureInitialized("watchKlines", this.klineInterval);
|
||||
this.gateway.onKlines(this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders();
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[GrvtExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
||||
}
|
||||
}) as T;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private ensureInitialized(context?: string, interval?: string): Promise<void> {
|
||||
if (interval) {
|
||||
this.klineInterval = interval;
|
||||
}
|
||||
if (!this.initPromise) {
|
||||
this.initContexts.clear();
|
||||
this.initPromise = this.gateway
|
||||
.ensureInitialized(this.klineInterval)
|
||||
.then((value) => {
|
||||
this.clearRetry();
|
||||
return value;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.handleInitError("initialize", error);
|
||||
this.initPromise = null;
|
||||
this.scheduleRetry();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
if (context && !this.initContexts.has(context)) {
|
||||
this.initContexts.add(context);
|
||||
this.initPromise.catch((error) => {
|
||||
this.handleInitError(context, error);
|
||||
this.scheduleRetry();
|
||||
});
|
||||
}
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private scheduleRetry(): void {
|
||||
if (this.retryTimer) return;
|
||||
this.retryTimer = setTimeout(() => {
|
||||
this.retryTimer = null;
|
||||
if (this.initPromise) return;
|
||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
||||
void this.ensureInitialized("retry");
|
||||
}, this.retryDelayMs);
|
||||
}
|
||||
|
||||
private clearRetry(): void {
|
||||
if (this.retryTimer) {
|
||||
clearTimeout(this.retryTimer);
|
||||
this.retryTimer = null;
|
||||
}
|
||||
this.retryDelayMs = 3000;
|
||||
}
|
||||
|
||||
private handleInitError(context: string, error: unknown): void {
|
||||
const now = Date.now();
|
||||
if (now - this.lastInitErrorAt < 5000) return;
|
||||
this.lastInitErrorAt = now;
|
||||
console.error(`[GrvtExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
}
|
||||
|
||||
function requireValue<T>(value: T | undefined | null, key: string): T {
|
||||
if (value == null || value === "") {
|
||||
throw new Error(`Missing required environment variable ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeSymbol(symbol: string | undefined, instrument: string): string {
|
||||
if (symbol) return symbol.toUpperCase();
|
||||
return instrument.replace(/[_-]/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
function loadSignatureProviderFromEnv(
|
||||
logger?: GrvtGatewayOptions["logger"]
|
||||
): GrvtSignatureProvider | undefined {
|
||||
const signerModule = process.env.GRVT_SIGNER_PATH;
|
||||
if (!signerModule) return undefined;
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const resolved = signerModule.startsWith(".") || signerModule.startsWith("/")
|
||||
? path.resolve(process.cwd(), signerModule)
|
||||
: signerModule;
|
||||
const loaded = require(resolved);
|
||||
if (typeof loaded === "function") {
|
||||
return loaded as GrvtSignatureProvider;
|
||||
}
|
||||
if (loaded && typeof loaded.default === "function") {
|
||||
return loaded.default as GrvtSignatureProvider;
|
||||
}
|
||||
console.warn(
|
||||
`[GrvtExchangeAdapter] 模块 ${resolved} 未导出签名函数 (function default export)`
|
||||
);
|
||||
} catch (error) {
|
||||
const log = logger ?? ((ctx, err) => console.error(`[GrvtExchangeAdapter] ${ctx}`, err));
|
||||
log("loadSignatureProvider", error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { GrvtClient, GrvtWsClient, EGrvtEnvironment } from "@grvt/sdk";
|
||||
|
||||
export interface GrvtCredentials {
|
||||
apiKey: string;
|
||||
apiSecret: string;
|
||||
env: EGrvtEnvironment;
|
||||
subAccountId: string;
|
||||
instrument: string;
|
||||
}
|
||||
|
||||
export class GrvtGateway {
|
||||
private readonly grvtClient: GrvtClient;
|
||||
private readonly wsClient: GrvtWsClient;
|
||||
|
||||
constructor(private readonly credentials: GrvtCredentials) {
|
||||
this.grvtClient = new GrvtClient({
|
||||
apiKey: credentials.apiKey,
|
||||
apiSecret: credentials.apiSecret,
|
||||
env: credentials.env,
|
||||
});
|
||||
this.wsClient = new GrvtWsClient({ apiKey: credentials.apiKey, env: credentials.env });
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.wsClient.connect();
|
||||
}
|
||||
}
|
||||
|
||||
+1266
-176
File diff suppressed because it is too large
Load Diff
+46
-1
@@ -195,6 +195,51 @@ export interface GrvtKline {
|
||||
number_of_trades?: number;
|
||||
}
|
||||
|
||||
export interface GrvtSignature {
|
||||
signer: string;
|
||||
r: string;
|
||||
s: string;
|
||||
v: number;
|
||||
expiration: string;
|
||||
nonce: number;
|
||||
}
|
||||
|
||||
export interface GrvtUnsignedOrderLeg {
|
||||
instrument: string;
|
||||
size: string;
|
||||
limit_price?: string;
|
||||
is_buying_asset: boolean;
|
||||
}
|
||||
|
||||
export interface GrvtTriggerMetadata {
|
||||
trigger_type: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||
tpsl: {
|
||||
trigger_by: "UNSPECIFIED" | "INDEX" | "LAST" | "MID" | "MARK";
|
||||
trigger_price: string;
|
||||
close_position: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GrvtOrderMetadataInput {
|
||||
client_order_id: string;
|
||||
trigger?: GrvtTriggerMetadata;
|
||||
broker?: string | null;
|
||||
}
|
||||
|
||||
export interface GrvtUnsignedOrder {
|
||||
sub_account_id: string;
|
||||
is_market: boolean;
|
||||
time_in_force: string;
|
||||
post_only: boolean;
|
||||
reduce_only: boolean;
|
||||
legs: GrvtUnsignedOrderLeg[];
|
||||
metadata: GrvtOrderMetadataInput;
|
||||
}
|
||||
|
||||
export interface GrvtSignedOrder extends GrvtUnsignedOrder {
|
||||
signature: GrvtSignature;
|
||||
}
|
||||
|
||||
export interface AsterAccountAsset {
|
||||
asset: string;
|
||||
walletBalance: string;
|
||||
@@ -290,7 +335,7 @@ export interface AsterKline {
|
||||
}
|
||||
|
||||
export interface AsterOrder {
|
||||
orderId: number;
|
||||
orderId: number | string;
|
||||
clientOrderId: string;
|
||||
symbol: string;
|
||||
side: OrderSide;
|
||||
|
||||
+22
-12
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { makerConfig } from "../config";
|
||||
import { AsterExchangeAdapter } from "../exchanges/aster-adapter";
|
||||
import { createExchangeAdapter, resolveExchangeId } from "../exchanges/create-adapter";
|
||||
import { MakerEngine, type MakerEngineSnapshot } from "../core/maker-engine";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
import { formatNumber } from "../utils/format";
|
||||
@@ -28,18 +28,28 @@ export function MakerApp({ onExit }: MakerAppProps) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const apiKey = process.env.ASTER_API_KEY;
|
||||
const apiSecret = process.env.ASTER_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const adapter = new AsterExchangeAdapter({
|
||||
apiKey,
|
||||
apiSecret,
|
||||
symbol: makerConfig.symbol,
|
||||
});
|
||||
const exchangeId = resolveExchangeId();
|
||||
let adapter;
|
||||
if (exchangeId === "aster") {
|
||||
const apiKey = process.env.ASTER_API_KEY;
|
||||
const apiSecret = process.env.ASTER_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量"));
|
||||
return;
|
||||
}
|
||||
adapter = createExchangeAdapter({
|
||||
exchange: exchangeId,
|
||||
symbol: makerConfig.symbol,
|
||||
aster: { apiKey, apiSecret },
|
||||
});
|
||||
} else {
|
||||
adapter = createExchangeAdapter({
|
||||
exchange: exchangeId,
|
||||
symbol: makerConfig.symbol,
|
||||
grvt: { symbol: makerConfig.symbol },
|
||||
});
|
||||
}
|
||||
const engine = new MakerEngine(makerConfig, adapter);
|
||||
engineRef.current = engine;
|
||||
setSnapshot(engine.getSnapshot());
|
||||
|
||||
+22
-13
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { makerConfig } from "../config";
|
||||
import { AsterExchangeAdapter } from "../exchanges/aster-adapter";
|
||||
import { createExchangeAdapter, resolveExchangeId } from "../exchanges/create-adapter";
|
||||
import { OffsetMakerEngine, type OffsetMakerEngineSnapshot } from "../core/offset-maker-engine";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
import { formatNumber } from "../utils/format";
|
||||
@@ -28,18 +28,28 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const apiKey = process.env.ASTER_API_KEY;
|
||||
const apiSecret = process.env.ASTER_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const adapter = new AsterExchangeAdapter({
|
||||
apiKey,
|
||||
apiSecret,
|
||||
symbol: makerConfig.symbol,
|
||||
});
|
||||
const exchangeId = resolveExchangeId();
|
||||
let adapter;
|
||||
if (exchangeId === "aster") {
|
||||
const apiKey = process.env.ASTER_API_KEY;
|
||||
const apiSecret = process.env.ASTER_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量"));
|
||||
return;
|
||||
}
|
||||
adapter = createExchangeAdapter({
|
||||
exchange: exchangeId,
|
||||
symbol: makerConfig.symbol,
|
||||
aster: { apiKey, apiSecret },
|
||||
});
|
||||
} else {
|
||||
adapter = createExchangeAdapter({
|
||||
exchange: exchangeId,
|
||||
symbol: makerConfig.symbol,
|
||||
grvt: { symbol: makerConfig.symbol },
|
||||
});
|
||||
}
|
||||
const engine = new OffsetMakerEngine(makerConfig, adapter);
|
||||
engineRef.current = engine;
|
||||
setSnapshot(engine.getSnapshot());
|
||||
@@ -192,4 +202,3 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+22
-12
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { tradingConfig } from "../config";
|
||||
import { AsterExchangeAdapter } from "../exchanges/aster-adapter";
|
||||
import { createExchangeAdapter, resolveExchangeId } from "../exchanges/create-adapter";
|
||||
import { TrendEngine, type TrendEngineSnapshot } from "../core/trend-engine";
|
||||
import { formatNumber } from "../utils/format";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
@@ -30,18 +30,28 @@ export function TrendApp({ onExit }: TrendAppProps) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const apiKey = process.env.ASTER_API_KEY;
|
||||
const apiSecret = process.env.ASTER_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const adapter = new AsterExchangeAdapter({
|
||||
apiKey,
|
||||
apiSecret,
|
||||
symbol: tradingConfig.symbol,
|
||||
});
|
||||
const exchangeId = resolveExchangeId();
|
||||
let adapter;
|
||||
if (exchangeId === "aster") {
|
||||
const apiKey = process.env.ASTER_API_KEY;
|
||||
const apiSecret = process.env.ASTER_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量"));
|
||||
return;
|
||||
}
|
||||
adapter = createExchangeAdapter({
|
||||
exchange: exchangeId,
|
||||
symbol: tradingConfig.symbol,
|
||||
aster: { apiKey, apiSecret },
|
||||
});
|
||||
} else {
|
||||
adapter = createExchangeAdapter({
|
||||
exchange: exchangeId,
|
||||
symbol: tradingConfig.symbol,
|
||||
grvt: { symbol: tradingConfig.symbol },
|
||||
});
|
||||
}
|
||||
const engine = new TrendEngine(tradingConfig, adapter);
|
||||
engineRef.current = engine;
|
||||
setSnapshot(engine.getSnapshot());
|
||||
|
||||
Reference in New Issue
Block a user