fix: add web/src/lib/api/ split files and fix .gitignore

The .gitignore had a Python-inherited `lib/` rule that blocked the
entire web/src/lib/api/ directory from being tracked. The original
api.ts was a file (not matched), but the split created a directory
inside lib/ which was ignored. Remove the `lib/` gitignore rule and
add the api module split files that were missing from the previous commit.
This commit is contained in:
tinkle-community
2026-03-12 13:38:32 +08:00
parent cb31782be4
commit fcda921d41
9 changed files with 752 additions and 1 deletions

1
.gitignore vendored
View File

@@ -79,7 +79,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/

197
web/src/lib/api/backtest.ts Normal file
View File

@@ -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<BacktestRunsResponse> {
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<BacktestRunsResponse>(res)
},
async startBacktest(config: BacktestStartConfig): Promise<BacktestRunMetadata> {
const res = await fetch(`${API_BASE}/backtest/start`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ config }),
})
return handleJSONResponse<BacktestRunMetadata>(res)
},
async pauseBacktest(runId: string): Promise<BacktestRunMetadata> {
const res = await fetch(`${API_BASE}/backtest/pause`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ run_id: runId }),
})
return handleJSONResponse<BacktestRunMetadata>(res)
},
async resumeBacktest(runId: string): Promise<BacktestRunMetadata> {
const res = await fetch(`${API_BASE}/backtest/resume`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ run_id: runId }),
})
return handleJSONResponse<BacktestRunMetadata>(res)
},
async stopBacktest(runId: string): Promise<BacktestRunMetadata> {
const res = await fetch(`${API_BASE}/backtest/stop`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ run_id: runId }),
})
return handleJSONResponse<BacktestRunMetadata>(res)
},
async updateBacktestLabel(
runId: string,
label: string
): Promise<BacktestRunMetadata> {
const res = await fetch(`${API_BASE}/backtest/label`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ run_id: runId, label }),
})
return handleJSONResponse<BacktestRunMetadata>(res)
},
async deleteBacktestRun(runId: string): Promise<void> {
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<BacktestStatusPayload> {
const res = await fetch(`${API_BASE}/backtest/status?run_id=${runId}`, {
headers: getAuthHeaders(),
})
return handleJSONResponse<BacktestStatusPayload>(res)
},
async getBacktestEquity(
runId: string,
timeframe?: string,
limit?: number
): Promise<BacktestEquityPoint[]> {
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<BacktestEquityPoint[]>(res)
},
async getBacktestTrades(
runId: string,
limit = 200
): Promise<BacktestTradeEvent[]> {
const query = new URLSearchParams({
run_id: runId,
limit: String(limit),
})
const res = await fetch(`${API_BASE}/backtest/trades?${query}`, {
headers: getAuthHeaders(),
})
return handleJSONResponse<BacktestTradeEvent[]>(res)
},
async getBacktestMetrics(runId: string): Promise<BacktestMetrics> {
const res = await fetch(`${API_BASE}/backtest/metrics?run_id=${runId}`, {
headers: getAuthHeaders(),
})
return handleJSONResponse<BacktestMetrics>(res)
},
async getBacktestKlines(
runId: string,
symbol: string,
timeframe?: string
): Promise<BacktestKlinesResponse> {
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<BacktestKlinesResponse>(res)
},
async getBacktestTrace(
runId: string,
cycle?: number
): Promise<DecisionRecord> {
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<DecisionRecord>(res)
},
async getBacktestDecisions(
runId: string,
limit = 20,
offset = 0
): Promise<DecisionRecord[]> {
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<DecisionRecord[]>(res)
},
async exportBacktest(runId: string): Promise<Blob> {
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()
},
}

186
web/src/lib/api/config.ts Normal file
View File

@@ -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<AIModel[]> {
const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`)
if (!result.success) throw new Error('获取模型配置失败')
return Array.isArray(result.data) ? result.data : []
},
async getSupportedModels(): Promise<AIModel[]> {
const result = await httpClient.get<AIModel[]>(
`${API_BASE}/supported-models`
)
if (!result.success) throw new Error('获取支持的模型失败')
return result.data!
},
async getPromptTemplates(): Promise<string[]> {
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<void> {
// 检查是否启用了传输加密
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<Exchange[]> {
const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`)
if (!result.success) throw new Error('获取交易所配置失败')
return result.data!
},
async getSupportedExchanges(): Promise<Exchange[]> {
const result = await httpClient.get<Exchange[]>(
`${API_BASE}/supported-exchanges`
)
if (!result.success) throw new Error('获取支持的交易所失败')
return result.data!
},
async updateExchangeConfigs(
request: UpdateExchangeConfigRequest
): Promise<void> {
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<void> {
const result = await httpClient.delete(`${API_BASE}/exchanges/${exchangeId}`)
if (!result.success) throw new Error('删除交易所账户失败')
},
async updateExchangeConfigsEncrypted(
request: UpdateExchangeConfigRequest
): Promise<void> {
// 检查是否启用了传输加密
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!
},
}

123
web/src/lib/api/data.ts Normal file
View File

@@ -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<SystemStatus> {
const url = traderId
? `${API_BASE}/status?trader_id=${traderId}`
: `${API_BASE}/status`
const result = await httpClient.get<SystemStatus>(url)
if (!result.success) throw new Error('获取系统状态失败')
return result.data!
},
async getAccount(traderId?: string): Promise<AccountInfo> {
const url = traderId
? `${API_BASE}/account?trader_id=${traderId}`
: `${API_BASE}/account`
const result = await httpClient.get<AccountInfo>(url)
if (!result.success) throw new Error('获取账户信息失败')
console.log('Account data fetched:', result.data)
return result.data!
},
async getPositions(traderId?: string): Promise<Position[]> {
const url = traderId
? `${API_BASE}/positions?trader_id=${traderId}`
: `${API_BASE}/positions`
const result = await httpClient.get<Position[]>(url)
if (!result.success) throw new Error('获取持仓列表失败')
return result.data!
},
async getDecisions(traderId?: string): Promise<DecisionRecord[]> {
const url = traderId
? `${API_BASE}/decisions?trader_id=${traderId}`
: `${API_BASE}/decisions`
const result = await httpClient.get<DecisionRecord[]>(url)
if (!result.success) throw new Error('获取决策日志失败')
return result.data!
},
async getLatestDecisions(
traderId?: string,
limit: number = 5
): Promise<DecisionRecord[]> {
const params = new URLSearchParams()
if (traderId) {
params.append('trader_id', traderId)
}
params.append('limit', limit.toString())
const result = await httpClient.get<DecisionRecord[]>(
`${API_BASE}/decisions/latest?${params}`
)
if (!result.success) throw new Error('获取最新决策失败')
return result.data!
},
async getStatistics(traderId?: string): Promise<Statistics> {
const url = traderId
? `${API_BASE}/statistics?trader_id=${traderId}`
: `${API_BASE}/statistics`
const result = await httpClient.get<Statistics>(url)
if (!result.success) throw new Error('获取统计信息失败')
return result.data!
},
async getEquityHistory(traderId?: string): Promise<any[]> {
const url = traderId
? `${API_BASE}/equity-history?trader_id=${traderId}`
: `${API_BASE}/equity-history`
const result = await httpClient.get<any[]>(url)
if (!result.success) throw new Error('获取历史数据失败')
return result.data!
},
async getEquityHistoryBatch(traderIds: string[], hours?: number): Promise<any> {
const result = await httpClient.post<any>(
`${API_BASE}/equity-history-batch`,
{ trader_ids: traderIds, hours: hours || 0 }
)
if (!result.success) throw new Error('获取批量历史数据失败')
return result.data!
},
async getTopTraders(): Promise<any[]> {
const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`)
if (!result.success) throw new Error('获取前5名交易员失败')
return result.data!
},
async getPublicTraderConfig(traderId: string): Promise<any> {
const result = await httpClient.get<any>(
`${API_BASE}/trader/${traderId}/config`
)
if (!result.success) throw new Error('获取公开交易员配置失败')
return result.data!
},
async getCompetition(): Promise<CompetitionData> {
const result = await httpClient.get<CompetitionData>(
`${API_BASE}/competition`
)
if (!result.success) throw new Error('获取竞赛数据失败')
return result.data!
},
async getPositionHistory(traderId: string, limit: number = 100): Promise<PositionHistoryResponse> {
const result = await httpClient.get<PositionHistoryResponse>(
`${API_BASE}/positions/history?trader_id=${traderId}&limit=${limit}`
)
if (!result.success) throw new Error('获取历史仓位失败')
return result.data!
},
}

View File

@@ -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<string, string> {
const token = localStorage.getItem('auth_token')
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
return headers
}
export async function handleJSONResponse<T>(res: Response): Promise<T> {
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
}

15
web/src/lib/api/index.ts Normal file
View File

@@ -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,
}

View File

@@ -0,0 +1,72 @@
import type {
Strategy,
StrategyConfig,
} from '../../types'
import { API_BASE, httpClient } from './helpers'
export const strategyApi = {
async getStrategies(): Promise<Strategy[]> {
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<Strategy> {
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/${strategyId}`)
if (!result.success) throw new Error('获取策略失败')
return result.data!
},
async getActiveStrategy(): Promise<Strategy> {
const result = await httpClient.get<Strategy>(`${API_BASE}/strategies/active`)
if (!result.success) throw new Error('获取激活策略失败')
return result.data!
},
async getDefaultStrategyConfig(): Promise<StrategyConfig> {
const result = await httpClient.get<StrategyConfig>(`${API_BASE}/strategies/default-config`)
if (!result.success) throw new Error('获取默认策略配置失败')
return result.data!
},
async createStrategy(data: {
name: string
description: string
config: StrategyConfig
}): Promise<Strategy> {
const result = await httpClient.post<Strategy>(`${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<Strategy> {
const result = await httpClient.put<Strategy>(`${API_BASE}/strategies/${strategyId}`, data)
if (!result.success) throw new Error('更新策略失败')
return result.data!
},
async deleteStrategy(strategyId: string): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/strategies/${strategyId}`)
if (!result.success) throw new Error('删除策略失败')
},
async activateStrategy(strategyId: string): Promise<Strategy> {
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/activate`)
if (!result.success) throw new Error('激活策略失败')
return result.data!
},
async duplicateStrategy(strategyId: string): Promise<Strategy> {
const result = await httpClient.post<Strategy>(`${API_BASE}/strategies/${strategyId}/duplicate`)
if (!result.success) throw new Error('复制策略失败')
return result.data!
},
}

View File

@@ -0,0 +1,25 @@
import type { TelegramConfig } from '../../types'
import { API_BASE, httpClient } from './helpers'
export const telegramApi = {
async getTelegramConfig(): Promise<TelegramConfig> {
const result = await httpClient.get<TelegramConfig>(`${API_BASE}/telegram`)
if (!result.success) throw new Error('获取Telegram配置失败')
return result.data!
},
async updateTelegramConfig(token: string, modelId?: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' })
if (!result.success) throw new Error('保存Telegram配置失败')
},
async unbindTelegram(): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/telegram/binding`)
if (!result.success) throw new Error('解绑Telegram失败')
},
async updateTelegramModel(modelId: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId })
if (!result.success) throw new Error('更新Telegram模型失败')
},
}

View File

@@ -0,0 +1,94 @@
import type {
TraderInfo,
TraderConfigData,
CreateTraderRequest,
} from '../../types'
import { API_BASE, httpClient } from './helpers'
export const traderApi = {
async getTraders(): Promise<TraderInfo[]> {
const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`)
if (!result.success) throw new Error('获取trader列表失败')
return Array.isArray(result.data) ? result.data : []
},
async getPublicTraders(): Promise<any[]> {
const result = await httpClient.get<any[]>(`${API_BASE}/traders`)
if (!result.success) throw new Error('获取公开trader列表失败')
return result.data!
},
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
const result = await httpClient.post<TraderInfo>(
`${API_BASE}/traders`,
request
)
if (!result.success) throw new Error('创建交易员失败')
return result.data!
},
async deleteTrader(traderId: string): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`)
if (!result.success) throw new Error('删除交易员失败')
},
async startTrader(traderId: string): Promise<void> {
const result = await httpClient.post(
`${API_BASE}/traders/${traderId}/start`
)
if (!result.success) throw new Error('启动交易员失败')
},
async stopTrader(traderId: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`)
if (!result.success) throw new Error('停止交易员失败')
},
async toggleCompetition(traderId: string, showInCompetition: boolean): Promise<void> {
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<void> {
const result = await httpClient.put(
`${API_BASE}/traders/${traderId}/prompt`,
{ custom_prompt: customPrompt }
)
if (!result.success) throw new Error('更新自定义策略失败')
},
async getTraderConfig(traderId: string): Promise<TraderConfigData> {
const result = await httpClient.get<TraderConfigData>(
`${API_BASE}/traders/${traderId}/config`
)
if (!result.success) throw new Error('获取交易员配置失败')
return result.data!
},
async updateTrader(
traderId: string,
request: CreateTraderRequest
): Promise<TraderInfo> {
const result = await httpClient.put<TraderInfo>(
`${API_BASE}/traders/${traderId}`,
request
)
if (!result.success) throw new Error('更新交易员失败')
return result.data!
},
}