feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/asset_logo_widget.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../models/bot_models.dart';
|
||||
|
||||
class BotPositionCard extends StatelessWidget {
|
||||
final BotTradeOrderModel position;
|
||||
final VoidCallback? onClosePressed;
|
||||
|
||||
const BotPositionCard({
|
||||
super.key,
|
||||
required this.position,
|
||||
this.onClosePressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLong = position.isLong;
|
||||
final isProfitable = position.unrealizedPnlEur >= 0;
|
||||
final pnlPercent = position.pnlPercent;
|
||||
final rMult = position.rMultiple;
|
||||
|
||||
return GlassContainer(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AssetLogoWidget(
|
||||
symbolOrName: position.symbol.isNotEmpty ? position.symbol : position.isin,
|
||||
imageUrl: '/api/v1/logo/${position.isin}',
|
||||
size: 36,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
position.symbol,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: isLong ? AppTheme.primaryEmerald.withValues(alpha: 0.2) : AppTheme.accentRed.withValues(alpha: 0.2),
|
||||
),
|
||||
child: Text(
|
||||
position.direction.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isLong ? AppTheme.primaryEmerald : AppTheme.accentRed,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_buildVenueBadge(position.venue),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
position.isin,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${isProfitable ? '+' : ''}€${position.unrealizedPnlEur.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isProfitable ? AppTheme.primaryEmerald : AppTheme.accentRed,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${isProfitable ? '+' : ''}${pnlPercent.toStringAsFixed(2)}% (${rMult >= 0 ? '+' : ''}${rMult.toStringAsFixed(1)}R)',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isProfitable ? AppTheme.primaryEmerald : AppTheme.accentRed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1, color: Colors.white10),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildPriceInfo('Entry / Buy-In', '€${position.averageBuyIn > 0 ? position.averageBuyIn.toStringAsFixed(2) : position.entryPrice.toStringAsFixed(2)}'),
|
||||
_buildPriceInfo('Current Price', '€${position.currentPrice.toStringAsFixed(2)}'),
|
||||
_buildPriceInfo('Stop Loss', '€${position.currentStopLoss.toStringAsFixed(2)}'),
|
||||
_buildPriceInfo('Take Profit 1', '€${position.takeProfit1.toStringAsFixed(2)}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildDynamicStateBadge(position),
|
||||
if (position.isActive && onClosePressed != null)
|
||||
TextButton.icon(
|
||||
onPressed: onClosePressed,
|
||||
icon: Icon(Icons.close, size: 14, color: AppTheme.accentRed),
|
||||
label: Text('Glattstellen', style: TextStyle(fontSize: 12, color: AppTheme.accentRed)),
|
||||
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceInfo(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: const TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVenueBadge(String venue) {
|
||||
final isAlpaca = venue.toLowerCase().contains('alpaca');
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: isAlpaca ? Colors.blue.withValues(alpha: 0.15) : Colors.purple.withValues(alpha: 0.15),
|
||||
),
|
||||
child: Text(
|
||||
isAlpaca ? 'Alpaca US' : 'Synthetic KO',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isAlpaca ? Colors.lightBlueAccent : Colors.purpleAccent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDynamicStateBadge(BotTradeOrderModel position) {
|
||||
String text = 'Pending TP1';
|
||||
Color color = Colors.amber;
|
||||
|
||||
if (position.isBreakEven) {
|
||||
text = 'Free-Roll Active (BE)';
|
||||
color = AppTheme.primaryEmerald;
|
||||
} else if (position.isTp1Hit) {
|
||||
text = 'TP1 Hit (Trailing Active)';
|
||||
color = Colors.cyanAccent;
|
||||
} else if (position.isClosed) {
|
||||
text = 'Closed';
|
||||
color = Colors.grey;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: color.withValues(alpha: 0.15),
|
||||
border: Border.all(color: color.withValues(alpha: 0.4), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.bolt, size: 12, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user