feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core

This commit is contained in:
2026-08-24 21:37:43 +02:00
parent 676496b77d
commit 0894c40f07
113 changed files with 12413 additions and 3613 deletions
@@ -14,62 +14,25 @@ class TradeCalculationCard extends StatelessWidget {
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 entry = trade.averageBuyIn;
final quantity = trade.totalQuantity;
final posSize = entry * quantity;
final isShort = !trade.direction.isLong;
final quantity = entry > 0 ? (posSize / entry) : 0.0;
// 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;
// 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;
// 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;
// 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 crv = (rewardAmountAbs != null && riskAmountAbs > 0) ? (rewardAmountAbs / riskAmountAbs) : null;
final content = Container(
padding: const EdgeInsets.all(14),
@@ -85,47 +48,33 @@ class TradeCalculationCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_statTile(
'Stückzahl (Basiswert)',
'Stückzahl',
'${quantity.toStringAsFixed(2)} Stk.',
Colors.white,
),
_statTile(
'Max. Verlust (SL)',
'Risiko bis Stop-Loss',
'-€${riskAmountAbs.toStringAsFixed(2)}',
AppTheme.accentRed,
),
_statTile(
'Gewinn-Potenzial (TP)',
'+€${rewardAmountAbs.toStringAsFixed(2)}',
'Potenzial bis 1. Ziel',
rewardAmountAbs != null ? '+€${rewardAmountAbs.toStringAsFixed(2)}' : 'Trailing-Exit',
AppTheme.primaryEmerald,
),
_statTile(
'Chance-Risiko (CRV)',
crv > 0 ? '1 : ${crv.toStringAsFixed(2)}' : '-',
crv != null ? '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,
),
),
],
Text(
'Investiertes Kapital: €${posSize.toStringAsFixed(2)} • Exit-Strategie: ${trade.exitPlan.strategyType.label}',
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
),
if (targets.length > 1) ...[
if (stages.length > 1) ...[
const Divider(color: Colors.white10, height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -140,30 +89,24 @@ class TradeCalculationCard extends StatelessWidget {
),
),
Text(
'${targets.length} Ziele',
'${stages.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;
...stages.map((stage) {
final targetPrice = stage.targetPrice;
final isCurrent = primaryTp != null && (primaryTp - 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 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 = (targetMovePct * 100).abs();
final baseMove = entry > 0 ? (targetMove / entry * 100).abs() : 0.0;
return Container(
margin: const EdgeInsets.only(bottom: 6),
@@ -195,7 +138,7 @@ class TradeCalculationCard extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Text(
'TP${idx + 1}',
'TP${stage.stageNumber}',
style: TextStyle(
color: isCurrent ? Colors.black : Colors.white,
fontSize: 10,