feat(trades): add live execution cockpit, closing cockpit, calculation cards and precision trade settings
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user