128 lines
4.9 KiB
Dart
128 lines
4.9 KiB
Dart
import 'package:dio/dio.dart';
|
|
import '../../../core/network/api_client.dart';
|
|
import '../models/backtest_history_entry_model.dart';
|
|
import '../models/backtest_report_model.dart';
|
|
|
|
/// Backend endpoint contract: `FinlyticBackend/Controllers/SimulationController.cs`
|
|
/// (`POST /api/v1/simulation/run`, body shape `BacktestRequestDto` in
|
|
/// `FinlyticCore/Dtos/Simulation/SimulationDtos.cs`).
|
|
class SimulationRepository {
|
|
final ApiClient apiClient;
|
|
|
|
const SimulationRepository({required this.apiClient});
|
|
|
|
Future<BacktestReportModel> runBacktest({
|
|
required String isin,
|
|
required String symbol,
|
|
required String strategyKey,
|
|
required String timeframe,
|
|
DateTime? startDateUtc,
|
|
DateTime? endDateUtc,
|
|
double startingCapital = 10000.0,
|
|
double riskPerTradePercent = 1.0,
|
|
bool includeFeesAndSlippage = true,
|
|
bool simulateKnockOutDerivatives = false,
|
|
double? targetLeverage,
|
|
Map<String, double>? strategyParameters,
|
|
}) async {
|
|
final now = DateTime.now().toUtc();
|
|
final start = (startDateUtc ?? now.subtract(const Duration(days: 180))).toUtc();
|
|
final end = (endDateUtc ?? now).toUtc();
|
|
|
|
final response = await apiClient.post('/api/v1/simulation/run', data: {
|
|
'isin': isin,
|
|
'symbol': symbol,
|
|
'strategyKey': strategyKey,
|
|
'timeframe': timeframe,
|
|
'startDateUtc': start.toIso8601String(),
|
|
'endDateUtc': end.toIso8601String(),
|
|
'startingCapital': startingCapital,
|
|
'riskPerTradePercent': riskPerTradePercent,
|
|
'includeFeesAndSlippage': includeFeesAndSlippage,
|
|
'simulateKnockOutDerivatives': simulateKnockOutDerivatives,
|
|
if (targetLeverage != null) 'targetLeverage': targetLeverage,
|
|
if (strategyParameters != null && strategyParameters.isNotEmpty) 'strategyParameters': strategyParameters,
|
|
});
|
|
|
|
if (response.statusCode == 200 && response.data is Map) {
|
|
return BacktestReportModel.fromJson(Map<String, dynamic>.from(response.data as Map));
|
|
}
|
|
|
|
throw Exception('Simulation fehlgeschlagen: Status ${response.statusCode}');
|
|
}
|
|
|
|
/// Lightweight history of past backtest runs for an asset (`GET /api/v1/simulation/history/{isin}`).
|
|
Future<List<BacktestHistoryEntryModel>> getBacktestHistory({
|
|
required String isin,
|
|
String? strategyKey,
|
|
int limit = 20,
|
|
}) async {
|
|
final response = await apiClient.get(
|
|
'/api/v1/simulation/history/$isin',
|
|
queryParameters: {
|
|
if (strategyKey != null && strategyKey.isNotEmpty) 'strategyKey': strategyKey,
|
|
'limit': limit,
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200 && response.data is List) {
|
|
return (response.data as List)
|
|
.whereType<Map>()
|
|
.map((e) => BacktestHistoryEntryModel.fromJson(Map<String, dynamic>.from(e)))
|
|
.toList();
|
|
}
|
|
|
|
throw Exception('Backtest-Verlauf konnte nicht geladen werden: Status ${response.statusCode}');
|
|
}
|
|
|
|
/// Full report (trades + equity curve) for one specific past run (`GET /api/v1/simulation/history/run/{runId}`).
|
|
Future<BacktestReportModel> getBacktestRunDetail(String runId) async {
|
|
final response = await apiClient.get('/api/v1/simulation/history/run/$runId');
|
|
|
|
if (response.statusCode == 200 && response.data is Map) {
|
|
return BacktestReportModel.fromJson(Map<String, dynamic>.from(response.data as Map));
|
|
}
|
|
|
|
throw Exception('Backtest-Lauf konnte nicht geladen werden: Status ${response.statusCode}');
|
|
}
|
|
|
|
/// Saved indicator-parameter profile for one (isin, strategyKey) pair
|
|
/// (`GET /api/v1/simulation/parameters/{isin}/{strategyKey}`). Returns `null` when none was ever saved -
|
|
/// an expected, legitimate empty state (the server responds 404), not an error to surface to the user.
|
|
Future<Map<String, double>?> getStrategyParameters({
|
|
required String isin,
|
|
required String strategyKey,
|
|
}) async {
|
|
try {
|
|
final response = await apiClient.get('/api/v1/simulation/parameters/$isin/$strategyKey');
|
|
if (response.statusCode == 200 && response.data is Map) {
|
|
final raw = Map<String, dynamic>.from(response.data as Map)['parameters'];
|
|
if (raw is Map) {
|
|
return raw.map((key, value) => MapEntry(key.toString(), (value as num).toDouble()));
|
|
}
|
|
}
|
|
return null;
|
|
} on DioException catch (e) {
|
|
if (e.response?.statusCode == 404) return null;
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Saves/updates a per-asset/per-strategy indicator-parameter profile (`POST /api/v1/simulation/parameters`).
|
|
Future<void> saveStrategyParameters({
|
|
required String isin,
|
|
required String strategyKey,
|
|
required Map<String, double> parameters,
|
|
}) async {
|
|
final response = await apiClient.post('/api/v1/simulation/parameters', data: {
|
|
'isin': isin,
|
|
'strategyKey': strategyKey,
|
|
'parameters': parameters,
|
|
});
|
|
|
|
if (response.statusCode != 200) {
|
|
throw Exception('Parameter-Profil konnte nicht gespeichert werden: Status ${response.statusCode}');
|
|
}
|
|
}
|
|
}
|