feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user