feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user