201 lines
8.1 KiB
Dart
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'));
|
|
}
|
|
}
|
|
}
|