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

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)),
],
);
}
}