Files
Finlytic/FinlyticApp/lib/features/trades/widgets/trade_execution_cockpit.dart
T

1021 lines
49 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/status_badge.dart';
import '../../asset_detail/repositories/asset_repository.dart';
import '../models/trade_model.dart';
import '../models/trade_acceptance_dto.dart';
import '../models/derivative_item_model.dart';
import 'derivative_picker_modal.dart';
import 'trade_execution_ai_plan_card.dart';
class TradeExecutionCockpit extends StatefulWidget {
final TradeModel trade;
final String defaultSymbol;
final bool isActive;
final Function(TradeAcceptanceDto dto) onAccept;
final Function(String tradeId)? onReject;
const TradeExecutionCockpit({
super.key,
required this.trade,
required this.defaultSymbol,
this.isActive = false,
required this.onAccept,
this.onReject,
});
static Future<void> show(
BuildContext context, {
required TradeModel trade,
required String defaultSymbol,
bool isActive = false,
required Function(TradeAcceptanceDto dto) onAccept,
Function(String tradeId)? onReject,
}) {
return showDialog(
context: context,
barrierDismissible: true,
builder: (dialogContext) => TradeExecutionCockpit(
trade: trade,
defaultSymbol: defaultSymbol,
isActive: isActive,
onAccept: onAccept,
onReject: onReject,
),
);
}
@override
State<TradeExecutionCockpit> createState() => _TradeExecutionCockpitState();
}
class _InstrumentOption {
final String label;
final String value;
const _InstrumentOption(this.label, this.value);
}
class _TradeExecutionCockpitState extends State<TradeExecutionCockpit> {
late TextEditingController _entryPriceCtrl;
late TextEditingController _positionSizeCtrl;
late TextEditingController _leverageCtrl;
late TextEditingController _stopLossCtrl;
late TextEditingController _takeProfitCtrl;
late TextEditingController _derivativeIsinCtrl;
late TextEditingController _entryFeeCtrl;
late TextEditingController _exitFeeCtrl;
late String _selectedInstrument;
DerivativeItemModel? _selectedDerivative;
double? _derivativePrice;
bool _isFetchingDerivativePrice = false;
List<_InstrumentOption> get _availableInstruments {
final assetType = widget.trade.assetType.toLowerCase();
final categories = widget.trade.derivativeProductCategories;
final hasCfd = widget.trade.hasCfd;
final list = <_InstrumentOption>[];
if (assetType == 'crypto') {
list.add(const _InstrumentOption('Krypto (Spot)', 'Crypto'));
if (hasCfd) {
list.add(const _InstrumentOption('Krypto CFD', 'CFD'));
}
} else if (assetType == 'etf') {
list.add(const _InstrumentOption('ETF (Spot)', 'Stock'));
if (categories.isEmpty || categories.contains('knockOutProduct')) {
list.add(const _InstrumentOption('⚡ Knock-Out (Turbo)', 'KnockOut'));
}
if (categories.contains('vanillaWarrant')) {
list.add(const _InstrumentOption('Optionsschein', 'Option'));
}
if (categories.contains('factorCertificate')) {
list.add(const _InstrumentOption('Faktor-Zertifikat', 'Factor'));
}
if (hasCfd) {
list.add(const _InstrumentOption('CFD', 'CFD'));
}
} else {
// Default: Stock or other
list.add(const _InstrumentOption('Aktie (Spot)', 'Stock'));
if (categories.isEmpty || categories.contains('knockOutProduct')) {
list.add(const _InstrumentOption('⚡ Knock-Out (Turbo)', 'KnockOut'));
}
if (categories.contains('vanillaWarrant')) {
list.add(const _InstrumentOption('Optionsschein', 'Option'));
}
if (categories.contains('factorCertificate')) {
list.add(const _InstrumentOption('Faktor-Zertifikat', 'Factor'));
}
if (hasCfd) {
list.add(const _InstrumentOption('CFD', 'CFD'));
}
}
return list;
}
String _formatCurrencyNum(double val) {
if (val <= 0) return '0';
if (val == val.roundToDouble()) {
return val.toInt().toString();
}
var s = val.toStringAsFixed(2);
if (s.endsWith('0')) {
s = s.substring(0, s.length - 1);
}
return s.replaceAll('.', ',');
}
String _formatLeverage(double lev) {
if (lev <= 0) return '1x';
if (lev == lev.roundToDouble()) {
return '${lev.toInt()}x';
}
var s = lev.toStringAsFixed(2);
if (s.endsWith('0')) {
s = s.substring(0, s.length - 1);
}
return '${s.replaceAll('.', ',')}x';
}
String _formatLeverageNum(double lev) {
if (lev <= 0) return '1';
if (lev == lev.roundToDouble()) {
return lev.toInt().toString();
}
var s = lev.toStringAsFixed(2);
if (s.endsWith('0')) {
s = s.substring(0, s.length - 1);
}
return s.replaceAll('.', ',');
}
@override
void initState() {
super.initState();
final initEntry = widget.trade.actualEntryPrice > 0 ? widget.trade.actualEntryPrice : (widget.trade.entryPrice > 0 ? widget.trade.entryPrice : 100.0);
final initPos = widget.trade.positionSize > 0 ? widget.trade.positionSize : 1000.0;
final initLev = widget.trade.leverageUsed > 0 ? widget.trade.leverageUsed : (widget.trade.maxLeverage > 0 ? widget.trade.maxLeverage : 1.0);
_entryPriceCtrl = TextEditingController(text: initEntry.toStringAsFixed(2));
_positionSizeCtrl = TextEditingController(text: _formatCurrencyNum(initPos));
_leverageCtrl = TextEditingController(text: _formatLeverageNum(initLev));
_stopLossCtrl = TextEditingController(text: widget.trade.stopLoss > 0 ? widget.trade.stopLoss.toStringAsFixed(2) : '');
_takeProfitCtrl = TextEditingController(text: widget.trade.takeProfit > 0 ? widget.trade.takeProfit.toStringAsFixed(2) : '');
_derivativeIsinCtrl = TextEditingController(text: widget.trade.derivativeIsin);
_entryFeeCtrl = TextEditingController(text: widget.trade.entryFee >= 0 ? _formatCurrencyNum(widget.trade.entryFee > 0 ? widget.trade.entryFee : 1.0) : '1.00');
_exitFeeCtrl = TextEditingController(text: widget.trade.exitFee >= 0 ? _formatCurrencyNum(widget.trade.exitFee > 0 ? widget.trade.exitFee : 1.0) : '1.00');
final available = _availableInstruments;
final normalized = _normalizeInstrumentType(widget.trade.instrumentType);
if (available.any((opt) => opt.value == normalized)) {
_selectedInstrument = normalized;
} else {
_selectedInstrument = available.isNotEmpty ? available.first.value : 'Stock';
}
if (widget.trade.derivativeIsin.isNotEmpty) {
_fetchLiveDerivativePrice(widget.trade.derivativeIsin);
}
_entryPriceCtrl.addListener(() => setState(() {}));
_positionSizeCtrl.addListener(() => setState(() {}));
_leverageCtrl.addListener(() => setState(() {}));
_stopLossCtrl.addListener(() => setState(() {}));
_takeProfitCtrl.addListener(() => setState(() {}));
_entryFeeCtrl.addListener(() => setState(() {}));
_exitFeeCtrl.addListener(() => setState(() {}));
}
@override
void dispose() {
_entryPriceCtrl.dispose();
_positionSizeCtrl.dispose();
_leverageCtrl.dispose();
_stopLossCtrl.dispose();
_takeProfitCtrl.dispose();
_derivativeIsinCtrl.dispose();
_entryFeeCtrl.dispose();
_exitFeeCtrl.dispose();
super.dispose();
}
String _normalizeInstrumentType(String raw) {
final clean = raw.toLowerCase().trim();
if (clean.contains('knock') || clean.contains('zertifikat') || clean.contains('turbo')) return 'KnockOut';
if (clean.contains('factor') || clean.contains('faktor')) return 'Factor';
if (clean.contains('option') || clean.contains('warrant')) return 'Option';
if (clean.contains('cfd')) return 'CFD';
if (clean.contains('crypto') || clean.contains('krypto')) return 'Crypto';
return 'Stock';
}
double _parse(TextEditingController ctrl, double fallback) {
final clean = ctrl.text.replaceAll(',', '.').trim();
return double.tryParse(clean) ?? fallback;
}
double get _entryPrice => _parse(_entryPriceCtrl, widget.trade.entryPrice > 0 ? widget.trade.entryPrice : 1.0);
double get _positionSize => _parse(_positionSizeCtrl, 1000.0);
double get _leverage => _parse(_leverageCtrl, 1.0);
double get _stopLoss => _parse(_stopLossCtrl, widget.trade.stopLoss);
double get _takeProfit => _parse(_takeProfitCtrl, widget.trade.takeProfit);
double get _entryFee => _parse(_entryFeeCtrl, 1.0);
double get _exitFee => _parse(_exitFeeCtrl, 1.0);
double get _totalFees => _entryFee + _exitFee;
double get _effectiveLeverage {
final isLeveraged = _selectedInstrument == 'KnockOut' ||
_selectedInstrument == 'Option' ||
_selectedInstrument == 'Factor' ||
_selectedInstrument == 'CFD';
return isLeveraged && _leverage > 0 ? _leverage : 1.0;
}
double get _quantity => (_entryPrice > 0 && _positionSize > 0) ? (_positionSize / _entryPrice) : 0.0;
int get _derivativeQuantity => (_derivativePrice != null && _derivativePrice! > 0 && _positionSize > 0) ? (_positionSize / _derivativePrice!).floor() : 0;
double get _riskAmountAbs {
if (_entryPrice <= 0 || _stopLoss <= 0) return 0.0;
final isShort = widget.trade.signalType.toUpperCase() == 'SELL' || widget.trade.signalType.toUpperCase() == 'SHORT';
final movePct = isShort ? ((_stopLoss - _entryPrice) / _entryPrice) : ((_entryPrice - _stopLoss) / _entryPrice);
// Leveraged loss on position
final rawLoss = (movePct * _positionSize * _effectiveLeverage).abs();
// Knock-Out and options cannot lose more than the invested position capital (Totalverlust-Kappung)
final isDerivative = _selectedInstrument == 'KnockOut' || _selectedInstrument == 'Option' || _selectedInstrument == 'Factor';
final cappedLoss = isDerivative ? rawLoss.clamp(0.0, _positionSize) : rawLoss;
return cappedLoss + _totalFees;
}
double get _rewardAmountAbs {
if (_entryPrice <= 0 || _takeProfit <= 0) return 0.0;
final isShort = widget.trade.signalType.toUpperCase() == 'SELL' || widget.trade.signalType.toUpperCase() == 'SHORT';
final movePct = isShort ? ((_entryPrice - _takeProfit) / _entryPrice) : ((_takeProfit - _entryPrice) / _entryPrice);
final rawProfit = (movePct * _positionSize * _effectiveLeverage);
final profitAfterFees = rawProfit - _totalFees;
return profitAfterFees > 0 ? profitAfterFees : 0.0;
}
double get _crv {
if (_riskAmountAbs <= 0 || _rewardAmountAbs <= 0) return 0.0;
return _rewardAmountAbs / _riskAmountAbs;
}
List<double> get _availableTpTargets {
if (widget.trade.takeProfitTargets.isNotEmpty) {
return widget.trade.takeProfitTargets;
}
if (widget.trade.takeProfit > 0) {
return [widget.trade.takeProfit];
}
return [];
}
double _calculateRewardForTarget(double targetPrice) {
if (_entryPrice <= 0 || targetPrice <= 0) return 0.0;
final isShort = widget.trade.signalType.toUpperCase() == 'SELL' || widget.trade.signalType.toUpperCase() == 'SHORT';
final movePct = isShort ? ((_entryPrice - targetPrice) / _entryPrice) : ((targetPrice - _entryPrice) / _entryPrice);
final rawProfit = (movePct * _positionSize * _effectiveLeverage);
final profitAfterFees = rawProfit - _totalFees;
return profitAfterFees > 0 ? profitAfterFees : 0.0;
}
double _calculateCrvForTarget(double targetPrice) {
final reward = _calculateRewardForTarget(targetPrice);
if (_riskAmountAbs <= 0 || reward <= 0) return 0.0;
return reward / _riskAmountAbs;
}
Future<void> _openDerivativeFinder() async {
final isinVal = widget.trade.isin.isNotEmpty ? widget.trade.isin : widget.defaultSymbol;
final symVal = widget.trade.symbol.isNotEmpty ? widget.trade.symbol : widget.defaultSymbol;
final nameVal = widget.trade.companyName.isNotEmpty ? widget.trade.companyName : symVal;
final priceVal = _entryPrice > 0 ? _entryPrice : (widget.trade.currentPrice > 0 ? widget.trade.currentPrice : widget.trade.entryPrice);
final selected = await DerivativePickerModal.show(
context,
underlyingIsin: isinVal,
underlyingSymbol: symVal,
underlyingName: nameVal,
initialSignalType: widget.trade.signalType,
currentUnderlyingPrice: priceVal,
);
if (selected != null) {
setState(() {
_selectedDerivative = selected;
_derivativeIsinCtrl.text = selected.isin;
_selectedInstrument = 'KnockOut';
if (selected.leverage > 0) {
_leverageCtrl.text = _formatLeverageNum(selected.leverage);
}
});
_fetchLiveDerivativePrice(selected.isin);
}
}
Future<void> _fetchLiveDerivativePrice(String isin) async {
if (isin.trim().isEmpty) return;
setState(() => _isFetchingDerivativePrice = true);
try {
final apiClient = context.read<ApiClient>();
final assetRepo = AssetRepository(apiClient: apiClient);
final technicals = await assetRepo.getAssetTechnical(isin.trim().toUpperCase(), true);
double? fetchedPrice;
if (technicals != null) {
if (technicals.candles.isNotEmpty) {
fetchedPrice = technicals.candles.last.close;
} else if (technicals.currentPrice != null && technicals.currentPrice! > 0) {
fetchedPrice = technicals.currentPrice;
}
}
if (fetchedPrice != null && fetchedPrice > 0 && mounted) {
setState(() {
_derivativePrice = fetchedPrice;
});
}
} catch (_) {
} finally {
if (mounted) setState(() => _isFetchingDerivativePrice = false);
}
}
TradeAcceptanceDto _buildDto() {
final isinVal = widget.trade.isin.isNotEmpty ? widget.trade.isin : (widget.trade.symbol.isNotEmpty ? widget.trade.symbol : widget.defaultSymbol);
final symbolVal = widget.trade.symbol.isNotEmpty ? widget.trade.symbol : widget.defaultSymbol;
return TradeAcceptanceDto(
userId: widget.trade.userId,
tradeId: widget.trade.id,
analysisId: widget.trade.analysisId,
isin: isinVal,
symbol: symbolVal,
actualEntryPrice: _entryPrice,
positionSize: _positionSize,
leverageUsed: _leverage,
entryFee: _entryFee,
exitFee: _exitFee,
quantity: _quantity,
executionTimestamp: DateTime.now().toUtc(),
signalType: widget.trade.signalType,
entryPrice: widget.trade.entryPrice,
stopLoss: _stopLoss,
takeProfit: _takeProfit,
instrumentType: _selectedInstrument,
derivativeIsin: _derivativeIsinCtrl.text.trim(),
timeframe: widget.trade.timeframe,
reasoning: widget.trade.reasoning,
);
}
@override
Widget build(BuildContext context) {
final isBuy = widget.trade.signalType.toUpperCase() == 'BUY' || widget.trade.signalType.toUpperCase() == 'LONG';
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Container(
width: 640,
constraints: const BoxConstraints(maxHeight: 820),
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(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 16, 14),
child: Row(
children: [
StatusBadge(label: widget.trade.signalType.toUpperCase(), color: signalColor),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.isActive ? 'Einstellungen für Trade #${widget.trade.id}' : '1-Click Trade Execution Cockpit',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
'${widget.trade.companyName.isNotEmpty ? widget.trade.companyName : widget.defaultSymbol} (${widget.trade.isin.isNotEmpty ? widget.trade.isin : widget.defaultSymbol})',
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white54),
),
],
),
),
const Divider(color: Colors.white12, height: 1),
// Scrollable Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// AI Plan Summary
TradeExecutionAiPlanCard(trade: widget.trade),
const SizedBox(height: 18),
// SECTION 1: INSTRUMENT & DERIVATIVES
const Text('1. FINANZINSTRUMENT & DERIVATE', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
const SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _availableInstruments.map((opt) {
return _instrumentChip(opt.label, opt.value);
}).toList(),
),
),
const SizedBox(height: 12),
// Derivat Picker Button
if (_selectedInstrument == 'KnockOut' || _selectedInstrument == 'Option' || _selectedInstrument == 'Factor') ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.accentCyan.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.accentCyan.withValues(alpha: 0.3)),
),
child: Row(
children: [
Icon(Icons.bolt, color: AppTheme.accentCyan, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_selectedDerivative != null
? '${_selectedDerivative!.issuerDisplayName} ${_selectedDerivative!.productCategoryName} (${_formatLeverage(_selectedDerivative!.leverage)})'
: (_derivativeIsinCtrl.text.isNotEmpty ? 'Derivat ISIN: ${_derivativeIsinCtrl.text}' : 'Kein Derivat gewählt (Basiswert aktiv)'),
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
),
if (_selectedDerivative != null) ...[
const SizedBox(height: 2),
Wrap(
spacing: 8,
children: [
if (_selectedDerivative!.barrier > 0)
Text('KO-Schwelle: €${_selectedDerivative!.barrier.toStringAsFixed(2)}', style: const TextStyle(color: Colors.orangeAccent, fontSize: 11)),
if (_derivativePrice != null && _derivativePrice! > 0)
Text('Derivatkurs: €${_derivativePrice!.toStringAsFixed(2)} ($_derivativeQuantity Stk.)', style: TextStyle(color: AppTheme.accentCyan, fontSize: 11, fontWeight: FontWeight.bold)),
],
),
],
],
),
),
ElevatedButton.icon(
onPressed: _openDerivativeFinder,
icon: const Icon(Icons.search, size: 14),
label: Text(_selectedDerivative != null ? 'Ändern' : 'Derivat wählen', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
],
),
),
const SizedBox(height: 16),
],
// SECTION 2: POSITION & KAPITAL
const Text('2. INVESTITION & POSITION', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
const SizedBox(height: 10),
// Quick Capital Pills
Row(
children: [
_capitalPill('250 €', 250),
_capitalPill('500 €', 500),
_capitalPill('1.000 €', 1000),
_capitalPill('2.500 €', 2500),
_capitalPill('5.000 €', 5000),
],
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _buildInput('Investitionsbetrag (€)', _positionSizeCtrl, Icons.account_balance_wallet),
),
const SizedBox(width: 12),
Expanded(
child: _buildInput(
'Einstiegskurs (€)',
_entryPriceCtrl,
Icons.price_change,
suffixWidget: _isFetchingDerivativePrice
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.cyan))
: null,
),
),
],
),
const SizedBox(height: 12),
// Stop-Loss & Take-Profit
Row(
children: [
Expanded(child: _buildInput('Stop-Loss (€)', _stopLossCtrl, Icons.shield_outlined, accentColor: AppTheme.accentRed)),
const SizedBox(width: 12),
Expanded(child: _buildInput('Take-Profit (€)', _takeProfitCtrl, Icons.trending_up, accentColor: AppTheme.primaryEmerald)),
],
),
if (_availableTpTargets.isNotEmpty) ...[
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'KI-Ziele (TP-Stufen):',
style: TextStyle(color: AppTheme.textMuted, fontSize: 11, fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
Expanded(
child: Wrap(
spacing: 6,
runSpacing: 4,
children: _availableTpTargets.asMap().entries.map((entry) {
final idx = entry.key;
final targetPrice = entry.value;
final isShort = widget.trade.signalType.toUpperCase() == 'SELL' || widget.trade.signalType.toUpperCase() == 'SHORT';
final diffPct = _entryPrice > 0
? (isShort ? ((_entryPrice - targetPrice) / _entryPrice * 100) : ((targetPrice - _entryPrice) / _entryPrice * 100))
: 0.0;
final isSelected = (_takeProfit - targetPrice).abs() < 0.001;
return InkWell(
onTap: () {
setState(() {
_takeProfitCtrl.text = targetPrice.toStringAsFixed(2);
});
},
borderRadius: BorderRadius.circular(8),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isSelected
? AppTheme.primaryEmerald.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? AppTheme.primaryEmerald
: Colors.white.withValues(alpha: 0.15),
width: isSelected ? 1.5 : 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'TP${idx + 1}: €${targetPrice.toStringAsFixed(2)}',
style: TextStyle(
color: isSelected ? AppTheme.primaryEmerald : Colors.white,
fontSize: 11,
fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold,
),
),
if (diffPct != 0) ...[
const SizedBox(width: 4),
Text(
'(${diffPct >= 0 ? "+" : ""}${diffPct.toStringAsFixed(1)}%)',
style: TextStyle(
color: diffPct >= 0 ? AppTheme.primaryEmerald : AppTheme.accentRed,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
],
),
),
);
}).toList(),
),
),
],
),
],
const SizedBox(height: 18),
// SECTION 3: AUTOMATISCH ERRECHNETE KENNZAHLEN (ZONE 2)
const Text('3. LIVE-KALKULATION (AUTOMATISCH)', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_statTile(
_selectedInstrument == 'KnockOut' || _selectedInstrument == 'Option' || _selectedInstrument == 'Factor'
? 'Stückzahl (${_derivativePrice != null ? "Derivat" : "Basiswert"})'
: 'Stückzahl',
_derivativePrice != null && _derivativePrice! > 0
? '$_derivativeQuantity Stk.'
: '${_quantity.toStringAsFixed(2)} Stk.',
Colors.white,
),
_statTile('Max. Verlust (SL)', '-€${_riskAmountAbs.toStringAsFixed(2)}', AppTheme.accentRed),
_statTile('Gewinn-Potenzial (TP)', '+€${_rewardAmountAbs.toStringAsFixed(2)}', AppTheme.primaryEmerald),
_statTile('Chance-Risiko (CRV)', _crv > 0 ? '1 : ${_crv.toStringAsFixed(2)}' : '-', AppTheme.accentCyan),
],
),
const Divider(color: Colors.white10, height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Pauschalgebühren (€${_totalFees.toStringAsFixed(2)} gesamt):', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
if (_effectiveLeverage > 1.0)
Text('Effektiver Hebel: ${_formatLeverage(_effectiveLeverage)}', style: TextStyle(color: AppTheme.accentCyan, fontSize: 11, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: _buildSmallFeeInput('Kauf (€)', _entryFeeCtrl),
),
const SizedBox(width: 8),
Expanded(
child: _buildSmallFeeInput('Verkauf (€)', _exitFeeCtrl),
),
const SizedBox(width: 8),
InkWell(
onTap: () {
setState(() {
if (_entryFee == 0) {
_entryFeeCtrl.text = '1,00';
} else {
_entryFeeCtrl.text = '0,00';
}
});
},
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: _entryFee == 0 ? AppTheme.accentCyan.withValues(alpha: 0.15) : Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _entryFee == 0 ? AppTheme.accentCyan : Colors.white12),
),
child: Text(
_entryFee == 0 ? '✓ Im Investitionsbetrag enthalten' : 'Kaufgebühr im Betrag enthalten?',
style: TextStyle(
color: _entryFee == 0 ? AppTheme.accentCyan : AppTheme.textMuted,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
const SizedBox(height: 4),
Text(
'️ Hinweis: Rechnerischer Näherungswert. Der effektive Hebel eines Derivats verändert sich dynamisch mit dem Kurs des Basiswerts (Omega/Delta).',
style: TextStyle(color: AppTheme.textMuted, fontSize: 11, fontStyle: FontStyle.italic),
),
],
),
// MULTI-TARGET GEWINNSTUFEN (TP1 - TP3)
if (_availableTpTargets.length > 1) ...[
const Divider(color: Colors.white10, height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'🎯 MEHRSTUFIGE GEWINN-KALKULATION',
style: TextStyle(
color: AppTheme.primaryEmerald,
fontSize: 11,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
),
),
Text(
'Klick zum Aktivieren',
style: TextStyle(color: AppTheme.textMuted, fontSize: 10),
),
],
),
const SizedBox(height: 8),
..._availableTpTargets.asMap().entries.map((entry) {
final idx = entry.key;
final targetPrice = entry.value;
final isSelected = (_takeProfit - targetPrice).abs() < 0.001;
final reward = _calculateRewardForTarget(targetPrice);
final crv = _calculateCrvForTarget(targetPrice);
final isShort = widget.trade.signalType.toUpperCase() == 'SELL' || widget.trade.signalType.toUpperCase() == 'SHORT';
final movePct = _entryPrice > 0
? (isShort ? ((_entryPrice - targetPrice) / _entryPrice * 100) : ((targetPrice - _entryPrice) / _entryPrice * 100))
: 0.0;
final retPct = _positionSize > 0 ? (reward / _positionSize * 100) : 0.0;
return InkWell(
onTap: () {
setState(() {
_takeProfitCtrl.text = targetPrice.toStringAsFixed(2);
});
},
borderRadius: BorderRadius.circular(8),
child: Container(
margin: const EdgeInsets.only(bottom: 5),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
decoration: BoxDecoration(
color: isSelected
? AppTheme.primaryEmerald.withValues(alpha: 0.14)
: Colors.white.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? AppTheme.primaryEmerald.withValues(alpha: 0.7)
: Colors.white.withValues(alpha: 0.07),
width: isSelected ? 1.5 : 1,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryEmerald : Colors.white12,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'TP${idx + 1}',
style: TextStyle(
color: isSelected ? Colors.black : Colors.white,
fontSize: 10,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(width: 8),
Text(
'${targetPrice.toStringAsFixed(2)}',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
const SizedBox(width: 6),
Text(
'(${movePct >= 0 ? "+" : ""}${movePct.toStringAsFixed(1)}% Basiswert)',
style: TextStyle(color: AppTheme.textMuted, fontSize: 10.5),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'+€${reward.toStringAsFixed(2)} (+${retPct.toStringAsFixed(1)}%)',
style: TextStyle(
color: AppTheme.primaryEmerald,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
Text(
'CRV 1 : ${crv > 0 ? crv.toStringAsFixed(2) : "-"}',
style: TextStyle(color: AppTheme.accentCyan, fontSize: 10),
),
],
),
],
),
),
);
}),
],
],
),
),
const SizedBox(height: 14),
// SECTION 4: RECHTLICHER DISCLAIMER
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.amber.withValues(alpha: 0.25)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.gavel_outlined, size: 16, color: Colors.amber.withValues(alpha: 0.85)),
const SizedBox(width: 10),
Expanded(
child: Text(
'Rechtlicher Hinweis: Keine Anlageberatung. Sämtliche Analysen, Kennzahlen und Simulationen dienen ausschließlich Informations- und Bildungszwecken. Der Handel mit Hebelprodukten (Derivate, CFDs) birgt erhebliche Risiken bis hin zum Totalverlust des eingesetzten Kapitals.',
style: TextStyle(color: AppTheme.textMuted, fontSize: 10.5, height: 1.35),
),
),
],
),
),
],
),
),
),
// Actions Footer
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
),
const Spacer(),
if (!widget.isActive && widget.onReject != null) ...[
OutlinedButton.icon(
onPressed: () {
widget.onReject!(widget.trade.id);
Navigator.pop(context);
},
icon: Icon(Icons.cancel, color: AppTheme.accentRed, size: 16),
label: Text('Ablehnen', style: TextStyle(color: AppTheme.accentRed)),
style: OutlinedButton.styleFrom(side: BorderSide(color: AppTheme.accentRed.withValues(alpha: 0.5))),
),
const SizedBox(width: 10),
],
ElevatedButton.icon(
onPressed: () {
final dto = _buildDto();
widget.onAccept(dto);
Navigator.pop(context);
},
icon: Icon(widget.isActive ? Icons.save : Icons.check_circle, size: 18),
label: Text(
widget.isActive ? 'Einstellungen Speichern' : 'Trade Jetzt Eröffnen',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
style: ElevatedButton.styleFrom(
backgroundColor: widget.isActive ? AppTheme.accentCyan : AppTheme.primaryEmerald,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
],
),
),
],
),
),
);
}
Widget _instrumentChip(String label, String value) {
final isSelected = _selectedInstrument == value;
return GestureDetector(
onTap: () => setState(() => _selectedInstrument = value),
child: Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryEmerald.withValues(alpha: 0.2) : AppTheme.glassSurface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isSelected ? AppTheme.primaryEmerald : AppTheme.glassBorder),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? AppTheme.primaryEmerald : Colors.white70,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
),
),
);
}
Widget _capitalPill(String label, double val) {
return GestureDetector(
onTap: () => setState(() => _positionSizeCtrl.text = val.toStringAsFixed(0)),
child: Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white12),
),
child: Text(label, style: const TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.bold)),
),
);
}
Widget _buildInput(String label, TextEditingController controller, IconData icon, {Color? accentColor, Widget? suffixWidget}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: accentColor ?? AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
TextField(
controller: controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold),
decoration: InputDecoration(
prefixIcon: Icon(icon, size: 16, color: accentColor ?? Colors.white54),
suffixIcon: suffixWidget,
filled: true,
fillColor: AppTheme.glassSurface,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: accentColor ?? AppTheme.accentCyan)),
),
),
],
);
}
Widget _buildSmallFeeInput(String label, TextEditingController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
const SizedBox(height: 3),
SizedBox(
height: 32,
child: TextField(
controller: controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
decoration: InputDecoration(
filled: true,
fillColor: AppTheme.glassSurface,
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: AppTheme.glassBorder)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: AppTheme.glassBorder)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: AppTheme.accentCyan)),
),
),
),
],
);
}
Widget _statTile(String label, String value, Color col) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
const SizedBox(height: 2),
Text(value, style: TextStyle(color: col, fontWeight: FontWeight.bold, fontSize: 13)),
],
);
}
}