361 lines
18 KiB
Dart
361 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../../../core/theme/app_theme.dart';
|
|
import '../../../core/network/api_client.dart';
|
|
import '../../asset_detail/repositories/asset_repository.dart';
|
|
import '../models/trade_model.dart';
|
|
import '../models/trade_acceptance_dto.dart';
|
|
import 'trade_execution_ai_plan_card.dart';
|
|
|
|
class TradeExecutionDialog {
|
|
static const double _defaultPositionSize = 1000.0;
|
|
static const double _defaultLeverage = 1.0;
|
|
static const List<String> _allowedInstruments = ['Stock', 'KnockOut', 'Option', 'CFD', 'Crypto'];
|
|
|
|
static String _normalizeInstrumentType(String raw) {
|
|
final clean = raw.toLowerCase().trim();
|
|
if (clean.contains('knock') || clean.contains('zertifikat') || clean.contains('turbo')) {
|
|
return 'KnockOut';
|
|
}
|
|
if (clean.contains('option')) {
|
|
return 'Option';
|
|
}
|
|
if (clean.contains('cfd')) {
|
|
return 'CFD';
|
|
}
|
|
if (clean.contains('crypto') || clean.contains('krypto')) {
|
|
return 'Crypto';
|
|
}
|
|
if (clean.contains('stock') || clean.contains('aktie') || clean.contains('etf')) {
|
|
return 'Stock';
|
|
}
|
|
return 'KnockOut';
|
|
}
|
|
|
|
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 / initEntry) : 10.0;
|
|
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());
|
|
|
|
final derivativeIsinController = TextEditingController(text: trade.derivativeIsin);
|
|
|
|
String selectedInstrumentType = _normalizeInstrumentType(
|
|
trade.instrumentType.isNotEmpty ? trade.instrumentType : 'KnockOut',
|
|
);
|
|
|
|
bool isFetchingDerivativePrice = false;
|
|
|
|
void recalculateQuantity() {
|
|
final entryStr = actualEntryController.text.replaceAll(',', '.').trim();
|
|
final posStr = positionSizeController.text.replaceAll(',', '.').trim();
|
|
|
|
final entry = double.tryParse(entryStr) ?? 0.0;
|
|
final posSize = double.tryParse(posStr) ?? 0.0;
|
|
if (entry > 0 && posSize > 0) {
|
|
final q = posSize / entry;
|
|
quantityController.text = q.toStringAsFixed(4);
|
|
}
|
|
}
|
|
|
|
TradeAcceptanceDto buildDto() {
|
|
final isinVal = trade.isin.isNotEmpty ? trade.isin : (trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol);
|
|
final symbolVal = trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol;
|
|
|
|
double parseNum(String text, double fallback) {
|
|
final clean = text.replaceAll(',', '.').trim();
|
|
return double.tryParse(clean) ?? fallback;
|
|
}
|
|
|
|
return TradeAcceptanceDto(
|
|
userId: trade.userId,
|
|
tradeId: trade.id,
|
|
analysisId: trade.analysisId,
|
|
isin: isinVal,
|
|
symbol: symbolVal,
|
|
actualEntryPrice: parseNum(actualEntryController.text, trade.actualEntryPrice > 0 ? trade.actualEntryPrice : trade.entryPrice),
|
|
positionSize: parseNum(positionSizeController.text, trade.positionSize > 0 ? trade.positionSize : 1000.0),
|
|
leverageUsed: parseNum(leverageController.text, trade.leverageUsed > 0 ? trade.leverageUsed : 1.0),
|
|
entryFee: parseNum(entryFeeController.text, trade.entryFee),
|
|
exitFee: parseNum(exitFeeController.text, trade.exitFee),
|
|
quantity: parseNum(quantityController.text, trade.quantity),
|
|
executionTimestamp: DateTime.now().toUtc(),
|
|
signalType: trade.signalType,
|
|
entryPrice: trade.entryPrice,
|
|
stopLoss: parseNum(slController.text, trade.stopLoss),
|
|
takeProfit: parseNum(tpController.text, trade.takeProfit),
|
|
instrumentType: selectedInstrumentType,
|
|
derivativeIsin: derivativeIsinController.text.trim(),
|
|
timeframe: trade.timeframe,
|
|
reasoning: trade.reasoning,
|
|
);
|
|
}
|
|
|
|
Future<void> fetchDerivativePrice(StateSetter setModalState, String inputIsin) async {
|
|
final cleanIsin = inputIsin.trim().toUpperCase();
|
|
if (cleanIsin.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Bitte gib eine gültige Derivat/Knock-Out ISIN ein.'), backgroundColor: Colors.amber, behavior: SnackBarBehavior.floating),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setModalState(() => isFetchingDerivativePrice = true);
|
|
try {
|
|
final apiClient = context.read<ApiClient>();
|
|
final assetRepo = AssetRepository(apiClient: apiClient);
|
|
final technicals = await assetRepo.getAssetTechnical(cleanIsin, 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) {
|
|
actualEntryController.text = fetchedPrice.toStringAsFixed(2);
|
|
recalculateQuantity();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Live-Kurs für Derivat $cleanIsin abgerufen: €${fetchedPrice.toStringAsFixed(2)}'),
|
|
backgroundColor: AppTheme.primaryEmerald,
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Kein Kurs für Derivat ISIN $cleanIsin gefunden.'), backgroundColor: Colors.amber, behavior: SnackBarBehavior.floating),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Fehler beim Abrufen des Kurses für $cleanIsin: $e'), backgroundColor: AppTheme.accentRed, behavior: SnackBarBehavior.floating),
|
|
);
|
|
} finally {
|
|
setModalState(() => isFetchingDerivativePrice = false);
|
|
}
|
|
}
|
|
|
|
actualEntryController.addListener(recalculateQuantity);
|
|
positionSizeController.addListener(recalculateQuantity);
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
return StatefulBuilder(
|
|
builder: (stfContext, setModalState) {
|
|
final isKnockout = selectedInstrumentType.toLowerCase().contains('knock') ||
|
|
selectedInstrumentType.toLowerCase().contains('zertifikat') ||
|
|
selectedInstrumentType.toLowerCase().contains('option') ||
|
|
selectedInstrumentType.toLowerCase().contains('cfd');
|
|
|
|
final safeInstrumentValue = _allowedInstruments.contains(selectedInstrumentType) ? selectedInstrumentType : 'KnockOut';
|
|
|
|
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),
|
|
TradeExecutionAiPlanCard(trade: trade),
|
|
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),
|
|
DropdownButtonFormField<String>(
|
|
initialValue: safeInstrumentValue,
|
|
dropdownColor: AppTheme.cardSurface,
|
|
decoration: const InputDecoration(labelText: 'Finanzinstrument Typ', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
items: const [
|
|
DropdownMenuItem(value: 'Stock', child: Text('Aktie / ETF (Direktinvestment)', style: TextStyle(color: Colors.white, fontSize: 13))),
|
|
DropdownMenuItem(value: 'KnockOut', child: Text('Knock-Out Zertifikat', style: TextStyle(color: Colors.white, fontSize: 13))),
|
|
DropdownMenuItem(value: 'Option', child: Text('Optionsschein / Derivat', style: TextStyle(color: Colors.white, fontSize: 13))),
|
|
DropdownMenuItem(value: 'CFD', child: Text('CFD (Hebel-Derivat)', style: TextStyle(color: Colors.white, fontSize: 13))),
|
|
DropdownMenuItem(value: 'Crypto', child: Text('Krypto', style: TextStyle(color: Colors.white, fontSize: 13))),
|
|
],
|
|
onChanged: (val) {
|
|
if (val != null) setModalState(() => selectedInstrumentType = val);
|
|
},
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (isKnockout) ...[
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: derivativeIsinController,
|
|
decoration: const InputDecoration(labelText: 'Knock-Out / Derivat ISIN (z.B. DE000...)', hintText: 'ISIN des Hebels eingeben...', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton.icon(
|
|
onPressed: isFetchingDerivativePrice ? null : () => fetchDerivativePrice(setModalState, derivativeIsinController.text),
|
|
icon: isFetchingDerivativePrice
|
|
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black))
|
|
: const Icon(Icons.bolt, size: 16),
|
|
label: const Text('tr_GetPrice'),
|
|
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)),
|
|
),
|
|
],
|
|
),
|
|
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 (Invest. / Einstieg)', 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 && 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)),
|
|
),
|
|
if (!isActive && onReject != null) const SizedBox(width: 8),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
final dto = buildDto();
|
|
onAccept(dto);
|
|
Navigator.of(dialogContext).pop();
|
|
},
|
|
icon: Icon(isActive ? Icons.save : Icons.check_circle, size: 16),
|
|
label: Text(isActive ? 'Einstellungen Speichern' : 'Trade Annehmen & Ausführen'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: isActive ? AppTheme.accentCyan : AppTheme.primaryEmerald,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
} |