420 lines
21 KiB
Dart
420 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:finlytic_app/core/theme/app_theme.dart';
|
|
import 'package:finlytic_app/core/widgets/status_badge.dart';
|
|
import '../../../../features/trades/models/trade_model.dart';
|
|
import '../../../../features/trades/models/trade_acceptance_dto.dart';
|
|
|
|
class TradeExecutionDialog {
|
|
static const double _defaultPositionSize = 1000.0;
|
|
static const double _defaultLeverage = 1.0;
|
|
|
|
static void show(
|
|
BuildContext context, {
|
|
required TradeModel trade,
|
|
required String defaultSymbol,
|
|
bool isActive = false,
|
|
required Function(TradeAcceptanceDto dto) onAccept,
|
|
Function(String tradeId)? onReject,
|
|
}) {
|
|
final initEntry = trade.actualEntryPrice > 0 ? trade.actualEntryPrice : (trade.entryPrice > 0 ? trade.entryPrice : 100.0);
|
|
final initPos = trade.positionSize > 0 ? trade.positionSize : _defaultPositionSize;
|
|
final initLev = trade.leverageUsed > 0 ? trade.leverageUsed : _defaultLeverage;
|
|
final calcQty = (initEntry > 0 && initPos > 0) ? (initPos * initLev) / initEntry : 10.0;
|
|
|
|
// We don't have a direct quantity field in TradeModel, but we calculate it.
|
|
// Let's use the explicit quantity if it exists, otherwise calculate it
|
|
final initQty = trade.quantity > 0 ? trade.quantity : calcQty;
|
|
|
|
final actualEntryController = TextEditingController(text: initEntry.toStringAsFixed(2));
|
|
final positionSizeController = TextEditingController(text: initPos.toStringAsFixed(2));
|
|
final leverageController = TextEditingController(text: initLev.toStringAsFixed(1));
|
|
final quantityController = TextEditingController(text: initQty.toStringAsFixed(4));
|
|
|
|
final entryFeeController = TextEditingController(text: trade.entryFee.toStringAsFixed(2));
|
|
|
|
final exitFeeController = TextEditingController(text: trade.exitFee.toStringAsFixed(2));
|
|
|
|
final slController = TextEditingController(text: trade.stopLoss.toString());
|
|
final tpController = TextEditingController(text: trade.takeProfit.toString());
|
|
|
|
void recalculateQuantity() {
|
|
final entry = double.tryParse(actualEntryController.text) ?? 0.0;
|
|
final posSize = double.tryParse(positionSizeController.text) ?? 0.0;
|
|
final lev = double.tryParse(leverageController.text) ?? 1.0;
|
|
if (entry > 0 && posSize > 0) {
|
|
final q = (posSize * lev) / entry;
|
|
quantityController.text = q.toStringAsFixed(4);
|
|
}
|
|
}
|
|
|
|
actualEntryController.addListener(recalculateQuantity);
|
|
positionSizeController.addListener(recalculateQuantity);
|
|
leverageController.addListener(recalculateQuantity);
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
return AlertDialog(
|
|
backgroundColor: AppTheme.cardSurface,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
side: BorderSide(color: AppTheme.glassBorder),
|
|
),
|
|
title: Row(
|
|
children: [
|
|
Icon(isActive ? Icons.tune : Icons.edit_note_outlined, color: AppTheme.primaryEmerald, size: 22),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(isActive ? 'Einstellungen für Trade #${trade.id}' : 'Trade-Ausführung & Parameter', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
),
|
|
content: SizedBox(
|
|
width: 580,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Trade-ID: ${trade.id} | Symbol/ISIN: ${trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol}', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
|
const SizedBox(height: 12),
|
|
|
|
Builder(
|
|
builder: (context) {
|
|
final signal = trade.signalType.toUpperCase();
|
|
final isLong = signal == 'BUY' || signal == 'LONG';
|
|
final signalColor = isLong ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
|
|
|
final entryZoneMin = trade.entryZoneMin;
|
|
final entryZoneMax = trade.entryZoneMax;
|
|
final entryPrice = trade.entryPrice;
|
|
final stopLoss = trade.stopLoss;
|
|
final takeProfit = trade.takeProfit;
|
|
final takeProfitTargets = trade.takeProfitTargets;
|
|
final crv = (takeProfit - entryPrice) / (entryPrice - stopLoss).abs();
|
|
final maxLeverage = trade.maxLeverage;
|
|
|
|
final reasoning = trade.reasoning;
|
|
final techRationale = trade.technicalRationale;
|
|
final fundRationale = trade.fundamentalRationale;
|
|
final riskWarning = trade.riskWarning;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: signalColor.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: signalColor, width: 1.5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
StatusBadge(label: isLong ? 'LONG / KAUFEN' : 'SHORT / VERKAUFEN', color: signalColor),
|
|
const SizedBox(width: 8),
|
|
if (trade.instrumentType.isNotEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.glassSurface,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(trade.instrumentType.toString(), style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
children: [
|
|
if (trade.winRate > 0) ...[
|
|
Icon(Icons.bolt, size: 14, color: AppTheme.accentCyan),
|
|
Text('Win-Rate: ${trade.winRate}%', style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 12)),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Haltedauer: ${trade.timeframe.isNotEmpty ? trade.timeframe : '1-14 Tage'}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
|
Text('Risiko: ${trade.riskTolerance}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
|
if (trade.vixValue > 0)
|
|
Text('VIX: ${_fmt(trade.vixValue)} (${trade.vixRegime})', style: TextStyle(color: Colors.orangeAccent, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
const Divider(color: Colors.white12, height: 16),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_buildTradeStat('Einstiegszone', entryZoneMin > 0 && entryZoneMax > 0 ? '€${_fmt(entryZoneMin)} - €${_fmt(entryZoneMax)}' : '€${_fmt(entryPrice)}', Colors.white),
|
|
_buildTradeStat('Stop-Loss Target', '€${_fmt(stopLoss)}', AppTheme.accentRed),
|
|
_buildTradeStat('Take-Profit Target', takeProfitTargets.isNotEmpty ? takeProfitTargets.map((t) => '€${_fmt(t)}').join(' / ') : '€${_fmt(takeProfit)}', AppTheme.primaryEmerald),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
if (crv > 0) _buildTradeStat('Chance-Risiko-Verh. (CRV)', '${_fmt(crv)}x', AppTheme.accentCyan),
|
|
if (maxLeverage > 0) _buildTradeStat('Empf. Max Hebel', '${_fmt(maxLeverage)}x', Colors.orangeAccent),
|
|
_buildTradeStat('Signal Typ', isLong ? 'LONG / BULLISH' : 'SHORT / BEARISH', signalColor),
|
|
],
|
|
),
|
|
|
|
if (reasoning.isNotEmpty || techRationale.isNotEmpty || fundRationale.isNotEmpty || riskWarning.isNotEmpty) ...[
|
|
const SizedBox(height: 10),
|
|
ExpansionTile(
|
|
tilePadding: EdgeInsets.zero,
|
|
childrenPadding: EdgeInsets.zero,
|
|
dense: true,
|
|
title: Text('Ausführliche KI-Analyse & Begründung anzeigen', style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 12)),
|
|
children: [
|
|
if (reasoning.isNotEmpty) ...[
|
|
_buildRationaleBlock('Gesamt-Strategie & KI-Entscheidung', reasoning, Colors.white70),
|
|
const SizedBox(height: 6),
|
|
],
|
|
if (techRationale.isNotEmpty) ...[
|
|
_buildRationaleBlock('Technische Analyse', techRationale, Colors.white70),
|
|
const SizedBox(height: 6),
|
|
],
|
|
if (fundRationale.isNotEmpty) ...[
|
|
_buildRationaleBlock('Fundamentale Analyse', fundRationale, Colors.white70),
|
|
const SizedBox(height: 6),
|
|
],
|
|
if (riskWarning.isNotEmpty)
|
|
_buildRationaleBlock('Risikowarnung', riskWarning, AppTheme.accentRed),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text('Ihre Ausführungsdaten für das Depot:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)),
|
|
const SizedBox(height: 10),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: actualEntryController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Tatsächlicher Einstiegskurs (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: positionSizeController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Investitionsvolumen (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: leverageController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Genutzter Hebel (x)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: quantityController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Stückzahl (Autom. berechnet)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: entryFeeController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Einstiegsgebühr (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: exitFeeController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Ausstiegsgebühr (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: slController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Stop-Loss (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: tpController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(labelText: 'Take-Profit (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
|
|
),
|
|
if (isActive)
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
final dto = TradeAcceptanceDto(
|
|
userId: trade.userId,
|
|
tradeId: trade.id,
|
|
analysisId: trade.analysisId,
|
|
isin: trade.isin,
|
|
symbol: trade.symbol,
|
|
actualEntryPrice: double.tryParse(actualEntryController.text) ?? trade.entryPrice,
|
|
positionSize: double.tryParse(positionSizeController.text) ?? 1000.0,
|
|
leverageUsed: double.tryParse(leverageController.text) ?? 1.0,
|
|
entryFee: double.tryParse(entryFeeController.text) ?? 0.0,
|
|
exitFee: double.tryParse(exitFeeController.text) ?? 0.0,
|
|
quantity: double.tryParse(quantityController.text) ?? 0.0,
|
|
executionTimestamp: DateTime.now().toUtc(),
|
|
signalType: trade.signalType,
|
|
entryPrice: trade.entryPrice,
|
|
stopLoss: double.tryParse(slController.text) ?? trade.stopLoss,
|
|
takeProfit: double.tryParse(tpController.text) ?? trade.takeProfit,
|
|
instrumentType: trade.instrumentType,
|
|
timeframe: trade.timeframe,
|
|
reasoning: trade.reasoning,
|
|
);
|
|
onAccept(dto);
|
|
Navigator.pop(dialogContext);
|
|
},
|
|
icon: const Icon(Icons.save, size: 16),
|
|
label: const Text('Speichern'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.primaryEmerald,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
)
|
|
else ...[
|
|
if (onReject != null)
|
|
OutlinedButton.icon(
|
|
onPressed: () {
|
|
onReject(trade.id);
|
|
Navigator.pop(dialogContext);
|
|
},
|
|
icon: Icon(Icons.cancel, color: AppTheme.accentRed, size: 16),
|
|
label: Text('Trade Ablehnen', style: TextStyle(color: AppTheme.accentRed)),
|
|
style: OutlinedButton.styleFrom(
|
|
side: BorderSide(color: AppTheme.accentRed),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
final dto = TradeAcceptanceDto(
|
|
userId: trade.userId,
|
|
tradeId: trade.id,
|
|
analysisId: trade.analysisId,
|
|
isin: trade.isin,
|
|
symbol: trade.symbol,
|
|
actualEntryPrice: double.tryParse(actualEntryController.text) ?? trade.entryPrice,
|
|
positionSize: double.tryParse(positionSizeController.text) ?? 1000.0,
|
|
leverageUsed: double.tryParse(leverageController.text) ?? 1.0,
|
|
entryFee: double.tryParse(entryFeeController.text) ?? 0.0,
|
|
exitFee: double.tryParse(exitFeeController.text) ?? 0.0,
|
|
quantity: double.tryParse(quantityController.text) ?? 0.0,
|
|
executionTimestamp: DateTime.now().toUtc(),
|
|
signalType: trade.signalType,
|
|
entryPrice: trade.entryPrice,
|
|
stopLoss: double.tryParse(slController.text) ?? trade.stopLoss,
|
|
takeProfit: double.tryParse(tpController.text) ?? trade.takeProfit,
|
|
instrumentType: trade.instrumentType,
|
|
timeframe: trade.timeframe,
|
|
reasoning: trade.reasoning,
|
|
);
|
|
onAccept(dto);
|
|
Navigator.of(dialogContext).pop();
|
|
},
|
|
icon: const Icon(Icons.check_circle, size: 16),
|
|
label: const Text('Trade Annehmen & Ausführen'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.primaryEmerald,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
|
|
static String _fmt(dynamic val) {
|
|
if (val == null) return '0.00';
|
|
if (val is double) {
|
|
if (val > 100) return val.toStringAsFixed(1);
|
|
return val.toStringAsFixed(2);
|
|
}
|
|
return val.toString();
|
|
}
|
|
|
|
static Widget _buildTradeStat(String label, String value, Color color) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: TextStyle(color: Colors.white54, fontSize: 11)),
|
|
const SizedBox(height: 2),
|
|
Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13)),
|
|
],
|
|
);
|
|
}
|
|
|
|
static Widget _buildRationaleBlock(String title, String content, Color color) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 4),
|
|
Text(content, style: const TextStyle(color: Colors.white70, fontSize: 12, height: 1.4)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|