import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/network/signalr_service.dart'; import '../models/bot_models.dart'; import '../repositories/bot_repository.dart'; import 'bot_event.dart'; import 'bot_state.dart'; class BotBloc extends Bloc { final BotRepository repository; final SignalRService? signalRService; StreamSubscription? _botPositionSub; StreamSubscription? _portfolioSummarySub; BotBloc({ required this.repository, this.signalRService, }) : super(const BotInitial()) { on(_onFetchBotDashboard); on(_onBotPositionStreamReceived); on(_onPortfolioSummaryStreamReceived); on(_onTriggerBotPanicClose); on(_onExecuteManualBotProposal); on(_onUpdateBotConfigSettings); _initSignalRListeners(); } void _initSignalRListeners() { if (signalRService != null) { _botPositionSub = signalRService!.botPositionStream.listen((data) { try { final position = BotTradeOrderModel.fromJson(data); add(OnBotPositionStreamReceived(position)); } catch (_) {} }); _portfolioSummarySub = signalRService!.portfolioSummaryStream.listen((data) { try { final summary = AccountSummaryModel.fromJson(data); add(OnPortfolioSummaryStreamReceived(summary)); } catch (_) {} }); } } Future _onFetchBotDashboard(FetchBotDashboard event, Emitter emit) async { emit(const BotLoading()); try { final results = await Future.wait([ repository.fetchStatus(), repository.fetchSummary(), repository.fetchActivePositions(), ]); final status = results[0] as BotStatusModel; final summary = results[1] as AccountSummaryModel; final positions = results[2] as List; emit(BotLoaded( status: status, summary: summary, positions: positions, )); } catch (e) { emit(BotError(e.toString())); } } void _onBotPositionStreamReceived(OnBotPositionStreamReceived event, Emitter emit) { if (state is BotLoaded) { final current = state as BotLoaded; final updatedList = List.from(current.positions); final index = updatedList.indexWhere((p) => p.orderId == event.position.orderId); if (index != -1) { updatedList[index] = event.position; } else { updatedList.insert(0, event.position); } emit(current.copyWith(positions: updatedList)); } } void _onPortfolioSummaryStreamReceived(OnPortfolioSummaryStreamReceived event, Emitter emit) { if (state is BotLoaded) { final current = state as BotLoaded; emit(current.copyWith(summary: event.summary)); } } Future _onTriggerBotPanicClose(TriggerBotPanicClose event, Emitter emit) async { if (state is BotLoaded) { final current = state as BotLoaded; emit(current.copyWith(isPanicClosing: true)); try { final result = await repository.panicCloseAll(); final updatedPositions = await repository.fetchActivePositions(); final updatedSummary = await repository.fetchSummary(); // A partial result (some Alpaca positions could not be confirmed as closed by the broker) must // never be presented as a full success (Rules.md §4) - surface the skipped count explicitly. final message = result.skippedCount > 0 ? '${result.closedCount} Position(en) geschlossen, aber ${result.skippedCount} konnte(n) NICHT bestätigt geschlossen werden (Broker nicht erreichbar/konfiguriert). Bitte manuell prüfen!' : '${result.closedCount} Position(en) erfolgreich geschlossen.'; emit(current.copyWith( isPanicClosing: false, positions: updatedPositions, summary: updatedSummary, actionMessage: message, actionIsWarning: result.skippedCount > 0, )); } catch (e) { emit(current.copyWith( isPanicClosing: false, actionMessage: 'Fehler beim Notverkauf: $e', actionIsWarning: true, )); } } } Future _onExecuteManualBotProposal(ExecuteManualBotProposal event, Emitter emit) async { if (state is BotLoaded) { final current = state as BotLoaded; try { final order = await repository.executeProposal( event.proposalId, venue: event.venue, quantity: event.quantity, ); final updatedList = List.from(current.positions); final index = updatedList.indexWhere((p) => p.orderId == order.orderId); if (index != -1) { updatedList[index] = order; } else { updatedList.insert(0, order); } emit(current.copyWith( positions: updatedList, actionMessage: 'Trade ${order.symbol} erfolgreich ausgeführt (${order.venue}).', )); } catch (e) { emit(current.copyWith( actionMessage: 'Ausführungsfehler: $e', actionIsWarning: true, )); } } } Future _onUpdateBotConfigSettings(UpdateBotConfigSettings event, Emitter emit) async { if (state is BotLoaded) { final current = state as BotLoaded; try { final updatedStatus = await repository.updateSettings( autoExecutionEnabled: event.autoExecutionEnabled, maxPositions: event.maxPositions, riskPerTradePercent: event.riskPerTradePercent, minCompositeScore: event.minCompositeScore, ); emit(current.copyWith( status: updatedStatus, actionMessage: 'Bot-Konfiguration aktualisiert.', )); } catch (e) { emit(current.copyWith( actionMessage: 'Fehler beim Speichern der Einstellungen: $e', actionIsWarning: true, )); } } } @override Future close() { _botPositionSub?.cancel(); _portfolioSummarySub?.cancel(); return super.close(); } }