diff --git a/.gitignore b/.gitignore index 1f3eeb12..db7745d5 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/web/src/lib/api/backtest.ts b/web/src/lib/api/backtest.ts new file mode 100644 index 00000000..73279f31 --- /dev/null +++ b/web/src/lib/api/backtest.ts @@ -0,0 +1,197 @@ +import type { + DecisionRecord, + BacktestRunsResponse, + BacktestStartConfig, + BacktestStatusPayload, + BacktestEquityPoint, + BacktestTradeEvent, + BacktestMetrics, + BacktestRunMetadata, + BacktestKlinesResponse, +} from '../../types' +import { API_BASE, getAuthHeaders, handleJSONResponse } from './helpers' + +export const backtestApi = { + async getBacktestRuns(params?: { + state?: string + search?: string + limit?: number + offset?: number + }): Promise { + const query = new URLSearchParams() + if (params?.state) query.set('state', params.state) + if (params?.search) query.set('search', params.search) + if (params?.limit) query.set('limit', String(params.limit)) + if (params?.offset) query.set('offset', String(params.offset)) + const res = await fetch( + `${API_BASE}/backtest/runs${query.toString() ? `?${query}` : ''}`, + { + headers: getAuthHeaders(), + } + ) + return handleJSONResponse(res) + }, + + async startBacktest(config: BacktestStartConfig): Promise { + const res = await fetch(`${API_BASE}/backtest/start`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ config }), + }) + return handleJSONResponse(res) + }, + + async pauseBacktest(runId: string): Promise { + const res = await fetch(`${API_BASE}/backtest/pause`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ run_id: runId }), + }) + return handleJSONResponse(res) + }, + + async resumeBacktest(runId: string): Promise { + const res = await fetch(`${API_BASE}/backtest/resume`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ run_id: runId }), + }) + return handleJSONResponse(res) + }, + + async stopBacktest(runId: string): Promise { + const res = await fetch(`${API_BASE}/backtest/stop`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ run_id: runId }), + }) + return handleJSONResponse(res) + }, + + async updateBacktestLabel( + runId: string, + label: string + ): Promise { + const res = await fetch(`${API_BASE}/backtest/label`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ run_id: runId, label }), + }) + return handleJSONResponse(res) + }, + + async deleteBacktestRun(runId: string): Promise { + const res = await fetch(`${API_BASE}/backtest/delete`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ run_id: runId }), + }) + if (!res.ok) { + throw new Error(await res.text()) + } + }, + + async getBacktestStatus(runId: string): Promise { + const res = await fetch(`${API_BASE}/backtest/status?run_id=${runId}`, { + headers: getAuthHeaders(), + }) + return handleJSONResponse(res) + }, + + async getBacktestEquity( + runId: string, + timeframe?: string, + limit?: number + ): Promise { + const query = new URLSearchParams({ run_id: runId }) + if (timeframe) query.set('tf', timeframe) + if (limit) query.set('limit', String(limit)) + const res = await fetch(`${API_BASE}/backtest/equity?${query}`, { + headers: getAuthHeaders(), + }) + return handleJSONResponse(res) + }, + + async getBacktestTrades( + runId: string, + limit = 200 + ): Promise { + const query = new URLSearchParams({ + run_id: runId, + limit: String(limit), + }) + const res = await fetch(`${API_BASE}/backtest/trades?${query}`, { + headers: getAuthHeaders(), + }) + return handleJSONResponse(res) + }, + + async getBacktestMetrics(runId: string): Promise { + const res = await fetch(`${API_BASE}/backtest/metrics?run_id=${runId}`, { + headers: getAuthHeaders(), + }) + return handleJSONResponse(res) + }, + + async getBacktestKlines( + runId: string, + symbol: string, + timeframe?: string + ): Promise { + const query = new URLSearchParams({ run_id: runId, symbol }) + if (timeframe) query.set('timeframe', timeframe) + const res = await fetch(`${API_BASE}/backtest/klines?${query}`, { + headers: getAuthHeaders(), + }) + return handleJSONResponse(res) + }, + + async getBacktestTrace( + runId: string, + cycle?: number + ): Promise { + const query = new URLSearchParams({ run_id: runId }) + if (cycle) query.set('cycle', String(cycle)) + const res = await fetch(`${API_BASE}/backtest/trace?${query}`, { + headers: getAuthHeaders(), + }) + return handleJSONResponse(res) + }, + + async getBacktestDecisions( + runId: string, + limit = 20, + offset = 0 + ): Promise { + const query = new URLSearchParams({ + run_id: runId, + limit: String(limit), + offset: String(offset), + }) + const res = await fetch(`${API_BASE}/backtest/decisions?${query}`, { + headers: getAuthHeaders(), + }) + return handleJSONResponse(res) + }, + + async exportBacktest(runId: string): Promise { + const res = await fetch(`${API_BASE}/backtest/export?run_id=${runId}`, { + headers: getAuthHeaders(), + }) + if (!res.ok) { + const text = await res.text() + try { + const data = text ? JSON.parse(text) : null + throw new Error( + data?.error || data?.message || text || '导出失败,请稍后再试' + ) + } catch (err) { + if (err instanceof Error && err.message) { + throw err + } + throw new Error(text || '导出失败,请稍后再试') + } + } + return res.blob() + }, +} diff --git a/web/src/lib/api/config.ts b/web/src/lib/api/config.ts new file mode 100644 index 00000000..7ec1088b --- /dev/null +++ b/web/src/lib/api/config.ts @@ -0,0 +1,186 @@ +import type { + AIModel, + Exchange, + UpdateModelConfigRequest, + UpdateExchangeConfigRequest, + CreateExchangeRequest, +} from '../../types' +import { API_BASE, httpClient, CryptoService } from './helpers' + +export const configApi = { + async getModelConfigs(): Promise { + const result = await httpClient.get(`${API_BASE}/models`) + if (!result.success) throw new Error('获取模型配置失败') + return Array.isArray(result.data) ? result.data : [] + }, + + async getSupportedModels(): Promise { + const result = await httpClient.get( + `${API_BASE}/supported-models` + ) + if (!result.success) throw new Error('获取支持的模型失败') + return result.data! + }, + + async getPromptTemplates(): Promise { + const res = await fetch(`${API_BASE}/prompt-templates`) + if (!res.ok) throw new Error('获取提示词模板失败') + const data = await res.json() + if (Array.isArray(data.templates)) { + return data.templates.map((item: { name: string }) => item.name) + } + return [] + }, + + async updateModelConfigs(request: UpdateModelConfigRequest): Promise { + // 检查是否启用了传输加密 + const config = await CryptoService.fetchCryptoConfig() + + if (!config.transport_encryption) { + // 传输加密禁用时,直接发送明文 + const result = await httpClient.put(`${API_BASE}/models`, request) + if (!result.success) throw new Error('更新模型配置失败') + return + } + + // 获取RSA公钥 + const publicKey = await CryptoService.fetchPublicKey() + + // 初始化加密服务 + await CryptoService.initialize(publicKey) + + // 获取用户信息(从localStorage或其他地方) + const userId = localStorage.getItem('user_id') || '' + const sessionId = sessionStorage.getItem('session_id') || '' + + // 加密敏感数据 + const encryptedPayload = await CryptoService.encryptSensitiveData( + JSON.stringify(request), + userId, + sessionId + ) + + // 发送加密数据 + const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload) + if (!result.success) throw new Error('更新模型配置失败') + }, + + async getExchangeConfigs(): Promise { + const result = await httpClient.get(`${API_BASE}/exchanges`) + if (!result.success) throw new Error('获取交易所配置失败') + return result.data! + }, + + async getSupportedExchanges(): Promise { + const result = await httpClient.get( + `${API_BASE}/supported-exchanges` + ) + if (!result.success) throw new Error('获取支持的交易所失败') + return result.data! + }, + + async updateExchangeConfigs( + request: UpdateExchangeConfigRequest + ): Promise { + const result = await httpClient.put(`${API_BASE}/exchanges`, request) + if (!result.success) throw new Error('更新交易所配置失败') + }, + + async createExchange(request: CreateExchangeRequest): Promise<{ id: string }> { + const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request) + if (!result.success) throw new Error('创建交易所账户失败') + return result.data! + }, + + async createExchangeEncrypted(request: CreateExchangeRequest): Promise<{ id: string }> { + // 检查是否启用了传输加密 + const config = await CryptoService.fetchCryptoConfig() + + if (!config.transport_encryption) { + // 传输加密禁用时,直接发送明文 + const result = await httpClient.post<{ id: string }>(`${API_BASE}/exchanges`, request) + if (!result.success) throw new Error('创建交易所账户失败') + return result.data! + } + + // 获取RSA公钥 + const publicKey = await CryptoService.fetchPublicKey() + + // 初始化加密服务 + await CryptoService.initialize(publicKey) + + // 获取用户信息 + const userId = localStorage.getItem('user_id') || '' + const sessionId = sessionStorage.getItem('session_id') || '' + + // 加密敏感数据 + const encryptedPayload = await CryptoService.encryptSensitiveData( + JSON.stringify(request), + userId, + sessionId + ) + + // 发送加密数据 + const result = await httpClient.post<{ id: string }>( + `${API_BASE}/exchanges`, + encryptedPayload + ) + if (!result.success) throw new Error('创建交易所账户失败') + return result.data! + }, + + async deleteExchange(exchangeId: string): Promise { + const result = await httpClient.delete(`${API_BASE}/exchanges/${exchangeId}`) + if (!result.success) throw new Error('删除交易所账户失败') + }, + + async updateExchangeConfigsEncrypted( + request: UpdateExchangeConfigRequest + ): Promise { + // 检查是否启用了传输加密 + const config = await CryptoService.fetchCryptoConfig() + + if (!config.transport_encryption) { + // 传输加密禁用时,直接发送明文 + const result = await httpClient.put(`${API_BASE}/exchanges`, request) + if (!result.success) throw new Error('更新交易所配置失败') + return + } + + // 获取RSA公钥 + const publicKey = await CryptoService.fetchPublicKey() + + // 初始化加密服务 + await CryptoService.initialize(publicKey) + + // 获取用户信息(从localStorage或其他地方) + const userId = localStorage.getItem('user_id') || '' + const sessionId = sessionStorage.getItem('session_id') || '' + + // 加密敏感数据 + const encryptedPayload = await CryptoService.encryptSensitiveData( + JSON.stringify(request), + userId, + sessionId + ) + + // 发送加密数据 + const result = await httpClient.put( + `${API_BASE}/exchanges`, + encryptedPayload + ) + if (!result.success) throw new Error('更新交易所配置失败') + }, + + async getServerIP(): Promise<{ + public_ip: string + message: string + }> { + const result = await httpClient.get<{ + public_ip: string + message: string + }>(`${API_BASE}/server-ip`) + if (!result.success) throw new Error('获取服务器IP失败') + return result.data! + }, +} diff --git a/web/src/lib/api/data.ts b/web/src/lib/api/data.ts new file mode 100644 index 00000000..69292087 --- /dev/null +++ b/web/src/lib/api/data.ts @@ -0,0 +1,123 @@ +import type { + SystemStatus, + AccountInfo, + Position, + DecisionRecord, + Statistics, + CompetitionData, + PositionHistoryResponse, +} from '../../types' +import { API_BASE, httpClient } from './helpers' + +export const dataApi = { + async getStatus(traderId?: string): Promise { + const url = traderId + ? `${API_BASE}/status?trader_id=${traderId}` + : `${API_BASE}/status` + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取系统状态失败') + return result.data! + }, + + async getAccount(traderId?: string): Promise { + const url = traderId + ? `${API_BASE}/account?trader_id=${traderId}` + : `${API_BASE}/account` + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取账户信息失败') + console.log('Account data fetched:', result.data) + return result.data! + }, + + async getPositions(traderId?: string): Promise { + const url = traderId + ? `${API_BASE}/positions?trader_id=${traderId}` + : `${API_BASE}/positions` + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取持仓列表失败') + return result.data! + }, + + async getDecisions(traderId?: string): Promise { + const url = traderId + ? `${API_BASE}/decisions?trader_id=${traderId}` + : `${API_BASE}/decisions` + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取决策日志失败') + return result.data! + }, + + async getLatestDecisions( + traderId?: string, + limit: number = 5 + ): Promise { + const params = new URLSearchParams() + if (traderId) { + params.append('trader_id', traderId) + } + params.append('limit', limit.toString()) + + const result = await httpClient.get( + `${API_BASE}/decisions/latest?${params}` + ) + if (!result.success) throw new Error('获取最新决策失败') + return result.data! + }, + + async getStatistics(traderId?: string): Promise { + const url = traderId + ? `${API_BASE}/statistics?trader_id=${traderId}` + : `${API_BASE}/statistics` + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取统计信息失败') + return result.data! + }, + + async getEquityHistory(traderId?: string): Promise { + const url = traderId + ? `${API_BASE}/equity-history?trader_id=${traderId}` + : `${API_BASE}/equity-history` + const result = await httpClient.get(url) + if (!result.success) throw new Error('获取历史数据失败') + return result.data! + }, + + async getEquityHistoryBatch(traderIds: string[], hours?: number): Promise { + const result = await httpClient.post( + `${API_BASE}/equity-history-batch`, + { trader_ids: traderIds, hours: hours || 0 } + ) + if (!result.success) throw new Error('获取批量历史数据失败') + return result.data! + }, + + async getTopTraders(): Promise { + const result = await httpClient.get(`${API_BASE}/top-traders`) + if (!result.success) throw new Error('获取前5名交易员失败') + return result.data! + }, + + async getPublicTraderConfig(traderId: string): Promise { + const result = await httpClient.get( + `${API_BASE}/trader/${traderId}/config` + ) + if (!result.success) throw new Error('获取公开交易员配置失败') + return result.data! + }, + + async getCompetition(): Promise { + const result = await httpClient.get( + `${API_BASE}/competition` + ) + if (!result.success) throw new Error('获取竞赛数据失败') + return result.data! + }, + + async getPositionHistory(traderId: string, limit: number = 100): Promise { + const result = await httpClient.get( + `${API_BASE}/positions/history?trader_id=${traderId}&limit=${limit}` + ) + if (!result.success) throw new Error('获取历史仓位失败') + return result.data! + }, +} diff --git a/web/src/lib/api/helpers.ts b/web/src/lib/api/helpers.ts new file mode 100644 index 00000000..6c692051 --- /dev/null +++ b/web/src/lib/api/helpers.ts @@ -0,0 +1,40 @@ +import { CryptoService } from '../crypto' +import { httpClient } from '../httpClient' + +export const API_BASE = '/api' + +export { CryptoService, httpClient } + +// Helper function to get auth headers +export function getAuthHeaders(): Record { + const token = localStorage.getItem('auth_token') + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + return headers +} + +export async function handleJSONResponse(res: Response): Promise { + const text = await res.text() + if (!res.ok) { + let message = text || res.statusText + try { + const data = text ? JSON.parse(text) : null + if (data && typeof data === 'object') { + message = data.error || data.message || message + } + } catch { + /* ignore JSON parse errors */ + } + throw new Error(message || '请求失败') + } + if (!text) { + return {} as T + } + return JSON.parse(text) as T +} diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts new file mode 100644 index 00000000..615a56e2 --- /dev/null +++ b/web/src/lib/api/index.ts @@ -0,0 +1,15 @@ +import { traderApi } from './traders' +import { backtestApi } from './backtest' +import { strategyApi } from './strategies' +import { configApi } from './config' +import { dataApi } from './data' +import { telegramApi } from './telegram' + +export const api = { + ...traderApi, + ...backtestApi, + ...strategyApi, + ...configApi, + ...dataApi, + ...telegramApi, +} diff --git a/web/src/lib/api/strategies.ts b/web/src/lib/api/strategies.ts new file mode 100644 index 00000000..448c34e4 --- /dev/null +++ b/web/src/lib/api/strategies.ts @@ -0,0 +1,72 @@ +import type { + Strategy, + StrategyConfig, +} from '../../types' +import { API_BASE, httpClient } from './helpers' + +export const strategyApi = { + async getStrategies(): Promise { + const result = await httpClient.get<{ strategies: Strategy[] }>(`${API_BASE}/strategies`) + if (!result.success) throw new Error('获取策略列表失败') + const strategies = result.data?.strategies + return Array.isArray(strategies) ? strategies : [] + }, + + async getStrategy(strategyId: string): Promise { + const result = await httpClient.get(`${API_BASE}/strategies/${strategyId}`) + if (!result.success) throw new Error('获取策略失败') + return result.data! + }, + + async getActiveStrategy(): Promise { + const result = await httpClient.get(`${API_BASE}/strategies/active`) + if (!result.success) throw new Error('获取激活策略失败') + return result.data! + }, + + async getDefaultStrategyConfig(): Promise { + const result = await httpClient.get(`${API_BASE}/strategies/default-config`) + if (!result.success) throw new Error('获取默认策略配置失败') + return result.data! + }, + + async createStrategy(data: { + name: string + description: string + config: StrategyConfig + }): Promise { + const result = await httpClient.post(`${API_BASE}/strategies`, data) + if (!result.success) throw new Error('创建策略失败') + return result.data! + }, + + async updateStrategy( + strategyId: string, + data: { + name?: string + description?: string + config?: StrategyConfig + } + ): Promise { + const result = await httpClient.put(`${API_BASE}/strategies/${strategyId}`, data) + if (!result.success) throw new Error('更新策略失败') + return result.data! + }, + + async deleteStrategy(strategyId: string): Promise { + const result = await httpClient.delete(`${API_BASE}/strategies/${strategyId}`) + if (!result.success) throw new Error('删除策略失败') + }, + + async activateStrategy(strategyId: string): Promise { + const result = await httpClient.post(`${API_BASE}/strategies/${strategyId}/activate`) + if (!result.success) throw new Error('激活策略失败') + return result.data! + }, + + async duplicateStrategy(strategyId: string): Promise { + const result = await httpClient.post(`${API_BASE}/strategies/${strategyId}/duplicate`) + if (!result.success) throw new Error('复制策略失败') + return result.data! + }, +} diff --git a/web/src/lib/api/telegram.ts b/web/src/lib/api/telegram.ts new file mode 100644 index 00000000..2ee87869 --- /dev/null +++ b/web/src/lib/api/telegram.ts @@ -0,0 +1,25 @@ +import type { TelegramConfig } from '../../types' +import { API_BASE, httpClient } from './helpers' + +export const telegramApi = { + async getTelegramConfig(): Promise { + const result = await httpClient.get(`${API_BASE}/telegram`) + if (!result.success) throw new Error('获取Telegram配置失败') + return result.data! + }, + + async updateTelegramConfig(token: string, modelId?: string): Promise { + const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' }) + if (!result.success) throw new Error('保存Telegram配置失败') + }, + + async unbindTelegram(): Promise { + const result = await httpClient.delete(`${API_BASE}/telegram/binding`) + if (!result.success) throw new Error('解绑Telegram失败') + }, + + async updateTelegramModel(modelId: string): Promise { + const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId }) + if (!result.success) throw new Error('更新Telegram模型失败') + }, +} diff --git a/web/src/lib/api/traders.ts b/web/src/lib/api/traders.ts new file mode 100644 index 00000000..e7e1ba8c --- /dev/null +++ b/web/src/lib/api/traders.ts @@ -0,0 +1,94 @@ +import type { + TraderInfo, + TraderConfigData, + CreateTraderRequest, +} from '../../types' +import { API_BASE, httpClient } from './helpers' + +export const traderApi = { + async getTraders(): Promise { + const result = await httpClient.get(`${API_BASE}/my-traders`) + if (!result.success) throw new Error('获取trader列表失败') + return Array.isArray(result.data) ? result.data : [] + }, + + async getPublicTraders(): Promise { + const result = await httpClient.get(`${API_BASE}/traders`) + if (!result.success) throw new Error('获取公开trader列表失败') + return result.data! + }, + + async createTrader(request: CreateTraderRequest): Promise { + const result = await httpClient.post( + `${API_BASE}/traders`, + request + ) + if (!result.success) throw new Error('创建交易员失败') + return result.data! + }, + + async deleteTrader(traderId: string): Promise { + const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`) + if (!result.success) throw new Error('删除交易员失败') + }, + + async startTrader(traderId: string): Promise { + const result = await httpClient.post( + `${API_BASE}/traders/${traderId}/start` + ) + if (!result.success) throw new Error('启动交易员失败') + }, + + async stopTrader(traderId: string): Promise { + const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`) + if (!result.success) throw new Error('停止交易员失败') + }, + + async toggleCompetition(traderId: string, showInCompetition: boolean): Promise { + const result = await httpClient.put( + `${API_BASE}/traders/${traderId}/competition`, + { show_in_competition: showInCompetition } + ) + if (!result.success) throw new Error('更新竞技场显示设置失败') + }, + + async closePosition(traderId: string, symbol: string, side: string): Promise<{ message: string }> { + const result = await httpClient.post<{ message: string }>( + `${API_BASE}/traders/${traderId}/close-position`, + { symbol, side } + ) + if (!result.success) throw new Error('平仓失败') + return result.data! + }, + + async updateTraderPrompt( + traderId: string, + customPrompt: string + ): Promise { + const result = await httpClient.put( + `${API_BASE}/traders/${traderId}/prompt`, + { custom_prompt: customPrompt } + ) + if (!result.success) throw new Error('更新自定义策略失败') + }, + + async getTraderConfig(traderId: string): Promise { + const result = await httpClient.get( + `${API_BASE}/traders/${traderId}/config` + ) + if (!result.success) throw new Error('获取交易员配置失败') + return result.data! + }, + + async updateTrader( + traderId: string, + request: CreateTraderRequest + ): Promise { + const result = await httpClient.put( + `${API_BASE}/traders/${traderId}`, + request + ) + if (!result.success) throw new Error('更新交易员失败') + return result.data! + }, +}