feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../trades/models/trade_model.dart';
|
||||
import 'asset_trades_event.dart';
|
||||
import 'asset_trades_state.dart';
|
||||
import '../../repositories/asset_repository.dart';
|
||||
@@ -20,28 +19,23 @@ class AssetTradesBloc extends Bloc<AssetTradesEvent, AssetTradesState> {
|
||||
on<TriggerManualAnalysis>((event, emit) async {
|
||||
emit(AssetTradesLoading());
|
||||
try {
|
||||
final analysisRes = await repository.triggerManualAnalysis(event.isin, payload: event.payload);
|
||||
// Server contract: always 200 -> AssetEvaluationResultDto, whether the
|
||||
// pipeline produced a proposal or rejected the opportunity. The trade
|
||||
// list itself is unaffected until the user actually accepts a
|
||||
// proposal, so it is simply reloaded as-is; the analysis result is
|
||||
// surfaced separately for the UI to react to exactly once.
|
||||
final result = await repository.triggerManualAnalysis(event.isin, payload: event.payload);
|
||||
final existingTrades = await repository.getAssetTrades(event.isin, null);
|
||||
|
||||
final list = List<TradeModel>.from(existingTrades);
|
||||
final newProposal = analysisRes?.proposal;
|
||||
if (newProposal != null) {
|
||||
final isDuplicate = list.any((t) => t.id == newProposal.id || (t.analysisId.isNotEmpty && t.analysisId == newProposal.analysisId));
|
||||
if (!isDuplicate) {
|
||||
list.insert(0, newProposal);
|
||||
}
|
||||
}
|
||||
emit(AssetTradesLoaded(list));
|
||||
emit(AssetTradesLoaded(existingTrades, manualAnalysisResult: result));
|
||||
} catch (e) {
|
||||
emit(AssetTradesError("Failed to trigger manual analysis: $e"));
|
||||
}
|
||||
});
|
||||
on<RejectTradeEvent>((event, emit) async {
|
||||
try {
|
||||
await repository.rejectTrade(event.tradeId);
|
||||
add(LoadAssetTrades(event.isin));
|
||||
} catch (e) {
|
||||
emit(AssetTradesError("Failed to reject trade: $e"));
|
||||
on<DismissTradeEvent>((event, emit) {
|
||||
// Purely local: no server call, see DismissTradeEvent doc comment.
|
||||
final current = state;
|
||||
if (current is AssetTradesLoaded) {
|
||||
emit(AssetTradesLoaded(current.data.where((t) => t.id != event.tradeId).toList()));
|
||||
}
|
||||
});
|
||||
on<AcceptTradeEvent>((event, emit) async {
|
||||
@@ -52,6 +46,14 @@ class AssetTradesBloc extends Bloc<AssetTradesEvent, AssetTradesState> {
|
||||
emit(AssetTradesError("Failed to accept trade: $e"));
|
||||
}
|
||||
});
|
||||
on<AddTradeFillEvent>((event, emit) async {
|
||||
try {
|
||||
await repository.addTradeFill(event.tradeId, executedPrice: event.executedPrice, quantity: event.quantity);
|
||||
add(LoadAssetTrades(event.isin));
|
||||
} catch (e) {
|
||||
emit(AssetTradesError("Failed to update trade execution: $e"));
|
||||
}
|
||||
});
|
||||
on<CloseTradeEvent>((event, emit) async {
|
||||
try {
|
||||
await repository.closeTrade(event.tradeId, event.exitPrice);
|
||||
|
||||
@@ -13,16 +13,34 @@ class TriggerManualAnalysis extends AssetTradesEvent {
|
||||
final ManualAnalysisRequestDto? payload;
|
||||
TriggerManualAnalysis(this.isin, {this.payload});
|
||||
}
|
||||
class RejectTradeEvent extends AssetTradesEvent {
|
||||
/// Dismisses a trade proposal from the locally displayed list only.
|
||||
///
|
||||
/// There is no server-side "reject" anymore: a proposal is a system-wide
|
||||
/// opportunity that any user may accept independently, so rejecting it has
|
||||
/// no server-side meaning. This purely removes the card from the current
|
||||
/// in-memory list; the proposal keeps existing server-side until its 24h
|
||||
/// TTL expires, so it can reappear after the next reload (Rules.md §4 —
|
||||
/// no fabricated "permanently rejected" state is invented).
|
||||
class DismissTradeEvent extends AssetTradesEvent {
|
||||
final String tradeId;
|
||||
final String isin;
|
||||
RejectTradeEvent(this.tradeId, this.isin);
|
||||
DismissTradeEvent(this.tradeId);
|
||||
}
|
||||
class AcceptTradeEvent extends AssetTradesEvent {
|
||||
final TradeAcceptanceDto tradeAcceptanceDto;
|
||||
final String isin;
|
||||
AcceptTradeEvent(this.tradeAcceptanceDto, this.isin);
|
||||
}
|
||||
|
||||
/// Records a corrective/additional fill against an already-active trade
|
||||
/// (review-execution path). Distinct from [AcceptTradeEvent], which targets
|
||||
/// a proposal, not an existing trade — see `AssetRepository.addTradeFill`.
|
||||
class AddTradeFillEvent extends AssetTradesEvent {
|
||||
final String tradeId;
|
||||
final String isin;
|
||||
final double executedPrice;
|
||||
final double quantity;
|
||||
AddTradeFillEvent(this.tradeId, this.isin, this.executedPrice, this.quantity);
|
||||
}
|
||||
class CloseTradeEvent extends AssetTradesEvent {
|
||||
final String tradeId;
|
||||
final String isin;
|
||||
|
||||
@@ -5,7 +5,23 @@ class AssetTradesInitial extends AssetTradesState {}
|
||||
class AssetTradesLoading extends AssetTradesState {}
|
||||
class AssetTradesLoaded extends AssetTradesState {
|
||||
final List<TradeModel> data;
|
||||
AssetTradesLoaded(this.data);
|
||||
|
||||
/// Transient result of a just-triggered manual analysis. Only set on the
|
||||
/// state instance emitted directly by `TriggerManualAnalysis` — a plain
|
||||
/// reload/dismiss/accept emits a fresh `AssetTradesLoaded` without it, so a
|
||||
/// `BlocConsumer` listener naturally reacts to it exactly once instead of
|
||||
/// on every rebuild.
|
||||
///
|
||||
/// Always fully populated when set: the server contract no longer has a
|
||||
/// silent "204, no proposal" outcome, so unlike the old
|
||||
/// `manualAnalysisProposal`/`manualAnalysisEmpty` pair, a single non-null
|
||||
/// value here already tells the caller everything — check
|
||||
/// `manualAnalysisResult!.hasProposal` to distinguish an accepted
|
||||
/// opportunity from a rejected one with real scores/AI reasoning attached
|
||||
/// (Rules.md §4).
|
||||
final AssetEvaluationResultModel? manualAnalysisResult;
|
||||
|
||||
AssetTradesLoaded(this.data, {this.manualAnalysisResult});
|
||||
}
|
||||
class AssetTradesError extends AssetTradesState {
|
||||
final String message;
|
||||
|
||||
Reference in New Issue
Block a user