borsa / nextjs-app /src /app /api /backtest /route.ts
veteroner's picture
fix: make sync endpoint market-aware — prevent US scan results from overwriting BIST file
ce7f322
import { NextRequest, NextResponse } from 'next/server'
import { apiUrl, API_BASE } from '@/lib/runtime-config'
import { requireAuth } from '@/lib/api-auth'
import { apiError, ApiErrors } from '@/lib/api-response'
import { getHistoricalPrices, type Market } from '@/lib/yahoo-finance'
export const dynamic = 'force-dynamic'
export async function POST(req: NextRequest) {
const auth = await requireAuth(req)
if (!auth.authenticated) return auth.response
if (!API_BASE) return ApiErrors.noApiBase()
let body: Record<string, unknown> | null = null
try {
body = await req.json()
} catch {
return ApiErrors.invalidJson()
}
if (!body || typeof body !== 'object') {
return ApiErrors.bodyRequired()
}
const symbol = typeof body.symbol === 'string' ? body.symbol.trim().toUpperCase() : ''
if (!symbol || !/^[A-Z0-9.]{1,20}$/.test(symbol)) {
return ApiErrors.invalidSymbol()
}
body.symbol = symbol
const strategy = typeof body.strategy === 'string' ? body.strategy.trim() : ''
if (!strategy) {
return apiError('strategy alanı gerekli', 400)
}
const market = (typeof body.market === 'string' ? body.market : 'bist') as Market
// US mode: compute buy-and-hold locally from Yahoo Finance
if (market === 'us') {
try {
const startDate = typeof body.start_date === 'string' ? body.start_date : '2024-01-01'
const endDate = typeof body.end_date === 'string' ? body.end_date : '2025-01-01'
const initialCapital = typeof body.initial_capital === 'number' ? body.initial_capital : 10000
const prices = await getHistoricalPrices(symbol, '5y', '1d', 'us')
if (!prices || prices.length === 0) {
return apiError('No price data available for this symbol', 404)
}
// Filter by date range
const filtered = prices.filter((p) => {
const d = p.date
return d >= startDate && d <= endDate
})
if (filtered.length < 2) {
return apiError('Not enough data in the selected date range', 400)
}
const firstPrice = filtered[0].close
const lastPrice = filtered[filtered.length - 1].close
const shares = Math.floor(initialCapital / firstPrice)
const finalCapital = shares * lastPrice + (initialCapital - shares * firstPrice)
const totalReturn = ((finalCapital - initialCapital) / initialCapital) * 100
const buyAndHoldReturn = ((lastPrice - firstPrice) / firstPrice) * 100
// Calculate max drawdown
let peak = filtered[0].close
let maxDrawdown = 0
for (const p of filtered) {
if (p.close > peak) peak = p.close
const dd = ((peak - p.close) / peak) * 100
if (dd > maxDrawdown) maxDrawdown = dd
}
const result = {
symbol,
strategy: 'buy_and_hold',
period: { start_date: filtered[0].date, end_date: filtered[filtered.length - 1].date },
initial_capital: initialCapital,
final_capital: Math.round(finalCapital * 100) / 100,
total_return: Math.round(totalReturn * 100) / 100,
buy_and_hold_return: Math.round(buyAndHoldReturn * 100) / 100,
trades_count: 2,
win_rate: totalReturn > 0 ? 100 : 0,
max_drawdown: Math.round(maxDrawdown * 100) / 100,
sharpe_ratio: 0,
trades: [
{ type: 'BUY', date: filtered[0].date, price: Math.round(firstPrice * 100) / 100, shares },
{ type: 'SELL', date: filtered[filtered.length - 1].date, price: Math.round(lastPrice * 100) / 100, shares },
],
}
return NextResponse.json({ ok: true, data: result }, { status: 200 })
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Unknown error'
return ApiErrors.unknown(message)
}
}
// BIST mode: proxy to HF backend
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 30000)
let resp: Response
try {
resp = await fetch(apiUrl('/api/backtest'), {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
cache: 'no-store',
body: JSON.stringify(body ?? {}),
signal: controller.signal,
})
} finally {
clearTimeout(timeout)
}
const payload = await resp.json().catch(() => null)
if (!resp.ok) {
if (resp.status >= 500) {
return ApiErrors.upstreamUnavailable('backtest', {
detail: payload,
rootError: `Upstream backtest error: ${resp.status}`,
hint: 'HF backtest backend geçici olarak ulaşılamaz durumda. Backtest şu an çalıştırılamıyor.',
})
}
return ApiErrors.upstream('backtest', resp.status, payload)
}
return NextResponse.json({ ok: true, ...(payload ?? {}) }, { status: 200 })
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Unknown error'
return ApiErrors.upstreamUnavailable('backtest', {
rootError: message,
hint: 'HF backtest backend geçici olarak ulaşılamaz durumda. Backtest şu an çalıştırılamıyor.',
})
}
}