feat(trades): add live execution cockpit, closing cockpit, calculation cards and precision trade settings
This commit is contained in:
@@ -65,6 +65,7 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -84,7 +85,10 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(trade.instrumentType, style: TextStyle(color: AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
child: Text(
|
||||
trade.derivativeIsin.isNotEmpty ? '${trade.instrumentType} (${trade.derivativeIsin})' : trade.instrumentType,
|
||||
style: TextStyle(color: AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -99,7 +103,7 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentRed,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
@@ -125,7 +129,7 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
@@ -135,12 +139,62 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Active Drift Radar Bar
|
||||
if (isActive) ...[
|
||||
const SizedBox(height: 10),
|
||||
_buildDriftRadarBar(trade),
|
||||
],
|
||||
|
||||
// Pending Exit Alert Banner
|
||||
if (isActive && trade.hasPendingExitAlert) ...[
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentRed.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: AppTheme.accentRed, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('KI-Guardian Ratschlag: Position schließen!', style: TextStyle(color: AppTheme.accentRed, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
if (trade.pendingExitReason.isNotEmpty)
|
||||
Text(trade.pendingExitReason, style: const TextStyle(color: Colors.white70, fontSize: 11), maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onClose != null)
|
||||
ElevatedButton(
|
||||
onPressed: onClose,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentRed,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text('Schließen', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${trade.companyName.isNotEmpty ? trade.companyName : defaultSymbol} ($isin) | Timeframe: ${trade.timeframe} | Risk: ${trade.riskTolerance}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Target Price Metrics Grid
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -154,7 +208,11 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTradeStat('Einstiegszone', entryZoneMin > 0 && entryZoneMax > 0 ? '€${_fmt(entryZoneMin)} - €${_fmt(entryZoneMax)}' : '€${_fmt(entryPrice)}', Colors.white),
|
||||
_buildTradeStat('Stop-Loss', '€${_fmt(stopLoss)}', AppTheme.accentRed),
|
||||
_buildTradeStat(
|
||||
trade.driftStatus == DriftStatus.trailingActive ? 'Stop (Trailing)' : 'Stop-Loss',
|
||||
'€${_fmt(stopLoss)}',
|
||||
AppTheme.accentRed,
|
||||
),
|
||||
_buildTradeStat('Take-Profit', takeProfitTargets.isNotEmpty ? takeProfitTargets.map((t) => '€${_fmt(t)}').join(' / ') : '€${_fmt(takeProfit)}', AppTheme.primaryEmerald),
|
||||
],
|
||||
),
|
||||
@@ -171,6 +229,8 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Execution Details if active
|
||||
if (actualEntry > 0 || posSize > 0 || levUsed > 0 || qty > 0) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
@@ -208,6 +268,8 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Realized PnL if closed
|
||||
if (status == 'CLOSED' || trade.pnlAbsolute != 0) ...[
|
||||
const SizedBox(height: 12),
|
||||
Builder(
|
||||
@@ -243,12 +305,72 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
_buildTradeStat('Rendite (%)', '${(pnlPctVal >= 0 ? "+" : "")}${_fmt(pnlPctVal)}%', isWin ? AppTheme.primaryEmerald : AppTheme.accentRed),
|
||||
],
|
||||
),
|
||||
if (trade.closeReason.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text('Grund: ${trade.closeReason}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
// KI Timeline Expansion
|
||||
if (trade.hourlyUpdates.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: const EdgeInsets.only(bottom: 6),
|
||||
dense: true,
|
||||
leading: Icon(Icons.history_toggle_off, color: AppTheme.accentCyan, size: 18),
|
||||
title: Text(
|
||||
'KI-Guardian Verlauf (${trade.hourlyUpdates.length} Prüfungen)',
|
||||
style: TextStyle(color: AppTheme.accentCyan, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
children: trade.hourlyUpdates.reversed.take(4).map((u) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.03),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${u.timestamp.hour.toString().padLeft(2, '0')}:${u.timestamp.minute.toString().padLeft(2, '0')}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: (u.recommendation.toLowerCase().contains('close')
|
||||
? AppTheme.accentRed
|
||||
: (u.recommendation.toLowerCase().contains('adjust') ? Colors.blue : AppTheme.primaryEmerald))
|
||||
.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(u.recommendation, style: const TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
u.reasoning.isNotEmpty ? u.reasoning : 'Kurs: €${u.currentPrice.toStringAsFixed(2)} | VIX: ${u.vixValue.toStringAsFixed(1)}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
|
||||
// AI Analysis Expansion
|
||||
if (reasoning.isNotEmpty || techRationale.isNotEmpty || fundRationale.isNotEmpty || riskWarning.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
ExpansionTile(
|
||||
@@ -279,6 +401,53 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriftRadarBar(TradeModel t) {
|
||||
Color col;
|
||||
String label;
|
||||
IconData icon;
|
||||
|
||||
switch (t.driftStatus) {
|
||||
case DriftStatus.exitAlert:
|
||||
col = AppTheme.accentRed;
|
||||
label = 'Drift-Radar: Ausstieg empfohlen';
|
||||
icon = Icons.warning_rounded;
|
||||
break;
|
||||
case DriftStatus.trailingActive:
|
||||
col = AppTheme.accentCyan;
|
||||
label = 'Drift-Radar: Trailing-Stop aktiv nachgezogen';
|
||||
icon = Icons.security;
|
||||
break;
|
||||
case DriftStatus.driftWarning:
|
||||
col = Colors.orangeAccent;
|
||||
label = 'Drift-Radar: Leichte Abweichung von Prognose';
|
||||
icon = Icons.tune;
|
||||
break;
|
||||
case DriftStatus.onTrack:
|
||||
col = AppTheme.primaryEmerald;
|
||||
label = 'Drift-Radar: Prognose intakt • KI überwacht stündlich';
|
||||
icon = Icons.radar;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: col.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: col.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: col, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(label, style: TextStyle(color: col, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTradeStat(String title, String val, Color col) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -301,3 +470,4 @@ class AssetTradeItemCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ class LiveTradeSettingsDialog {
|
||||
double tempFee = currentSettings.defaultOrderFee;
|
||||
bool tempAuto = currentSettings.autoAcceptSignals;
|
||||
|
||||
final posController = TextEditingController(text: tempPos.toStringAsFixed(0));
|
||||
final levController = TextEditingController(text: tempLev.toStringAsFixed(1));
|
||||
final posController = TextEditingController(text: tempPos == tempPos.roundToDouble() ? tempPos.toInt().toString() : tempPos.toStringAsFixed(2));
|
||||
final levController = TextEditingController(text: tempLev == tempLev.roundToDouble() ? tempLev.toInt().toString() : tempLev.toStringAsFixed(2));
|
||||
final feeController = TextEditingController(text: tempFee.toStringAsFixed(2));
|
||||
|
||||
showDialog(
|
||||
|
||||
+443
-148
@@ -1,174 +1,469 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../models/manual_analysis_request_dto.dart';
|
||||
|
||||
class ManualAnalysisDialog {
|
||||
class ManualAnalysisDialog extends StatefulWidget {
|
||||
final String symbol;
|
||||
final double initialRiskScore;
|
||||
final void Function(ManualAnalysisRequestDto) onTrigger;
|
||||
|
||||
const ManualAnalysisDialog({
|
||||
super.key,
|
||||
required this.symbol,
|
||||
required this.initialRiskScore,
|
||||
required this.onTrigger,
|
||||
});
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String symbol,
|
||||
required double initialRiskScore,
|
||||
required void Function(ManualAnalysisRequestDto) onTrigger,
|
||||
}) {
|
||||
double riskScore = initialRiskScore;
|
||||
final minTimeframeController = TextEditingController(text: '1');
|
||||
final maxTimeframeController = TextEditingController(text: '14');
|
||||
String timeframeUnit = 'Tage';
|
||||
String instrumentType = 'Knock-Out Zertifikat (Turbo)';
|
||||
final notesController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (builderContext, setModalState) {
|
||||
return AlertDialog(
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
),
|
||||
title: Row(
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => ManualAnalysisDialog(
|
||||
symbol: symbol,
|
||||
initialRiskScore: initialRiskScore,
|
||||
onTrigger: onTrigger,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<ManualAnalysisDialog> createState() => _ManualAnalysisDialogState();
|
||||
}
|
||||
|
||||
class _ManualAnalysisDialogState extends State<ManualAnalysisDialog> with SingleTickerProviderStateMixin {
|
||||
late double _riskScore;
|
||||
String _selectedTimeframePreset = 'swing'; // intraday, swing, position, custom
|
||||
final _minTimeframeCtrl = TextEditingController(text: '1');
|
||||
final _maxTimeframeCtrl = TextEditingController(text: '14');
|
||||
String _timeframeUnit = 'Tage';
|
||||
String _instrumentType = 'Knock-Out Zertifikat (Turbo)';
|
||||
final _notesCtrl = TextEditingController();
|
||||
|
||||
bool _isAnalyzing = false;
|
||||
int _analysisStage = 0; // 0: Idle, 1: Marktdaten, 2: TA/FA Indikatoren, 3: KI Setup
|
||||
Timer? _stageTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_riskScore = widget.initialRiskScore;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stageTimer?.cancel();
|
||||
_minTimeframeCtrl.dispose();
|
||||
_maxTimeframeCtrl.dispose();
|
||||
_notesCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _selectTimeframePreset(String key, int min, int max, String unit) {
|
||||
setState(() {
|
||||
_selectedTimeframePreset = key;
|
||||
_minTimeframeCtrl.text = min.toString();
|
||||
_maxTimeframeCtrl.text = max.toString();
|
||||
_timeframeUnit = unit;
|
||||
});
|
||||
}
|
||||
|
||||
void _selectRiskPreset(double score) {
|
||||
setState(() {
|
||||
_riskScore = score;
|
||||
});
|
||||
}
|
||||
|
||||
void _startAnalysis() {
|
||||
setState(() {
|
||||
_isAnalyzing = true;
|
||||
_analysisStage = 1;
|
||||
});
|
||||
|
||||
_stageTimer = Timer.periodic(const Duration(milliseconds: 700), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
if (_analysisStage < 3) {
|
||||
setState(() {
|
||||
_analysisStage++;
|
||||
});
|
||||
} else {
|
||||
timer.cancel();
|
||||
final payload = ManualAnalysisRequestDto(
|
||||
isin: widget.symbol,
|
||||
symbol: widget.symbol,
|
||||
riskScore: _riskScore.toInt(),
|
||||
minTimeframeValue: int.tryParse(_minTimeframeCtrl.text) ?? 1,
|
||||
maxTimeframeValue: int.tryParse(_maxTimeframeCtrl.text) ?? 14,
|
||||
timeframeUnit: _timeframeUnit,
|
||||
instrumentType: _instrumentType,
|
||||
userNotes: _notesCtrl.text,
|
||||
headline: 'Manuelle KI-Analyse für ${widget.symbol}',
|
||||
);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
widget.onTrigger(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String get _stageText {
|
||||
switch (_analysisStage) {
|
||||
case 1:
|
||||
return 'Lade Live-Marktdaten & Orderbuch...';
|
||||
case 2:
|
||||
return 'Berechne Technische Indikatoren & Muster...';
|
||||
case 3:
|
||||
return 'KI generiert optimales Trade-Setup...';
|
||||
default:
|
||||
return 'Analyse Jetzt Ausführen';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final riskColor = _riskScore < 35
|
||||
? AppTheme.primaryEmerald
|
||||
: (_riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed);
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
child: Container(
|
||||
width: 560,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardSurface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
blurRadius: 30,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 16, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_awesome, color: AppTheme.accentCyan, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text('KI-Analyse für $symbol', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentCyan.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(Icons.auto_awesome, color: AppTheme.accentCyan, size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'KI-Trade Setup Generator',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'Asset: ${widget.symbol}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!_isAnalyzing)
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close, color: Colors.white54),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 440,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Wählen Sie Ihre Zielparameter für die Trade-Evaluierung:', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Zeithorizont (Timeframe):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: minTimeframeController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Von', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const Divider(color: Colors.white12, height: 1),
|
||||
|
||||
// Content
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. TIMEFRAME PRESETS
|
||||
const Text('1. ZEITHORIZONT (TIMEFRAME)', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_presetChip('⚡ Intraday (1–4 Std.)', 'intraday', () => _selectTimeframePreset('intraday', 1, 4, 'Stunden')),
|
||||
_presetChip('🌊 Swing-Trade (1–14 Tage)', 'swing', () => _selectTimeframePreset('swing', 1, 14, 'Tage')),
|
||||
_presetChip('📈 Positions-Trade (2–8 Wo.)', 'position', () => _selectTimeframePreset('position', 2, 8, 'Wochen')),
|
||||
_presetChip('⚙ Benutzerdefiniert', 'custom', () => setState(() => _selectedTimeframePreset = 'custom')),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (_selectedTimeframePreset == 'custom') ...[
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _minTimeframeCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Von', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: maxTimeframeController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Bis', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: timeframeUnit,
|
||||
dropdownColor: AppTheme.cardSurface,
|
||||
decoration: const InputDecoration(labelText: 'Einheit', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Stunden', child: Text('Stunden')),
|
||||
DropdownMenuItem(value: 'Tage', child: Text('Tage')),
|
||||
DropdownMenuItem(value: 'Wochen', child: Text('Wochen')),
|
||||
DropdownMenuItem(value: 'Monate', child: Text('Monate')),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) setModalState(() => timeframeUnit = val);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Risikobereitschaft:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
Text(
|
||||
'${riskScore.toInt()}/100 (${riskScore < 30 ? "Konservativ" : (riskScore < 70 ? "Ausgewogen" : "Spekulativ")})',
|
||||
style: TextStyle(
|
||||
color: riskScore < 30 ? AppTheme.primaryEmerald : (riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: riskScore,
|
||||
min: 0,
|
||||
max: 100,
|
||||
divisions: 100,
|
||||
activeColor: riskScore < 30 ? AppTheme.primaryEmerald : (riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed),
|
||||
inactiveColor: AppTheme.glassSurface,
|
||||
onChanged: (val) => setModalState(() => riskScore = val),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Instrumententyp (Trade Republic):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
const SizedBox(height: 6),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: instrumentType,
|
||||
dropdownColor: AppTheme.cardSurface,
|
||||
decoration: const InputDecoration(contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10)),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Aktie / ETF (Direktinvestment)', child: Text('Aktie / ETF (Direktinvestment)')),
|
||||
DropdownMenuItem(value: 'Optionsschein (Warrant)', child: Text('Optionsschein (Warrant)')),
|
||||
DropdownMenuItem(value: 'Knock-Out Zertifikat (Turbo)', child: Text('Knock-Out Zertifikat (Turbo)')),
|
||||
DropdownMenuItem(value: 'Faktor-Zertifikat', child: Text('Faktor-Zertifikat')),
|
||||
DropdownMenuItem(value: 'Krypto (Crypto)', child: Text('Krypto (Crypto)')),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) setModalState(() => instrumentType = val);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Anmerkung für die KI:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: notesController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Z.B. Besonderes Augenmerk auf Hebelprodukte legen, enge Stopps berücksichtigen...',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _maxTimeframeCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Bis', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: _timeframeUnit,
|
||||
dropdownColor: AppTheme.cardSurface,
|
||||
decoration: const InputDecoration(labelText: 'Einheit', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Stunden', child: Text('Stunden')),
|
||||
DropdownMenuItem(value: 'Tage', child: Text('Tage')),
|
||||
DropdownMenuItem(value: 'Wochen', child: Text('Wochen')),
|
||||
DropdownMenuItem(value: 'Monate', child: Text('Monate')),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) setState(() => _timeframeUnit = val);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// 2. RISIKO-PROFIL
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('2. RISIKOBEREITSCHAFT', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
|
||||
Text(
|
||||
'${_riskScore.toInt()}/100 (${_riskScore < 35 ? "Konservativ" : (_riskScore < 70 ? "Ausgewogen" : "Spekulativ")})',
|
||||
style: TextStyle(color: riskColor, fontWeight: FontWeight.bold, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
_riskProfileChip('🟢 Konservativ', 25, AppTheme.primaryEmerald),
|
||||
_riskProfileChip('🟡 Ausgewogen', 50, Colors.orangeAccent),
|
||||
_riskProfileChip('🔴 Spekulativ', 85, AppTheme.accentRed),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _riskScore,
|
||||
min: 0,
|
||||
max: 100,
|
||||
divisions: 100,
|
||||
activeColor: riskColor,
|
||||
inactiveColor: AppTheme.glassSurface,
|
||||
onChanged: _isAnalyzing ? null : (val) => setState(() => _riskScore = val),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 3. INSTRUMENT
|
||||
const Text('3. FINANZINSTRUMENT', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _instrumentType,
|
||||
dropdownColor: AppTheme.cardSurface,
|
||||
decoration: const InputDecoration(contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10)),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Knock-Out Zertifikat (Turbo)', child: Text('Knock-Out Zertifikat (Turbo) - Hebel')),
|
||||
DropdownMenuItem(value: 'Aktie / ETF (Direktinvestment)', child: Text('Aktie / ETF (Direktinvestment)')),
|
||||
DropdownMenuItem(value: 'Optionsschein (Warrant)', child: Text('Optionsschein (Warrant)')),
|
||||
DropdownMenuItem(value: 'Faktor-Zertifikat', child: Text('Faktor-Zertifikat')),
|
||||
DropdownMenuItem(value: 'Krypto (Crypto)', child: Text('Krypto (Crypto)')),
|
||||
],
|
||||
onChanged: _isAnalyzing ? null : (val) {
|
||||
if (val != null) setState(() => _instrumentType = val);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 4. NOTIZEN & QUICK TAGS
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('4. ANWEISUNG FÜR DIE KI (OPTIONAL)', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
children: [
|
||||
_hintTag('Enge Stops'),
|
||||
_hintTag('Hoher Hebel'),
|
||||
_hintTag('Earnings Play'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: _notesCtrl,
|
||||
maxLines: 2,
|
||||
enabled: !_isAnalyzing,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Z.B. Besonderes Augenmerk auf Hebelprodukte legen, enge Stopps berücksichtigen...',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Animated Footer Button
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isAnalyzing ? null : _startAnalysis,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentCyan,
|
||||
foregroundColor: Colors.black,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
elevation: _isAnalyzing ? 0 : 4,
|
||||
),
|
||||
child: _isAnalyzing
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.black),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
_stageText,
|
||||
key: ValueKey<int>(_analysisStage),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.flash_on, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Analyse Jetzt Ausführen', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 15)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final payload = ManualAnalysisRequestDto(
|
||||
isin: symbol,
|
||||
symbol: symbol,
|
||||
riskScore: riskScore.toInt(),
|
||||
minTimeframeValue: int.tryParse(minTimeframeController.text) ?? 1,
|
||||
maxTimeframeValue: int.tryParse(maxTimeframeController.text) ?? 14,
|
||||
timeframeUnit: timeframeUnit,
|
||||
instrumentType: instrumentType,
|
||||
userNotes: notesController.text,
|
||||
headline: 'Manuelle KI-Analyse für $symbol',
|
||||
);
|
||||
Navigator.pop(dialogContext);
|
||||
onTrigger(payload);
|
||||
},
|
||||
icon: const Icon(Icons.flash_on),
|
||||
label: const Text('Analyse Jetzt Ausführen'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _presetChip(String label, String key, VoidCallback onTap) {
|
||||
final isSelected = _selectedTimeframePreset == key;
|
||||
return GestureDetector(
|
||||
onTap: _isAnalyzing ? null : onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppTheme.accentCyan.withValues(alpha: 0.2) : AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: isSelected ? AppTheme.accentCyan : AppTheme.glassBorder),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppTheme.accentCyan : Colors.white70,
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _riskProfileChip(String label, double score, Color color) {
|
||||
final isSelected = (_riskScore - score).abs() < 15;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: _isAnalyzing ? null : () => _selectRiskPreset(score),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color.withValues(alpha: 0.2) : AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: isSelected ? color : AppTheme.glassBorder),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? color : Colors.white70,
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _hintTag(String text) {
|
||||
return GestureDetector(
|
||||
onTap: _isAnalyzing
|
||||
? null
|
||||
: () {
|
||||
if (!_notesCtrl.text.contains(text)) {
|
||||
_notesCtrl.text = _notesCtrl.text.isEmpty ? text : '${_notesCtrl.text}, $text';
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white10,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'+ $text',
|
||||
style: TextStyle(color: AppTheme.accentCyan, fontSize: 10, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user