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
@@ -0,0 +1,325 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/asset_logo_widget.dart';
import '../../../core/widgets/glass_container.dart';
import '../../trades/models/trade_model.dart';
class ProposalDecisionScreen extends StatelessWidget {
final TradeProposalModel proposal;
final VoidCallback? onExecuteBot;
final VoidCallback? onManualTrade;
const ProposalDecisionScreen({
super.key,
required this.proposal,
this.onExecuteBot,
this.onManualTrade,
});
@override
Widget build(BuildContext context) {
final symbol = proposal.symbol.isNotEmpty ? proposal.symbol : 'ASSET';
final isin = proposal.underlyingIsin;
final isLong = proposal.isLong;
final strategyKey = proposal.strategyKey.isNotEmpty ? proposal.strategyKey : 'Unbekannte Strategie';
final score = proposal.compositeScore;
final entryPrice = proposal.entryPrice;
final invalidationPrice = proposal.invalidationPrice;
final aiValidation = proposal.aiValidation;
final hasAiThesisContent = aiValidation != null && aiValidation.hasContent;
final isRuleBasedApproval = aiValidation != null && !aiValidation.isAiValidated;
final aiConfidence = aiValidation?.confidence;
final deriv = proposal.selectedDerivative;
final hasDerivative = deriv != null;
return Scaffold(
backgroundColor: AppTheme.darkBackground,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text('Trade Proposal: $symbol', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Asset Card
GlassContainer(
padding: const EdgeInsets.all(16),
child: Row(
children: [
AssetLogoWidget(
symbolOrName: symbol,
imageUrl: '/api/v1/logo/$isin',
size: 48,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(symbol, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white)),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: isLong ? AppTheme.primaryEmerald.withValues(alpha: 0.2) : AppTheme.accentRed.withValues(alpha: 0.2),
),
child: Text(
isLong ? 'LONG' : 'SHORT',
style: TextStyle(
color: isLong ? AppTheme.primaryEmerald : AppTheme.accentRed,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
],
),
const SizedBox(height: 4),
Text(isin, style: TextStyle(color: AppTheme.textMuted, fontSize: 12)),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppTheme.primaryEmerald.withValues(alpha: 0.15),
border: Border.all(color: AppTheme.primaryEmerald, width: 1.5),
),
child: Column(
children: [
Text('${score.toStringAsFixed(0)} Pkt', style: TextStyle(color: AppTheme.primaryEmerald, fontWeight: FontWeight.bold, fontSize: 16)),
Text('Score', style: TextStyle(color: AppTheme.textMuted, fontSize: 9)),
],
),
),
],
),
),
const SizedBox(height: 16),
// AI Thesis & Catalysts Card
GlassContainer(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.psychology, size: 18, color: Colors.purpleAccent),
SizedBox(width: 8),
Text('KI-Guardian Thesis & Validierung', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.white)),
],
),
const SizedBox(height: 10),
if (aiValidation != null)
Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: isRuleBasedApproval
? Colors.amber.withValues(alpha: 0.12)
: AppTheme.primaryEmerald.withValues(alpha: 0.12),
border: Border.all(color: isRuleBasedApproval ? Colors.amber : AppTheme.primaryEmerald),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isRuleBasedApproval ? Icons.rule_outlined : Icons.smart_toy_outlined,
size: 14,
color: isRuleBasedApproval ? Colors.amber : AppTheme.primaryEmerald,
),
const SizedBox(width: 6),
Text(
isRuleBasedApproval
? 'Regelbasierte Freigabe keine KI-Bewertung durchgeführt'
: (aiConfidence != null
? 'KI-validiert · Konfidenz ${(aiConfidence * 100).toStringAsFixed(0)}%'
: 'KI-validiert · Konfidenz nicht verfügbar'),
style: TextStyle(
color: isRuleBasedApproval ? Colors.amber : AppTheme.primaryEmerald,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
],
),
),
if (!hasAiThesisContent)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, size: 16, color: AppTheme.textMuted),
const SizedBox(width: 8),
Expanded(
child: Text(
aiValidation == null
? 'Keine KI-Validierung für diesen Vorschlag verfügbar.'
: 'Keine weiteren Details zur Freigabe hinterlegt.',
style: TextStyle(color: AppTheme.textMuted, fontSize: 13, height: 1.4),
),
),
],
)
else ...[
Text(aiValidation.thesisSummary, style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4)),
if (aiValidation.keyCatalysts.isNotEmpty) ...[
const SizedBox(height: 12),
Text('Katalysatoren & Stärken ($strategyKey @ €${entryPrice.toStringAsFixed(2)}):', style: TextStyle(color: AppTheme.primaryEmerald, fontSize: 12, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
...aiValidation.keyCatalysts.map((c) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Row(
children: [
Icon(Icons.check_circle, size: 12, color: AppTheme.primaryEmerald),
const SizedBox(width: 6),
Expanded(child: Text(c, style: TextStyle(color: AppTheme.textMuted, fontSize: 12))),
],
),
)),
],
if (aiValidation.identifiedRisks.isNotEmpty) ...[
const SizedBox(height: 12),
const Text('Identifizierte Risiken:', style: TextStyle(color: Colors.amber, fontSize: 12, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
...aiValidation.identifiedRisks.map((r) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Row(
children: [
const Icon(Icons.warning_amber, size: 12, color: Colors.amber),
const SizedBox(width: 6),
Expanded(child: Text(r, style: TextStyle(color: AppTheme.textMuted, fontSize: 12))),
],
),
)),
],
],
],
),
),
const SizedBox(height: 16),
// Knock-Out Derivative & Safety Buffer Card
GlassContainer(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.shield_outlined, size: 18, color: Colors.cyanAccent),
SizedBox(width: 8),
Text('Optimaler Knock-Out Schein', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.white)),
],
),
const SizedBox(height: 12),
if (!hasDerivative)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, size: 16, color: AppTheme.textMuted),
const SizedBox(width: 8),
Expanded(
child: Text(
'Kein passendes Knock-Out Produkt für diesen Vorschlag gefunden.',
style: TextStyle(color: AppTheme.textMuted, fontSize: 13, height: 1.4),
),
),
],
)
else ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildDetailCol('Emittent', deriv.issuer.isNotEmpty ? deriv.issuer : ''),
_buildDetailCol('Hebel', '${deriv.leverage.toStringAsFixed(1)}x'),
_buildDetailCol('KO-Barriere', '${deriv.barrier.toStringAsFixed(2)}'),
_buildDetailCol('Sicherheitspuffer', '${deriv.safetyBufferPercent.toStringAsFixed(1)}%'),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: (deriv.safetyBufferPercent / 20.0).clamp(0.0, 1.0),
backgroundColor: Colors.white10,
valueColor: AlwaysStoppedAnimation<Color>(deriv.safetyBufferPercent >= 5.0 ? AppTheme.primaryEmerald : Colors.amber),
minHeight: 6,
borderRadius: BorderRadius.circular(3),
),
const SizedBox(height: 6),
Text(
'Barriere liegt ${deriv.safetyBufferPercent.toStringAsFixed(1)}% unter dem Chart Stop-Loss (€${invalidationPrice.toStringAsFixed(2)}).',
style: TextStyle(color: AppTheme.textMuted, fontSize: 10),
),
const SizedBox(height: 6),
Text(
deriv.derivativeWkn != null ? 'WKN: ${deriv.derivativeWkn}' : 'WKN: nicht verfügbar',
style: TextStyle(
color: AppTheme.textMuted,
fontSize: 10,
fontStyle: deriv.derivativeWkn != null ? FontStyle.normal : FontStyle.italic,
),
),
],
],
),
),
const SizedBox(height: 24),
// Action Buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: onExecuteBot,
icon: const Icon(Icons.smart_toy_outlined),
label: const Text('An Bot übergeben'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: onManualTrade,
icon: const Icon(Icons.touch_app_outlined),
label: const Text('Manuell eröffnen'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white30),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
),
const SizedBox(height: 32),
],
),
),
);
}
Widget _buildDetailCol(String label, String val) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
const SizedBox(height: 2),
Text(val, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
],
);
}
}