320 lines
12 KiB
Dart
320 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../../../core/theme/app_theme.dart';
|
|
import '../models/trade_model.dart';
|
|
|
|
class TradeCalculationCard extends StatelessWidget {
|
|
final TradeModel trade;
|
|
final bool initiallyExpanded;
|
|
final bool isCollapsible;
|
|
|
|
const TradeCalculationCard({
|
|
super.key,
|
|
required this.trade,
|
|
this.initiallyExpanded = true,
|
|
this.isCollapsible = false,
|
|
});
|
|
|
|
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';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final entry = trade.actualEntryPrice > 0
|
|
? trade.actualEntryPrice
|
|
: (trade.entryPrice > 0 ? trade.entryPrice : 1.0);
|
|
final posSize = trade.positionSize > 0 ? trade.positionSize : 1000.0;
|
|
final lev = trade.leverageUsed > 0 ? trade.leverageUsed : 1.0;
|
|
final isShort = trade.signalType.toUpperCase() == 'SELL' ||
|
|
trade.signalType.toUpperCase() == 'SHORT';
|
|
final totalFees = trade.entryFee + (trade.exitFee > 0 ? trade.exitFee : 1.0);
|
|
|
|
final quantity = entry > 0 ? (posSize / entry) : 0.0;
|
|
|
|
// SL Risk
|
|
final sl = trade.stopLoss;
|
|
final movePctSL = entry > 0 && sl > 0
|
|
? (isShort ? ((sl - entry) / entry) : ((entry - sl) / entry))
|
|
: 0.0;
|
|
final rawLoss = (movePctSL * posSize * lev).abs();
|
|
final isDerivative = trade.instrumentType.toLowerCase().contains('knock') ||
|
|
trade.instrumentType.toLowerCase().contains('option') ||
|
|
trade.instrumentType.toLowerCase().contains('factor') ||
|
|
trade.instrumentType.toLowerCase().contains('turbo');
|
|
final cappedLoss = isDerivative ? rawLoss.clamp(0.0, posSize) : rawLoss;
|
|
final riskAmountAbs = cappedLoss + totalFees;
|
|
|
|
// TP Reward
|
|
final tp = trade.takeProfit;
|
|
final movePctTP = entry > 0 && tp > 0
|
|
? (isShort ? ((entry - tp) / entry) : ((tp - entry) / entry))
|
|
: 0.0;
|
|
final rawProfit = (movePctTP * posSize * lev);
|
|
final profitAfterFees = rawProfit - totalFees;
|
|
final rewardAmountAbs = profitAfterFees > 0 ? profitAfterFees : 0.0;
|
|
|
|
// CRV
|
|
final crv = (riskAmountAbs > 0 && rewardAmountAbs > 0)
|
|
? (rewardAmountAbs / riskAmountAbs)
|
|
: 0.0;
|
|
|
|
// Multi-Targets
|
|
final targets = trade.takeProfitTargets.isNotEmpty
|
|
? trade.takeProfitTargets
|
|
: (trade.takeProfit > 0 ? [trade.takeProfit] : <double>[]);
|
|
|
|
final content = Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.3),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_statTile(
|
|
'Stückzahl (Basiswert)',
|
|
'${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),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Pauschalgebühren: €${totalFees.toStringAsFixed(2)} (€${trade.entryFee.toStringAsFixed(2)} Kauf + €${(trade.exitFee > 0 ? trade.exitFee : 1.0).toStringAsFixed(2)} Verkauf)',
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
|
),
|
|
if (lev > 1.0)
|
|
Text(
|
|
'Effektiver Hebel: ${_formatLeverage(lev)}',
|
|
style: TextStyle(
|
|
color: AppTheme.accentCyan,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (targets.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(
|
|
'${targets.length} Ziele',
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
...targets.asMap().entries.map((entryItem) {
|
|
final idx = entryItem.key;
|
|
final targetPrice = entryItem.value;
|
|
final isCurrent = (tp - targetPrice).abs() < 0.001;
|
|
|
|
final targetMovePct = entry > 0
|
|
? (isShort
|
|
? ((entry - targetPrice) / entry)
|
|
: ((targetPrice - entry) / entry))
|
|
: 0.0;
|
|
final rawTargetProfit = targetMovePct * posSize * lev;
|
|
final netTargetProfit = rawTargetProfit - totalFees;
|
|
final cappedNet = netTargetProfit > 0 ? netTargetProfit : 0.0;
|
|
final retPct = posSize > 0 ? (cappedNet / posSize * 100) : 0.0;
|
|
final targetCrv = (riskAmountAbs > 0 && cappedNet > 0)
|
|
? (cappedNet / riskAmountAbs)
|
|
: 0.0;
|
|
final baseMove = (targetMovePct * 100).abs();
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 6),
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: isCurrent
|
|
? AppTheme.primaryEmerald.withValues(alpha: 0.14)
|
|
: Colors.white.withValues(alpha: 0.03),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: isCurrent
|
|
? AppTheme.primaryEmerald.withValues(alpha: 0.7)
|
|
: Colors.white.withValues(alpha: 0.07),
|
|
width: isCurrent ? 1.5 : 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: isCurrent
|
|
? AppTheme.primaryEmerald
|
|
: Colors.white12,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
'TP${idx + 1}',
|
|
style: TextStyle(
|
|
color: isCurrent ? Colors.black : Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'€${targetPrice.toStringAsFixed(2)}',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'(+${baseMove.toStringAsFixed(1)}% Basiswert)',
|
|
style: TextStyle(
|
|
color: AppTheme.textMuted, fontSize: 10.5),
|
|
),
|
|
],
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
'+€${cappedNet.toStringAsFixed(2)} (+${retPct.toStringAsFixed(1)}%)',
|
|
style: TextStyle(
|
|
color: isCurrent
|
|
? AppTheme.primaryEmerald
|
|
: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
if (targetCrv > 0)
|
|
Text(
|
|
'CRV 1 : ${targetCrv.toStringAsFixed(2)}',
|
|
style: TextStyle(
|
|
color: isCurrent
|
|
? AppTheme.primaryEmerald
|
|
: AppTheme.accentCyan,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
|
|
if (!isCollapsible) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'3. LIVE-KALKULATION (AUTOMATISCH)',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
content,
|
|
],
|
|
);
|
|
}
|
|
|
|
return Theme(
|
|
data: ThemeData(dividerColor: Colors.transparent),
|
|
child: ExpansionTile(
|
|
initiallyExpanded: initiallyExpanded,
|
|
tilePadding: EdgeInsets.zero,
|
|
childrenPadding: EdgeInsets.zero,
|
|
dense: true,
|
|
iconColor: AppTheme.primaryEmerald,
|
|
collapsedIconColor: Colors.white70,
|
|
title: Row(
|
|
children: [
|
|
Icon(Icons.calculate_outlined, color: AppTheme.primaryEmerald, size: 16),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'Live-Kalkulation & Gewinn-Potenzial',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
children: [
|
|
content,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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: 12.5)),
|
|
],
|
|
);
|
|
}
|
|
}
|