feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core

This commit is contained in:
2026-08-24 21:37:43 +02:00
parent 676496b77d
commit 0894c40f07
113 changed files with 12413 additions and 3613 deletions
@@ -30,6 +30,35 @@ class TradeRepository {
}
}
/// Fetches system-wide, currently active trade proposals
/// (`GET /api/v1/user/trades?status=Proposed`).
///
/// Unlike [fetchTrades], the server does **not** return `ActiveTradeDto`
/// (`TradeModel`) for this query: `UserTradesController.GetUserTrades`
/// branches on `status == "Proposed"` and instead calls `engine_GetProposals`,
/// which returns `List<TradeProposalDto>` — a structurally different shape
/// (`proposalId` instead of `id`, no `userId`/PnL fields, since proposals are
/// system-wide opportunities not owned by any user). Parsing that response
/// as `TradeModel` would silently produce garbage/empty fields, so this is a
/// dedicated method that parses `TradeProposalModel` instead of overloading
/// [fetchTrades] for two incompatible server-side contracts.
Future<List<TradeProposalModel>> fetchProposals() async {
try {
final response = await apiClient.get('/api/v1/user/trades', queryParameters: {
'status': 'Proposed',
'_t': DateTime.now().millisecondsSinceEpoch,
});
if (response.statusCode == 200 && response.data != null) {
final List<dynamic> data = response.data;
return data.map((json) => TradeProposalModel.fromJson(Map<String, dynamic>.from(json))).toList();
}
return [];
} catch (e) {
throw Exception('Vorschläge konnten nicht geladen werden: $e');
}
}
Future<List<DerivativeItemModel>> fetchDerivatives(
String isin, {
String optionType = 'long',
@@ -73,12 +102,13 @@ class TradeRepository {
}
}
Future<void> rejectTrade(String tradeId) async {
final response = await apiClient.post('/api/v1/user/trades/$tradeId/reject');
if (response.statusCode != 200) {
throw Exception('Trade konnte nicht abgelehnt werden');
}
}
// NOTE: there is intentionally no `rejectTrade` here anymore. A trade
// proposal is a system-wide opportunity that many users may accept
// independently; "rejecting" it server-side would have no meaning and
// the corresponding endpoint (`POST /api/v1/user/trades/{id}/reject`) has
// been removed. Dismissing a proposal is now a purely local UI action
// (see `AssetTradesBloc`'s `DismissTradeEvent`) — the proposal keeps
// existing server-side until it naturally expires (24h TTL).
Future<void> closeTrade(String id, {CloseTradeRequestDto? dto}) async {
final response = await apiClient.post('/api/v1/user/trades/$id/close', data: dto?.toJson());
@@ -86,5 +116,34 @@ class TradeRepository {
throw Exception('Trade konnte nicht geschlossen werden');
}
}
/// Records an additional/corrective fill against an already-active trade
/// (`EngineController.AddTradeFill` -> `engine_AddFill`). This is the
/// correct server-side counterpart for the "review/edit execution
/// numbers" path on an active trade — unlike `acceptTrade`, which targets
/// a proposal, not an existing trade. `userId`/`tradeId` are always
/// overwritten server-side from the JWT claim/route, never trusted from
/// this payload.
Future<TradeModel> addTradeFill(
String tradeId, {
required double executedPrice,
required double quantity,
double fee = 0,
String? note,
}) async {
final response = await apiClient.post(
'/api/v1/engine/trades/$tradeId/fills',
data: {
'executedPrice': executedPrice,
'quantity': quantity,
'fee': fee,
if (note != null) 'note': note,
},
);
if (response.statusCode == 200 && response.data != null) {
return TradeModel.fromJson(response.data);
}
throw Exception('Ausführung konnte nicht gespeichert werden');
}
}