1021 lines
49 KiB
Dart
1021 lines
49 KiB
Dart
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)),
|
||
],
|
||
);
|
||
}
|
||
}
|