Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/backtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (s *Server) handleBacktestStart(c *gin.Context) {

runner, err := s.backtestManager.Start(context.Background(), cfg)
if err != nil {
SafeError(c, http.StatusBadRequest, "Failed to start backtest", err)
SafeError(c, http.StatusBadRequest, SanitizeError(err, "Failed to start backtest"), err)
return
}

Expand Down
12 changes: 12 additions & 0 deletions backtest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ func (cfg *BacktestConfig) Validate() error {
return fmt.Errorf("invalid decision_timeframe: %w", err)
}
cfg.DecisionTimeframe = normalizedDecision
if !containsString(cfg.Timeframes, cfg.DecisionTimeframe) {
return fmt.Errorf("decision_timeframe '%s' must be included in timeframes", cfg.DecisionTimeframe)
}

if cfg.DecisionCadenceNBars <= 0 {
cfg.DecisionCadenceNBars = 20
Expand Down Expand Up @@ -164,6 +167,15 @@ func (cfg *BacktestConfig) Duration() time.Duration {
return time.Unix(cfg.EndTS, 0).Sub(time.Unix(cfg.StartTS, 0))
}

func containsString(list []string, target string) bool {
for _, item := range list {
if item == target {
return true
}
}
return false
}

const (
// FillPolicyNextOpen uses the open price of the next bar for execution.
FillPolicyNextOpen = "next_open"
Expand Down
12 changes: 11 additions & 1 deletion backtest/datafeed.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,18 @@ func (df *DataFeed) loadAll() error {
}

// Generate backtest progress timeline using the primary timeframe of the first symbol
if len(df.symbols) == 0 {
return fmt.Errorf("no symbols configured")
}
firstSymbol := df.symbols[0]
primarySeries := df.symbolSeries[firstSymbol].byTF[df.primaryTF]
firstSeries, ok := df.symbolSeries[firstSymbol]
if !ok || firstSeries == nil {
return fmt.Errorf("symbol %s data not loaded", firstSymbol)
}
primarySeries, ok := firstSeries.byTF[df.primaryTF]
if !ok || primarySeries == nil {
return fmt.Errorf("decision timeframe %s not loaded for %s", df.primaryTF, firstSymbol)
}
startMs := start.UnixMilli()
endMs := end.UnixMilli()
for _, ts := range primarySeries.closeTimes {
Expand Down
2 changes: 1 addition & 1 deletion trader/auto_trader.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
}
}
} else {
return nil, fmt.Errorf("initial balance must be greater than 0, please set InitialBalance in config or ensure exchange account has balance")
return nil, fmt.Errorf("初始余额必须大于 0。请在配置中设置 InitialBalance,或将资金转入交易所合约账户后重试。Initial balance must be greater than 0. Please set InitialBalance in config or transfer funds to your exchange futures wallet and retry.")
}
}

Expand Down
10 changes: 9 additions & 1 deletion web/src/components/BacktestPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1379,7 +1379,15 @@ export function BacktestPage() {
const updated = isSelected
? formState.timeframes.filter((t) => t !== tf)
: [...formState.timeframes, tf]
if (updated.length > 0) handleFormChange('timeframes', updated)
if (updated.length > 0) {
setFormState((prev) => ({
...prev,
timeframes: updated,
decisionTf: updated.includes(prev.decisionTf)
? prev.decisionTf
: updated[0],
}))
}
}}
className="px-2 py-1 rounded text-xs transition-all"
style={{
Expand Down
19 changes: 16 additions & 3 deletions web/src/lib/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,13 @@ export class HttpClient {

// Handle 404 Not Found - system error
if (status === 404) {
toast.error('API Not Found', {
description: 'The requested endpoint does not exist (404)',
const serverMessage = this.getServerErrorMessage(error.response.data)
toast.error('API Not Found / API 未找到', {
description: serverMessage
? `Server: ${serverMessage} / 服务端: ${serverMessage}`
: 'The requested endpoint does not exist (404) / 请求的接口不存在 (404)',
})
throw new Error('API not found')
throw new Error(serverMessage || 'API not found')
}

// Handle 500+ Server Error - system error
Expand All @@ -159,6 +162,16 @@ export class HttpClient {
return Promise.reject(error)
}

private getServerErrorMessage(data: unknown): string {
if (!data) return ''
if (typeof data === 'string') return data
if (typeof data === 'object') {
const maybeMessage = (data as any).error || (data as any).message
if (typeof maybeMessage === 'string') return maybeMessage
}
return ''
}

/**
* Generic JSON request with standardized response
* System/network errors are already intercepted and shown via toast
Expand Down