feat(app): responsive asset detail layout, full width chart, reactive hero header, shimmer loaders and enriched fundamentals
This commit is contained in:
@@ -19,6 +19,7 @@ class TradeAcceptanceDto {
|
||||
final double? stopLoss;
|
||||
final double? takeProfit;
|
||||
final String? instrumentType;
|
||||
final String? derivativeIsin;
|
||||
final String? timeframe;
|
||||
final String? reasoning;
|
||||
|
||||
@@ -42,6 +43,7 @@ class TradeAcceptanceDto {
|
||||
this.stopLoss,
|
||||
this.takeProfit,
|
||||
this.instrumentType,
|
||||
this.derivativeIsin,
|
||||
this.timeframe,
|
||||
this.reasoning,
|
||||
});
|
||||
@@ -67,6 +69,7 @@ class TradeAcceptanceDto {
|
||||
'stopLoss': stopLoss,
|
||||
'takeProfit': takeProfit,
|
||||
'instrumentType': instrumentType,
|
||||
'derivativeIsin': derivativeIsin,
|
||||
'timeframe': timeframe,
|
||||
'reasoning': reasoning,
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ class TradeModel extends Equatable {
|
||||
final double winRate;
|
||||
final String timeframe;
|
||||
final String instrumentType;
|
||||
final String derivativeIsin;
|
||||
final DateTime? createdAt;
|
||||
|
||||
final String riskTolerance;
|
||||
@@ -67,6 +68,7 @@ class TradeModel extends Equatable {
|
||||
this.winRate = 50.0,
|
||||
this.timeframe = '1D',
|
||||
this.instrumentType = 'Stock',
|
||||
this.derivativeIsin = '',
|
||||
this.createdAt,
|
||||
this.riskTolerance = 'Moderate',
|
||||
this.vixValue = 0.0,
|
||||
@@ -92,21 +94,45 @@ class TradeModel extends Equatable {
|
||||
}
|
||||
|
||||
double get calculatedPnlAbs {
|
||||
if (pnlAbsolute != 0) return pnlAbsolute;
|
||||
if (isClosed && pnlAbsolute != 0) return pnlAbsolute;
|
||||
final curr = currentPrice;
|
||||
if (curr <= 0) return pnlAbsolute;
|
||||
final entry = actualEntryPrice > 0 ? actualEntryPrice : entryPrice;
|
||||
final curr = effectiveCurrentPrice;
|
||||
if (entry <= 0) return 0.0;
|
||||
final isShort = signalType == 'SELL' || signalType == 'SHORT';
|
||||
final rawMove = isShort ? ((entry - curr) / entry) : ((curr - entry) / entry);
|
||||
final posSize = positionSize > 0 ? positionSize : entry;
|
||||
final posSize = positionSize > 0 ? positionSize : (quantity > 0 ? quantity * entry : entry);
|
||||
final lev = leverageUsed > 0 ? leverageUsed : 1.0;
|
||||
return (rawMove * posSize * lev);
|
||||
final fees = entryFee + exitFee;
|
||||
return (rawMove * posSize * lev) - fees;
|
||||
}
|
||||
|
||||
double get calculatedPnlPct {
|
||||
if (pnlPercent != 0) return pnlPercent;
|
||||
if (isClosed && pnlPercent != 0) return pnlPercent;
|
||||
final pnlAbs = calculatedPnlAbs;
|
||||
final posSize = positionSize > 0 ? positionSize : (actualEntryPrice > 0 ? actualEntryPrice : entryPrice);
|
||||
final posSize = positionSize > 0 ? positionSize : (actualEntryPrice > 0 ? actualEntryPrice : (entryPrice > 0 ? entryPrice : 1.0));
|
||||
if (posSize <= 0) return 0.0;
|
||||
return (pnlAbs / posSize) * 100.0;
|
||||
}
|
||||
|
||||
double calculateLivePnlAbs(double livePrice) {
|
||||
if (isClosed && pnlAbsolute != 0) return pnlAbsolute;
|
||||
final curr = livePrice > 0 ? livePrice : currentPrice;
|
||||
if (curr <= 0) return pnlAbsolute;
|
||||
final entry = actualEntryPrice > 0 ? actualEntryPrice : entryPrice;
|
||||
if (entry <= 0) return 0.0;
|
||||
final isShort = signalType == 'SELL' || signalType == 'SHORT';
|
||||
final rawMove = isShort ? ((entry - curr) / entry) : ((curr - entry) / entry);
|
||||
final posSize = positionSize > 0 ? positionSize : (quantity > 0 ? quantity * entry : entry);
|
||||
final lev = leverageUsed > 0 ? leverageUsed : 1.0;
|
||||
final fees = entryFee + exitFee;
|
||||
return (rawMove * posSize * lev) - fees;
|
||||
}
|
||||
|
||||
double calculateLivePnlPct(double livePrice) {
|
||||
if (isClosed && pnlPercent != 0) return pnlPercent;
|
||||
final pnlAbs = calculateLivePnlAbs(livePrice);
|
||||
final posSize = positionSize > 0 ? positionSize : (actualEntryPrice > 0 ? actualEntryPrice : (entryPrice > 0 ? entryPrice : 1.0));
|
||||
if (posSize <= 0) return 0.0;
|
||||
return (pnlAbs / posSize) * 100.0;
|
||||
}
|
||||
@@ -161,6 +187,7 @@ class TradeModel extends Equatable {
|
||||
winRate: parseDbl(json['winRate'] ?? json['WinRate']),
|
||||
timeframe: (json['timeframe'] ?? json['Timeframe'])?.toString() ?? '1D',
|
||||
instrumentType: (json['instrumentType'] ?? json['InstrumentType'])?.toString() ?? 'Stock',
|
||||
derivativeIsin: (json['derivativeIsin'] ?? json['DerivativeIsin'] ?? json['knockoutIsin'] ?? json['KnockoutIsin'])?.toString() ?? '',
|
||||
createdAt: dt,
|
||||
riskTolerance: (json['riskTolerance'] ?? json['RiskTolerance'])?.toString() ?? 'Moderate',
|
||||
vixValue: parseDbl(json['vixValue'] ?? json['VixValue']),
|
||||
@@ -205,6 +232,7 @@ class TradeModel extends Equatable {
|
||||
'winRate': winRate,
|
||||
'timeframe': timeframe,
|
||||
'instrumentType': instrumentType,
|
||||
'derivativeIsin': derivativeIsin,
|
||||
'createdAt': createdAt?.toIso8601String(),
|
||||
'riskTolerance': riskTolerance,
|
||||
'vixValue': vixValue,
|
||||
|
||||
@@ -10,7 +10,9 @@ class TradeRepository {
|
||||
|
||||
Future<List<TradeModel>> fetchTrades({String? isin, String? status}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{};
|
||||
final queryParams = <String, dynamic>{
|
||||
'_t': DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
if (isin != null && isin.isNotEmpty) queryParams['isin'] = isin;
|
||||
if (status != null && status.isNotEmpty) queryParams['status'] = status;
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class _TradesFeedScreenContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
String _selectedFilter = 'Alle'; // 'Alle', 'Offen', 'Vorschläge', 'Geschlossen'
|
||||
String _selectedFilter = 'Offen'; // 'Alle', 'Offen', 'Vorschläge', 'Geschlossen'
|
||||
String _searchQuery = '';
|
||||
final TextEditingController _searchCtrl = TextEditingController();
|
||||
|
||||
@@ -165,7 +165,7 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
final rejectedTrades = allTrades.where((t) => t.isRejected).toList();
|
||||
|
||||
// Performance Header Calculations
|
||||
final totalOpenPnlAbs = activeTrades.fold<double>(0, (sum, t) => sum + t.pnlAbsolute);
|
||||
final totalOpenPnlAbs = activeTrades.fold<double>(0, (sum, t) => sum + t.calculatedPnlAbs);
|
||||
final isPnlPos = totalOpenPnlAbs >= 0;
|
||||
final winRatePct = allTrades.isNotEmpty
|
||||
? (allTrades.where((t) => t.pnlAbsolute >= 0).length / allTrades.length * 100)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../../favorites/cubit/favorites_cubit.dart';
|
||||
import '../../favorites/models/favorite_asset_model.dart';
|
||||
import '../models/trade_model.dart';
|
||||
import 'trade_detail_modal.dart';
|
||||
|
||||
@@ -26,23 +29,34 @@ class TradeCard extends StatelessWidget {
|
||||
final isActive = trade.isActive;
|
||||
final isClosed = trade.isClosed;
|
||||
|
||||
final pnlAbs = trade.calculatedPnlAbs;
|
||||
final pnlPct = trade.calculatedPnlPct;
|
||||
final isPnlPos = pnlAbs >= 0;
|
||||
final pnlColor = isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
return BlocBuilder<FavoritesCubit, FavoritesState>(
|
||||
builder: (context, favState) {
|
||||
double livePrice = 0.0;
|
||||
final keyUpper = (trade.isin.isNotEmpty ? trade.isin : trade.symbol).toUpperCase();
|
||||
final match = favState.favoriteDetails.firstWhere(
|
||||
(f) => f.isin.toUpperCase() == keyUpper || f.symbol.toUpperCase() == keyUpper,
|
||||
orElse: () => const FavoriteAssetModel(isin: '', symbol: '', name: '', currentPrice: 0.0, change24h: 0.0),
|
||||
);
|
||||
if (match.currentPrice > 0) {
|
||||
livePrice = match.currentPrice;
|
||||
}
|
||||
|
||||
final currPrice = trade.effectiveCurrentPrice;
|
||||
final pnlAbs = livePrice > 0 ? trade.calculateLivePnlAbs(livePrice) : trade.calculatedPnlAbs;
|
||||
final pnlPct = livePrice > 0 ? trade.calculateLivePnlPct(livePrice) : trade.calculatedPnlPct;
|
||||
final isPnlPos = pnlAbs >= 0;
|
||||
final pnlColor = isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
final currPrice = livePrice > 0 ? livePrice : trade.effectiveCurrentPrice;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => TradeDetailModal.show(
|
||||
context,
|
||||
trade: trade,
|
||||
onAccept: onAccept,
|
||||
onClose: onClose,
|
||||
),
|
||||
child: GlassContainer(
|
||||
margin: const EdgeInsets.only(bottom: 14),
|
||||
padding: const EdgeInsets.all(16),
|
||||
return GestureDetector(
|
||||
onTap: () => TradeDetailModal.show(
|
||||
context,
|
||||
trade: trade,
|
||||
onAccept: onAccept,
|
||||
onClose: onClose,
|
||||
),
|
||||
child: GlassContainer(
|
||||
margin: const EdgeInsets.only(bottom: 14),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -187,7 +201,7 @@ class TradeCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${trade.instrumentType.isNotEmpty ? trade.instrumentType : "Stock"} • ${trade.timeframe.isNotEmpty ? trade.timeframe : "1D"}${trade.leverageUsed > 1 ? " • ${trade.leverageUsed.toStringAsFixed(0)}x Hebel" : ""}',
|
||||
'${trade.instrumentType.isNotEmpty ? trade.instrumentType : "Stock"}${trade.derivativeIsin.isNotEmpty ? " (${trade.derivativeIsin})" : ""} • ${trade.timeframe.isNotEmpty ? trade.timeframe : "1D"}${trade.leverageUsed > 1 ? " • ${trade.leverageUsed.toStringAsFixed(0)}x Hebel" : ""}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
||||
),
|
||||
|
||||
@@ -241,14 +255,16 @@ class TradeCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _priceItem(String label, String val, Color valColor) {
|
||||
|
||||
@@ -287,6 +287,7 @@ class TradeDetailModal extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
_paramRow('Instrument Typ:', trade.instrumentType.isNotEmpty ? trade.instrumentType : 'Stock'),
|
||||
if (trade.derivativeIsin.isNotEmpty) _paramRow('Derivat / Hebel ISIN:', trade.derivativeIsin),
|
||||
_paramRow('Zeithorizont:', trade.timeframe.isNotEmpty ? trade.timeframe : '1D'),
|
||||
if (trade.leverageUsed > 1) _paramRow('Hebel:', '${trade.leverageUsed.toStringAsFixed(0)}x'),
|
||||
if (trade.positionSize > 0) _paramRow('Positionsgröße:', '${trade.positionSize.toStringAsFixed(2)} €'),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:finlytic_app/core/theme/app_theme.dart';
|
||||
import 'package:finlytic_app/core/widgets/status_badge.dart';
|
||||
import 'package:finlytic_app/core/network/api_client.dart';
|
||||
import '../../../../features/trades/models/trade_model.dart';
|
||||
import '../../../../features/trades/models/trade_acceptance_dto.dart';
|
||||
|
||||
@@ -8,381 +10,523 @@ class TradeExecutionDialog {
|
||||
static const double _defaultPositionSize = 1000.0;
|
||||
static const double _defaultLeverage = 1.0;
|
||||
|
||||
static const List<String> _allowedInstruments = ['Stock', 'KnockOut', 'Option', 'CFD', 'Crypto'];
|
||||
|
||||
/// Normalisiert beliebige Freitexte/Bezeichnungen auf die erlaubten Dropdown-Werte
|
||||
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'; // Fallback
|
||||
}
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required TradeModel trade,
|
||||
required String defaultSymbol,
|
||||
bool isActive = false,
|
||||
required Function(TradeAcceptanceDto dto) onAccept,
|
||||
Function(String tradeId)? onReject,
|
||||
}) {
|
||||
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 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);
|
||||
|
||||
// Normalisierte Zuweisung verhindert den DropdownButton Assertion-Error
|
||||
String selectedInstrumentType = _normalizeInstrumentType(
|
||||
trade.instrumentType.isNotEmpty ? trade.instrumentType : 'KnockOut',
|
||||
);
|
||||
|
||||
bool isFetchingDerivativePrice = false;
|
||||
|
||||
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;
|
||||
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 * lev) / entry;
|
||||
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 res = await apiClient.get('/api/v1/assets/$cleanIsin/technicals?forceRefresh=true');
|
||||
if (res.statusCode == 200 && res.data != null) {
|
||||
final Map<String, dynamic> data = res.data;
|
||||
double? fetchedPrice;
|
||||
if (data['candles'] is List && (data['candles'] as List).isNotEmpty) {
|
||||
fetchedPrice = ((data['candles'] as List).last['close'] as num?)?.toDouble();
|
||||
} else if (data['currentPrice'] != null) {
|
||||
fetchedPrice = (data['currentPrice'] as num?)?.toDouble();
|
||||
}
|
||||
|
||||
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 via tr_GetPrice: $e'),
|
||||
backgroundColor: AppTheme.accentRed,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setModalState(() => isFetchingDerivativePrice = false);
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
return StatefulBuilder(
|
||||
builder: (stfContext, setModalState) {
|
||||
final isKnockout = selectedInstrumentType.toLowerCase().contains('knock') ||
|
||||
selectedInstrumentType.toLowerCase().contains('zertifikat') ||
|
||||
selectedInstrumentType.toLowerCase().contains('option') ||
|
||||
selectedInstrumentType.toLowerCase().contains('cfd');
|
||||
|
||||
// Absicherung gegen Assertion-Errors: Stellt sicher, dass der selektierte Wert in der Liste existiert
|
||||
final safeInstrumentValue = _allowedInstruments.contains(selectedInstrumentType)
|
||||
? selectedInstrumentType
|
||||
: 'KnockOut';
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 580,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
title: Row(
|
||||
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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
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(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
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, style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (trade.winRate > 0)
|
||||
Row(
|
||||
children: [
|
||||
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: const 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),
|
||||
|
||||
// Instrument-Type Dropdown mit abgesichertem Value
|
||||
DropdownButtonFormField<String>(
|
||||
value: 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(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static String _fmt(dynamic val) {
|
||||
if (val == null) return '0.00';
|
||||
if (val is double) {
|
||||
@@ -396,7 +540,7 @@ class TradeExecutionDialog {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: Colors.white54, fontSize: 11)),
|
||||
Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
],
|
||||
@@ -416,4 +560,4 @@ class TradeExecutionDialog {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user