Files
Finlytic/FinlyticApp/lib/features/simulation/cubit/backtest_cubit.dart
T

201 lines
8.1 KiB
Dart

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/backtest_history_entry_model.dart';
import '../models/backtest_report_model.dart';
import '../repositories/simulation_repository.dart';
class BacktestState extends Equatable {
final bool isLoading;
final BacktestReportModel? report;
final String? errorMessage;
/// True while [report] is a past run fetched via [BacktestCubit.viewHistoricalRun] rather than a freshly
/// executed backtest - lets the UI label the summary/chart as "aus dem Verlauf" instead of implying a new
/// run just completed.
final bool isViewingHistoricalRun;
final bool isHistoryLoading;
final List<BacktestHistoryEntryModel> history;
final String? historyErrorMessage;
/// Tunable indicator-parameter overrides for the currently selected strategy, keyed by bare parameter name
/// (e.g. `"EmaFast"`, NOT `"TrendPullbackFvg.EmaFast"`) - the `"{strategyKey}."` prefix required by the
/// backend (`TechnicalContext.ParameterOverrides`) is applied only when sending/loading, so this map stays
/// meaningful regardless of which strategy is currently selected. Reset whenever the strategy changes (see
/// [BacktestCubit.resetParameterOverrides]) - the same bare name means something different per strategy.
final Map<String, double> parameterOverrides;
final bool isParametersLoading;
final String? parametersMessage;
const BacktestState({
this.isLoading = false,
this.report,
this.errorMessage,
this.isViewingHistoricalRun = false,
this.isHistoryLoading = false,
this.history = const [],
this.historyErrorMessage,
this.parameterOverrides = const {},
this.isParametersLoading = false,
this.parametersMessage,
});
BacktestState copyWith({
bool? isLoading,
BacktestReportModel? report,
String? errorMessage,
bool clearError = false,
bool? isViewingHistoricalRun,
bool? isHistoryLoading,
List<BacktestHistoryEntryModel>? history,
String? historyErrorMessage,
bool clearHistoryError = false,
Map<String, double>? parameterOverrides,
bool? isParametersLoading,
String? parametersMessage,
bool clearParametersMessage = false,
}) {
return BacktestState(
isLoading: isLoading ?? this.isLoading,
report: report ?? this.report,
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
isViewingHistoricalRun: isViewingHistoricalRun ?? this.isViewingHistoricalRun,
isHistoryLoading: isHistoryLoading ?? this.isHistoryLoading,
history: history ?? this.history,
historyErrorMessage: clearHistoryError ? null : (historyErrorMessage ?? this.historyErrorMessage),
parameterOverrides: parameterOverrides ?? this.parameterOverrides,
isParametersLoading: isParametersLoading ?? this.isParametersLoading,
parametersMessage: clearParametersMessage ? null : (parametersMessage ?? this.parametersMessage),
);
}
@override
List<Object?> get props => [
isLoading,
report,
errorMessage,
isViewingHistoricalRun,
isHistoryLoading,
history,
historyErrorMessage,
parameterOverrides,
isParametersLoading,
parametersMessage,
];
}
class BacktestCubit extends Cubit<BacktestState> {
final SimulationRepository repository;
BacktestCubit({required this.repository}) : super(const BacktestState());
Future<void> runBacktest({
required String isin,
required String symbol,
required String strategyKey,
required String timeframe,
}) async {
emit(state.copyWith(isLoading: true, clearError: true, isViewingHistoricalRun: false));
try {
final prefixedParams = state.parameterOverrides.isEmpty
? null
: state.parameterOverrides.map((name, value) => MapEntry('$strategyKey.$name', value));
final report = await repository.runBacktest(
isin: isin,
symbol: symbol,
strategyKey: strategyKey,
timeframe: timeframe,
strategyParameters: prefixedParams,
);
emit(state.copyWith(isLoading: false, report: report, clearError: true, isViewingHistoricalRun: false));
// The run that just completed is now the newest history entry - refresh so it shows up immediately
// instead of the user only seeing it after manually reopening the history list.
await loadHistory(isin: isin, strategyKey: strategyKey);
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: 'Fehler beim Starten des Backtests: $e'));
}
}
Future<void> loadHistory({required String isin, String? strategyKey}) async {
if (isin.trim().isEmpty) {
emit(state.copyWith(history: const [], clearHistoryError: true));
return;
}
emit(state.copyWith(isHistoryLoading: true, clearHistoryError: true));
try {
final history = await repository.getBacktestHistory(isin: isin, strategyKey: strategyKey);
emit(state.copyWith(isHistoryLoading: false, history: history, clearHistoryError: true));
} catch (e) {
emit(state.copyWith(isHistoryLoading: false, historyErrorMessage: 'Verlauf konnte nicht geladen werden: $e'));
}
}
Future<void> viewHistoricalRun(String runId) async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
final report = await repository.getBacktestRunDetail(runId);
emit(state.copyWith(isLoading: false, report: report, clearError: true, isViewingHistoricalRun: true));
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: 'Backtest-Lauf konnte nicht geladen werden: $e'));
}
}
/// Sets a single tunable-parameter override (bare name, e.g. `"EmaFast"`) for the currently selected strategy.
void setParameterOverride(String paramName, double value) {
final updated = Map<String, double>.from(state.parameterOverrides);
updated[paramName] = value;
emit(state.copyWith(parameterOverrides: updated, clearParametersMessage: true));
}
/// Clears all overrides - called whenever the selected strategy changes, since a bare parameter name means
/// something different per strategy (e.g. `"Period"` is an RSI period for one strategy, a Donchian-channel
/// length for another).
void resetParameterOverrides() {
emit(state.copyWith(parameterOverrides: const {}, clearParametersMessage: true));
}
/// Loads a previously saved parameter profile for (isin, strategyKey) into [BacktestState.parameterOverrides].
Future<void> loadSavedParameters({required String isin, required String strategyKey}) async {
emit(state.copyWith(isParametersLoading: true, clearParametersMessage: true));
try {
final saved = await repository.getStrategyParameters(isin: isin, strategyKey: strategyKey);
if (saved == null) {
emit(state.copyWith(
isParametersLoading: false,
parametersMessage: 'Kein gespeichertes Profil für dieses Asset/diese Strategie.',
));
return;
}
final prefix = '$strategyKey.';
final bare = <String, double>{};
saved.forEach((key, value) {
if (key.startsWith(prefix)) bare[key.substring(prefix.length)] = value;
});
emit(state.copyWith(
isParametersLoading: false,
parameterOverrides: bare,
parametersMessage: 'Gespeichertes Profil geladen.',
));
} catch (e) {
emit(state.copyWith(isParametersLoading: false, parametersMessage: 'Fehler beim Laden des Profils: $e'));
}
}
/// Saves the current [BacktestState.parameterOverrides] as a reusable profile for (isin, strategyKey).
Future<void> saveCurrentParameters({required String isin, required String strategyKey}) async {
emit(state.copyWith(isParametersLoading: true, clearParametersMessage: true));
try {
final prefixed = state.parameterOverrides.map((name, value) => MapEntry('$strategyKey.$name', value));
await repository.saveStrategyParameters(isin: isin, strategyKey: strategyKey, parameters: prefixed);
emit(state.copyWith(isParametersLoading: false, parametersMessage: 'Parameter-Profil gespeichert.'));
} catch (e) {
emit(state.copyWith(isParametersLoading: false, parametersMessage: 'Fehler beim Speichern des Profils: $e'));
}
}
}