263 lines
9.7 KiB
Dart
263 lines
9.7 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,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final entry = trade.averageBuyIn;
|
|
final quantity = trade.totalQuantity;
|
|
final posSize = entry * quantity;
|
|
final isShort = !trade.direction.isLong;
|
|
|
|
// Distance-to-stop risk, computed only from real DTO fields (averageBuyIn,
|
|
// currentStopLoss, totalQuantity) — no fee/leverage assumptions are
|
|
// invented since ActiveTradeDto no longer carries either (Rules.md §4).
|
|
final sl = trade.currentStopLoss;
|
|
final riskAmountAbs = (isShort ? (sl - entry) : (entry - sl)).abs() * quantity;
|
|
|
|
// Reward-to-target(s), same principle.
|
|
final stages = trade.exitPlan.takeProfitStages;
|
|
final primaryTp = trade.primaryTakeProfit;
|
|
final rewardAmountAbs = primaryTp != null ? (isShort ? (entry - primaryTp) : (primaryTp - entry)).abs() * quantity : null;
|
|
|
|
final crv = (rewardAmountAbs != null && riskAmountAbs > 0) ? (rewardAmountAbs / riskAmountAbs) : null;
|
|
|
|
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',
|
|
'${quantity.toStringAsFixed(2)} Stk.',
|
|
Colors.white,
|
|
),
|
|
_statTile(
|
|
'Risiko bis Stop-Loss',
|
|
'-€${riskAmountAbs.toStringAsFixed(2)}',
|
|
AppTheme.accentRed,
|
|
),
|
|
_statTile(
|
|
'Potenzial bis 1. Ziel',
|
|
rewardAmountAbs != null ? '+€${rewardAmountAbs.toStringAsFixed(2)}' : 'Trailing-Exit',
|
|
AppTheme.primaryEmerald,
|
|
),
|
|
_statTile(
|
|
'Chance-Risiko (CRV)',
|
|
crv != null ? '1 : ${crv.toStringAsFixed(2)}' : '-',
|
|
AppTheme.accentCyan,
|
|
),
|
|
],
|
|
),
|
|
const Divider(color: Colors.white10, height: 16),
|
|
Text(
|
|
'Investiertes Kapital: €${posSize.toStringAsFixed(2)} • Exit-Strategie: ${trade.exitPlan.strategyType.label}',
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
|
),
|
|
if (stages.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(
|
|
'${stages.length} Ziele',
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
...stages.map((stage) {
|
|
final targetPrice = stage.targetPrice;
|
|
final isCurrent = primaryTp != null && (primaryTp - targetPrice).abs() < 0.001;
|
|
|
|
final targetMove = (isShort ? (entry - targetPrice) : (targetPrice - entry));
|
|
final netTargetProfit = targetMove * quantity;
|
|
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 = entry > 0 ? (targetMove / entry * 100).abs() : 0.0;
|
|
|
|
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${stage.stageNumber}',
|
|
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)),
|
|
],
|
|
);
|
|
}
|
|
}
|