| 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 |
|
|
| |
| 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) |
| } |
|
|
| |
| 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 |
|
|
| |
| 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) |
| } |
| } |
|
|
| |
|
|
| 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.', |
| }) |
| } |
| } |
|
|