更新环境配置示例,添加 GRVT 相关的 API 凭证和选项,增强文档以支持新的交易所适配器,确保用户能够正确配置和使用 GRVT 交易功能。

This commit is contained in:
discountry
2025-09-27 16:12:20 +08:00
parent 64640f4494
commit dde7ab71a9
60 changed files with 16126 additions and 527 deletions
+29
View File
@@ -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 });
}
+245
View File
@@ -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;
}
-28
View File
@@ -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();
}
}
File diff suppressed because it is too large Load Diff
+46 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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());