189 lines
6.1 KiB
Dart
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();
|
|
}
|
|
}
|