Files
Finlytic/FinlyticApp/lib/features/bot/bloc/bot_bloc.dart
T

189 lines
6.1 KiB
Dart

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<BotEvent, BotState> {
final BotRepository repository;
final SignalRService? signalRService;
StreamSubscription? _botPositionSub;
StreamSubscription? _portfolioSummarySub;
BotBloc({
required this.repository,
this.signalRService,
}) : super(const BotInitial()) {
on<FetchBotDashboard>(_onFetchBotDashboard);
on<OnBotPositionStreamReceived>(_onBotPositionStreamReceived);
on<OnPortfolioSummaryStreamReceived>(_onPortfolioSummaryStreamReceived);
on<TriggerBotPanicClose>(_onTriggerBotPanicClose);
on<ExecuteManualBotProposal>(_onExecuteManualBotProposal);
on<UpdateBotConfigSettings>(_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<void> _onFetchBotDashboard(FetchBotDashboard event, Emitter<BotState> 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<BotTradeOrderModel>;
emit(BotLoaded(
status: status,
summary: summary,
positions: positions,
));
} catch (e) {
emit(BotError(e.toString()));
}
}
void _onBotPositionStreamReceived(OnBotPositionStreamReceived event, Emitter<BotState> emit) {
if (state is BotLoaded) {
final current = state as BotLoaded;
final updatedList = List<BotTradeOrderModel>.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<BotState> emit) {
if (state is BotLoaded) {
final current = state as BotLoaded;
emit(current.copyWith(summary: event.summary));
}
}
Future<void> _onTriggerBotPanicClose(TriggerBotPanicClose event, Emitter<BotState> 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<void> _onExecuteManualBotProposal(ExecuteManualBotProposal event, Emitter<BotState> 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<BotTradeOrderModel>.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<void> _onUpdateBotConfigSettings(UpdateBotConfigSettings event, Emitter<BotState> 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<void> close() {
_botPositionSub?.cancel();
_portfolioSummarySub?.cancel();
return super.close();
}
}