feat(App): update Finlytic Flutter app UI and blocs
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:finlytic_app/features/trades/repositories/trade_repository.dart';
|
||||
import 'trade_event.dart';
|
||||
import 'trade_state.dart';
|
||||
|
||||
class TradeBloc extends Bloc<TradeEvent, TradeState> {
|
||||
final TradeRepository repository;
|
||||
|
||||
TradeBloc({required this.repository}) : super(TradeInitial()) {
|
||||
on<FetchTrades>(_onFetchTrades);
|
||||
on<CloseTrade>(_onCloseTrade);
|
||||
on<AcceptTradeProposalEvent>(_onAcceptTradeProposal);
|
||||
}
|
||||
|
||||
Future<void> _onFetchTrades(FetchTrades event, Emitter<TradeState> emit) async {
|
||||
emit(TradeLoading());
|
||||
try {
|
||||
final trades = await repository.fetchTrades(isin: event.isin, status: event.status);
|
||||
emit(TradeLoaded(trades));
|
||||
} catch (e) {
|
||||
emit(const TradeError("Trades konnten nicht geladen werden"));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCloseTrade(CloseTrade event, Emitter<TradeState> emit) async {
|
||||
emit(TradeLoading());
|
||||
try {
|
||||
await repository.closeTrade(event.tradeId);
|
||||
final trades = await repository.fetchTrades();
|
||||
emit(TradeLoaded(trades));
|
||||
} catch (e) {
|
||||
emit(const TradeError("Position konnte nicht geschlossen werden"));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAcceptTradeProposal(AcceptTradeProposalEvent event, Emitter<TradeState> emit) async {
|
||||
emit(TradeLoading());
|
||||
try {
|
||||
await repository.acceptTrade(event.dto);
|
||||
final trades = await repository.fetchTrades();
|
||||
emit(TradeLoaded(trades));
|
||||
} catch (e) {
|
||||
emit(const TradeError("Trade konnte nicht akzeptiert werden"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../models/trade_acceptance_dto.dart';
|
||||
|
||||
abstract class TradeEvent extends Equatable {
|
||||
const TradeEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class FetchTrades extends TradeEvent {
|
||||
final String? status;
|
||||
final String? isin;
|
||||
|
||||
const FetchTrades({this.status, this.isin});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, isin];
|
||||
}
|
||||
|
||||
class CloseTrade extends TradeEvent {
|
||||
final String tradeId;
|
||||
|
||||
const CloseTrade(this.tradeId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tradeId];
|
||||
}
|
||||
|
||||
class AcceptTradeProposalEvent extends TradeEvent {
|
||||
final TradeAcceptanceDto dto;
|
||||
|
||||
const AcceptTradeProposalEvent(this.dto);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [dto];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:finlytic_app/features/trades/models/trade_model.dart';
|
||||
|
||||
abstract class TradeState extends Equatable {
|
||||
const TradeState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class TradeInitial extends TradeState {}
|
||||
|
||||
class TradeLoading extends TradeState {}
|
||||
|
||||
class TradeLoaded extends TradeState {
|
||||
final List<TradeModel> trades;
|
||||
|
||||
const TradeLoaded(this.trades);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [trades];
|
||||
}
|
||||
|
||||
class TradeError extends TradeState {
|
||||
final String message;
|
||||
|
||||
const TradeError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
class TradeAcceptanceDto {
|
||||
final String userId;
|
||||
final String tradeId;
|
||||
final String analysisId;
|
||||
final String isin;
|
||||
final String symbol;
|
||||
final double? actualEntryPrice;
|
||||
final double? positionSize;
|
||||
final double? leverageUsed;
|
||||
final double? entryFee;
|
||||
final double? exitFee;
|
||||
final double? quantity;
|
||||
final double? knockoutThreshold;
|
||||
final bool isRecurring;
|
||||
final DateTime executionTimestamp;
|
||||
|
||||
final String? signalType;
|
||||
final double? entryPrice;
|
||||
final double? stopLoss;
|
||||
final double? takeProfit;
|
||||
final String? instrumentType;
|
||||
final String? timeframe;
|
||||
final String? reasoning;
|
||||
|
||||
TradeAcceptanceDto({
|
||||
required this.userId,
|
||||
required this.tradeId,
|
||||
this.analysisId = '',
|
||||
required this.isin,
|
||||
required this.symbol,
|
||||
this.actualEntryPrice,
|
||||
this.positionSize,
|
||||
this.leverageUsed,
|
||||
this.entryFee,
|
||||
this.exitFee,
|
||||
this.quantity,
|
||||
this.knockoutThreshold,
|
||||
this.isRecurring = false,
|
||||
required this.executionTimestamp,
|
||||
this.signalType,
|
||||
this.entryPrice,
|
||||
this.stopLoss,
|
||||
this.takeProfit,
|
||||
this.instrumentType,
|
||||
this.timeframe,
|
||||
this.reasoning,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'userId': userId.isNotEmpty ? userId : 'default_user',
|
||||
'tradeId': tradeId,
|
||||
'analysisId': analysisId,
|
||||
'isin': isin,
|
||||
'symbol': symbol,
|
||||
'actualEntryPrice': actualEntryPrice,
|
||||
'positionSize': positionSize,
|
||||
'leverageUsed': leverageUsed,
|
||||
'entryFee': entryFee,
|
||||
'exitFee': exitFee,
|
||||
'quantity': quantity,
|
||||
'knockoutThreshold': knockoutThreshold,
|
||||
'isRecurring': isRecurring,
|
||||
'executionTimestamp': executionTimestamp.toIso8601String(),
|
||||
'signalType': signalType,
|
||||
'entryPrice': entryPrice,
|
||||
'stopLoss': stopLoss,
|
||||
'takeProfit': takeProfit,
|
||||
'instrumentType': instrumentType,
|
||||
'timeframe': timeframe,
|
||||
'reasoning': reasoning,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class TradeModel extends Equatable {
|
||||
final String id;
|
||||
final String analysisId;
|
||||
final String status;
|
||||
final bool isGlobalProposal;
|
||||
final String userId;
|
||||
final String symbol;
|
||||
final String isin;
|
||||
final String companyName;
|
||||
final String sector;
|
||||
final String signalType;
|
||||
final double entryPrice;
|
||||
final double actualEntryPrice;
|
||||
final double currentPrice;
|
||||
final double stopLoss;
|
||||
final double takeProfit;
|
||||
final double positionSize;
|
||||
final double leverageUsed;
|
||||
final double pnlAbsolute;
|
||||
final double pnlPercent;
|
||||
final String reasoning;
|
||||
final String technicalRationale;
|
||||
final String fundamentalRationale;
|
||||
final String riskWarning;
|
||||
final double winRate;
|
||||
final String timeframe;
|
||||
final String instrumentType;
|
||||
final DateTime? createdAt;
|
||||
|
||||
final String riskTolerance;
|
||||
final double vixValue;
|
||||
final String vixRegime;
|
||||
final List<double> takeProfitTargets;
|
||||
final double maxLeverage;
|
||||
final double entryZoneMin;
|
||||
final double entryZoneMax;
|
||||
final double entryFee;
|
||||
final double exitFee;
|
||||
final double quantity;
|
||||
|
||||
const TradeModel({
|
||||
required this.id,
|
||||
this.analysisId = '',
|
||||
this.status = 'Active',
|
||||
this.isGlobalProposal = false,
|
||||
this.userId = '',
|
||||
required this.symbol,
|
||||
this.isin = '',
|
||||
this.companyName = '',
|
||||
this.sector = '',
|
||||
required this.signalType,
|
||||
required this.entryPrice,
|
||||
this.actualEntryPrice = 0.0,
|
||||
this.currentPrice = 0.0,
|
||||
required this.stopLoss,
|
||||
required this.takeProfit,
|
||||
this.positionSize = 0.0,
|
||||
this.leverageUsed = 1.0,
|
||||
this.pnlAbsolute = 0.0,
|
||||
this.pnlPercent = 0.0,
|
||||
this.reasoning = '',
|
||||
this.technicalRationale = '',
|
||||
this.fundamentalRationale = '',
|
||||
this.riskWarning = '',
|
||||
this.winRate = 50.0,
|
||||
this.timeframe = '1D',
|
||||
this.instrumentType = 'Stock',
|
||||
this.createdAt,
|
||||
this.riskTolerance = 'Moderate',
|
||||
this.vixValue = 0.0,
|
||||
this.vixRegime = 'Normal',
|
||||
this.takeProfitTargets = const [],
|
||||
this.maxLeverage = 1.0,
|
||||
this.entryZoneMin = 0.0,
|
||||
this.entryZoneMax = 0.0,
|
||||
this.entryFee = 0.0,
|
||||
this.exitFee = 0.0,
|
||||
this.quantity = 0.0,
|
||||
});
|
||||
|
||||
bool get isActive => status.toLowerCase() == 'active';
|
||||
bool get isClosed => status.toLowerCase() == 'closed';
|
||||
bool get isRejected => status.toLowerCase() == 'rejected';
|
||||
bool get isProposed => (status.toLowerCase() == 'proposed' || isGlobalProposal) && !isRejected && !isActive && !isClosed;
|
||||
|
||||
double get effectiveCurrentPrice {
|
||||
if (currentPrice > 0) return currentPrice;
|
||||
if (actualEntryPrice > 0) return actualEntryPrice;
|
||||
return entryPrice;
|
||||
}
|
||||
|
||||
double get calculatedPnlAbs {
|
||||
if (pnlAbsolute != 0) return pnlAbsolute;
|
||||
final entry = actualEntryPrice > 0 ? actualEntryPrice : entryPrice;
|
||||
final curr = effectiveCurrentPrice;
|
||||
if (entry <= 0) return 0.0;
|
||||
final isShort = signalType == 'SELL' || signalType == 'SHORT';
|
||||
final rawMove = isShort ? ((entry - curr) / entry) : ((curr - entry) / entry);
|
||||
final posSize = positionSize > 0 ? positionSize : entry;
|
||||
final lev = leverageUsed > 0 ? leverageUsed : 1.0;
|
||||
return (rawMove * posSize * lev);
|
||||
}
|
||||
|
||||
double get calculatedPnlPct {
|
||||
if (pnlPercent != 0) return pnlPercent;
|
||||
final pnlAbs = calculatedPnlAbs;
|
||||
final posSize = positionSize > 0 ? positionSize : (actualEntryPrice > 0 ? actualEntryPrice : entryPrice);
|
||||
if (posSize <= 0) return 0.0;
|
||||
return (pnlAbs / posSize) * 100.0;
|
||||
}
|
||||
|
||||
factory TradeModel.fromJson(Map<String, dynamic> json) {
|
||||
double parseDbl(dynamic val) {
|
||||
if (val == null) return 0.0;
|
||||
if (val is num) return val.toDouble();
|
||||
return double.tryParse(val.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
final idVal = (json['tradeId'] ?? json['TradeId'] ?? json['id'] ?? json['Id'])?.toString() ?? '';
|
||||
final sig = (json['signalType'] ?? json['SignalType'] ?? json['side'] ?? json['Side'])?.toString() ?? 'BUY';
|
||||
final entry = parseDbl(json['entryPrice'] ?? json['EntryPrice']);
|
||||
final actualEntry = parseDbl(json['actualEntryPrice'] ?? json['ActualEntryPrice']);
|
||||
final currPrice = parseDbl(json['currentPrice'] ?? json['CurrentPrice'] ?? json['price'] ?? json['Price']);
|
||||
final sl = parseDbl(json['stopLoss'] ?? json['StopLoss']);
|
||||
final tp = parseDbl(json['takeProfit'] ?? json['TakeProfit']);
|
||||
final pnlAbs = parseDbl(json['pnlAbsolute'] ?? json['PnlAbsolute'] ?? json['pnl'] ?? json['Pnl']);
|
||||
final pnlPct = parseDbl(json['pnlPercent'] ?? json['PnlPercent']);
|
||||
|
||||
DateTime? dt;
|
||||
final createdStr = (json['createdAt'] ?? json['CreatedAt'])?.toString();
|
||||
if (createdStr != null && createdStr.isNotEmpty) {
|
||||
dt = DateTime.tryParse(createdStr);
|
||||
}
|
||||
|
||||
return TradeModel(
|
||||
id: idVal,
|
||||
analysisId: (json['analysisId'] ?? json['AnalysisId'])?.toString() ?? '',
|
||||
status: (json['status'] ?? json['Status'])?.toString() ?? 'Active',
|
||||
isGlobalProposal: json['isGlobalProposal'] == true || json['IsGlobalProposal'] == true,
|
||||
userId: (json['userId'] ?? json['UserId'])?.toString() ?? '',
|
||||
symbol: (json['symbol'] ?? json['Symbol'])?.toString() ?? 'UNKNOWN',
|
||||
isin: (json['isin'] ?? json['Isin'])?.toString() ?? '',
|
||||
companyName: (json['companyName'] ?? json['CompanyName'])?.toString() ?? '',
|
||||
sector: (json['sector'] ?? json['Sector'])?.toString() ?? '',
|
||||
signalType: sig.toUpperCase(),
|
||||
entryPrice: entry,
|
||||
actualEntryPrice: actualEntry,
|
||||
currentPrice: currPrice,
|
||||
stopLoss: sl,
|
||||
takeProfit: tp,
|
||||
positionSize: parseDbl(json['positionSize'] ?? json['PositionSize']),
|
||||
leverageUsed: parseDbl(json['leverageUsed'] ?? json['LeverageUsed']) == 0 ? 1.0 : parseDbl(json['leverageUsed'] ?? json['LeverageUsed']),
|
||||
pnlAbsolute: pnlAbs,
|
||||
pnlPercent: pnlPct,
|
||||
reasoning: (json['reasoning'] ?? json['Reasoning'])?.toString() ?? '',
|
||||
technicalRationale: (json['technicalRationale'] ?? json['TechnicalRationale'])?.toString() ?? '',
|
||||
fundamentalRationale: (json['fundamentalRationale'] ?? json['FundamentalRationale'])?.toString() ?? '',
|
||||
riskWarning: (json['riskWarning'] ?? json['RiskWarning'])?.toString() ?? '',
|
||||
winRate: parseDbl(json['winRate'] ?? json['WinRate']),
|
||||
timeframe: (json['timeframe'] ?? json['Timeframe'])?.toString() ?? '1D',
|
||||
instrumentType: (json['instrumentType'] ?? json['InstrumentType'])?.toString() ?? 'Stock',
|
||||
createdAt: dt,
|
||||
riskTolerance: (json['riskTolerance'] ?? json['RiskTolerance'])?.toString() ?? 'Moderate',
|
||||
vixValue: parseDbl(json['vixValue'] ?? json['VixValue']),
|
||||
vixRegime: (json['vixRegime'] ?? json['VixRegime'])?.toString() ?? 'Normal',
|
||||
takeProfitTargets: (json['takeProfitTargets'] ?? json['TakeProfitTargets']) is List
|
||||
? ((json['takeProfitTargets'] ?? json['TakeProfitTargets']) as List).map((e) => parseDbl(e)).toList()
|
||||
: [],
|
||||
maxLeverage: parseDbl(json['maxLeverage'] ?? json['MaxLeverage']),
|
||||
entryZoneMin: parseDbl(json['entryZoneMin'] ?? json['EntryZoneMin']),
|
||||
entryZoneMax: parseDbl(json['entryZoneMax'] ?? json['EntryZoneMax']),
|
||||
entryFee: parseDbl(json['entryFee'] ?? json['EntryFee']),
|
||||
exitFee: parseDbl(json['exitFee'] ?? json['ExitFee']),
|
||||
quantity: parseDbl(json['quantity'] ?? json['Quantity']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tradeId': id,
|
||||
'analysisId': analysisId,
|
||||
'status': status,
|
||||
'isGlobalProposal': isGlobalProposal,
|
||||
'userId': userId,
|
||||
'symbol': symbol,
|
||||
'isin': isin,
|
||||
'companyName': companyName,
|
||||
'sector': sector,
|
||||
'signalType': signalType,
|
||||
'entryPrice': entryPrice,
|
||||
'actualEntryPrice': actualEntryPrice,
|
||||
'currentPrice': currentPrice,
|
||||
'stopLoss': stopLoss,
|
||||
'takeProfit': takeProfit,
|
||||
'positionSize': positionSize,
|
||||
'leverageUsed': leverageUsed,
|
||||
'pnlAbsolute': pnlAbsolute,
|
||||
'pnlPercent': pnlPercent,
|
||||
'reasoning': reasoning,
|
||||
'technicalRationale': technicalRationale,
|
||||
'fundamentalRationale': fundamentalRationale,
|
||||
'riskWarning': riskWarning,
|
||||
'winRate': winRate,
|
||||
'timeframe': timeframe,
|
||||
'instrumentType': instrumentType,
|
||||
'createdAt': createdAt?.toIso8601String(),
|
||||
'riskTolerance': riskTolerance,
|
||||
'vixValue': vixValue,
|
||||
'vixRegime': vixRegime,
|
||||
'takeProfitTargets': takeProfitTargets,
|
||||
'maxLeverage': maxLeverage,
|
||||
'entryZoneMin': entryZoneMin,
|
||||
'entryZoneMax': entryZoneMax,
|
||||
'entryFee': entryFee,
|
||||
'exitFee': exitFee,
|
||||
'quantity': quantity,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
analysisId,
|
||||
status,
|
||||
isGlobalProposal,
|
||||
userId,
|
||||
symbol,
|
||||
isin,
|
||||
signalType,
|
||||
entryPrice,
|
||||
currentPrice,
|
||||
pnlAbsolute,
|
||||
pnlPercent,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'dart:async';
|
||||
import 'package:finlytic_app/core/network/api_client.dart';
|
||||
import 'package:finlytic_app/features/trades/models/trade_model.dart';
|
||||
import 'package:finlytic_app/features/trades/models/trade_acceptance_dto.dart';
|
||||
|
||||
class TradeRepository {
|
||||
final ApiClient apiClient;
|
||||
|
||||
TradeRepository({required this.apiClient});
|
||||
|
||||
Future<List<TradeModel>> fetchTrades({String? isin, String? status}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (isin != null && isin.isNotEmpty) queryParams['isin'] = isin;
|
||||
if (status != null && status.isNotEmpty) queryParams['status'] = status;
|
||||
|
||||
final response = await apiClient.get('/api/v1/trades', queryParameters: queryParams);
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((json) => TradeModel.fromJson(json)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
print('Error fetching trades: $e');
|
||||
throw Exception('Trades konnten nicht geladen werden');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> acceptTrade(TradeAcceptanceDto dto) async {
|
||||
final response = await apiClient.post('/api/v1/user/trades/accept', data: dto.toJson());
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Trade konnte nicht akzeptiert werden');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeTrade(String id) async {
|
||||
final response = await apiClient.post('/api/v1/user/trades/$id/close');
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Trade konnte nicht geschlossen werden');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/network/signalr_service.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../../auth/bloc/auth_bloc.dart';
|
||||
|
||||
import '../bloc/trade_bloc.dart';
|
||||
import '../bloc/trade_event.dart';
|
||||
import '../bloc/trade_state.dart';
|
||||
import '../models/trade_model.dart';
|
||||
import '../models/trade_acceptance_dto.dart';
|
||||
import '../repositories/trade_repository.dart';
|
||||
import '../widgets/trade_card.dart';
|
||||
import '../widgets/proposed_auto_trades_card.dart';
|
||||
import '../widgets/trade_acceptance_dialog.dart';
|
||||
import '../widgets/trade_execution_dialog.dart';
|
||||
|
||||
class TradesFeedScreen extends StatelessWidget {
|
||||
final ApiClient apiClient;
|
||||
final SignalRService signalRService;
|
||||
|
||||
const TradesFeedScreen({
|
||||
super.key,
|
||||
required this.apiClient,
|
||||
required this.signalRService,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => TradeBloc(
|
||||
repository: TradeRepository(apiClient: apiClient),
|
||||
)..add(const FetchTrades()),
|
||||
child: const _TradesFeedScreenContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TradesFeedScreenContent extends StatefulWidget {
|
||||
const _TradesFeedScreenContent();
|
||||
|
||||
@override
|
||||
State<_TradesFeedScreenContent> createState() => _TradesFeedScreenContentState();
|
||||
}
|
||||
|
||||
class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
String _selectedFilter = 'Alle'; // 'Alle', 'Offen', 'Vorschläge', 'Geschlossen'
|
||||
String _searchQuery = '';
|
||||
final TextEditingController _searchCtrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleAcceptProposal(BuildContext context, TradeModel trade) async {
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
final currentUserId = (authState is Authenticated) ? authState.user.userId : 'default_user';
|
||||
|
||||
final result = await showDialog<TradeAcceptanceDto>(
|
||||
context: context,
|
||||
builder: (ctx) => TradeAcceptanceDialog(
|
||||
trade: trade,
|
||||
theme: AppTheme.darkClassic,
|
||||
userId: currentUserId,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && mounted) {
|
||||
context.read<TradeBloc>().add(AcceptTradeProposalEvent(result));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Trade wird in dein Portfolio übernommen...'),
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.darkBackground,
|
||||
body: SafeArea(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<TradeBloc>().add(const FetchTrades());
|
||||
},
|
||||
color: AppTheme.primaryEmerald,
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title & Reload Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Live Portfolio & Trading',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'KI-Erkennungen, Vorschläge & Aktive Positionen',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.read<TradeBloc>().add(const FetchTrades());
|
||||
},
|
||||
icon: const Icon(Icons.refresh, color: Colors.white70),
|
||||
tooltip: 'Trades Aktualisieren',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Main Content Body
|
||||
Expanded(
|
||||
child: BlocBuilder<TradeBloc, TradeState>(
|
||||
builder: (context, state) {
|
||||
if (state is TradeLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: AppTheme.primaryEmerald),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is TradeError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 48, color: AppTheme.accentRed),
|
||||
const SizedBox(height: 12),
|
||||
Text(state.message, style: TextStyle(color: AppTheme.textMuted)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.read<TradeBloc>().add(const FetchTrades()),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryEmerald),
|
||||
child: const Text('Erneut Versuchen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is TradeLoaded) {
|
||||
final allTrades = state.trades;
|
||||
|
||||
// Separate proposals, active, closed, and rejected trades
|
||||
final proposals = allTrades.where((t) => t.isProposed).toList();
|
||||
final activeTrades = allTrades.where((t) => t.isActive).toList();
|
||||
final closedTrades = allTrades.where((t) => t.isClosed).toList();
|
||||
final rejectedTrades = allTrades.where((t) => t.isRejected).toList();
|
||||
|
||||
// Performance Header Calculations
|
||||
final totalOpenPnlAbs = activeTrades.fold<double>(0, (sum, t) => sum + t.pnlAbsolute);
|
||||
final isPnlPos = totalOpenPnlAbs >= 0;
|
||||
final winRatePct = allTrades.isNotEmpty
|
||||
? (allTrades.where((t) => t.pnlAbsolute >= 0).length / allTrades.length * 100)
|
||||
: 0.0;
|
||||
|
||||
// Filter list according to tab & search
|
||||
List<TradeModel> filteredList = allTrades;
|
||||
if (_selectedFilter == 'Alle') {
|
||||
filteredList = allTrades.where((t) => !t.isProposed).toList();
|
||||
} else if (_selectedFilter == 'Offen') {
|
||||
filteredList = activeTrades;
|
||||
} else if (_selectedFilter == 'Vorschläge') {
|
||||
filteredList = proposals;
|
||||
} else if (_selectedFilter == 'Geschlossen') {
|
||||
filteredList = closedTrades;
|
||||
} else if (_selectedFilter == 'Abgelehnt') {
|
||||
filteredList = rejectedTrades;
|
||||
}
|
||||
|
||||
if (_searchQuery.trim().isNotEmpty) {
|
||||
final q = _searchQuery.toLowerCase().trim();
|
||||
filteredList = filteredList.where((t) =>
|
||||
t.symbol.toLowerCase().contains(q) ||
|
||||
t.isin.toLowerCase().contains(q) ||
|
||||
t.companyName.toLowerCase().contains(q)).toList();
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
// 1. Performance Overview Bar
|
||||
GlassContainer(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_summaryStat('Offene Trades', '${activeTrades.length}', AppTheme.primaryEmerald),
|
||||
_summaryStat(
|
||||
'Offenes PnL',
|
||||
'${isPnlPos ? '+' : ''}${totalOpenPnlAbs.toStringAsFixed(2)} €',
|
||||
isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed,
|
||||
),
|
||||
_summaryStat('Trefferquote', '${winRatePct.toStringAsFixed(0)}%', Colors.amber),
|
||||
_summaryStat('Auto-Vorschläge', '${proposals.length}', AppTheme.accentCyan),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Featured Card: Auto KI Trade Proposals
|
||||
ProposedAutoTradesCard(
|
||||
proposals: proposals,
|
||||
onAcceptProposal: (trade) => _handleAcceptProposal(context, trade),
|
||||
),
|
||||
|
||||
// 3. Search & Filter Section
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_searchQuery = val;
|
||||
});
|
||||
},
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Suche nach Symbol, ISIN oder Name...',
|
||||
hintStyle: TextStyle(color: AppTheme.textMuted, fontSize: 13),
|
||||
prefixIcon: const Icon(Icons.search, size: 18, color: Colors.white54),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Filter Chips Row
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_filterChip('Alle', allTrades.length),
|
||||
_filterChip('Offen', activeTrades.length),
|
||||
_filterChip('Vorschläge', proposals.length),
|
||||
_filterChip('Abgelehnt', rejectedTrades.length),
|
||||
_filterChip('Geschlossen', closedTrades.length),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 4. Trades List
|
||||
if (filteredList.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.inbox, size: 40, color: AppTheme.textMuted),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Keine Trades in der Kategorie "$_selectedFilter" gefunden.',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final trade = filteredList[index];
|
||||
return TradeCard(
|
||||
trade: trade,
|
||||
onAccept: () => _handleAcceptProposal(context, trade),
|
||||
onSettings: () => _showTradeSettingsDialog(context, trade),
|
||||
onClose: () {
|
||||
context.read<TradeBloc>().add(CloseTrade(trade.id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Position wird geschlossen...')),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _summaryStat(String label, String value, Color valColor) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
|
||||
const SizedBox(height: 3),
|
||||
Text(value, style: TextStyle(color: valColor, fontWeight: FontWeight.bold, fontSize: 15)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _filterChip(String label, int count) {
|
||||
final isSelected = _selectedFilter == label;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedFilter = label;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppTheme.primaryEmerald : Colors.white.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppTheme.primaryEmerald : Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.black.withValues(alpha: 0.2) : Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTradeSettingsDialog(BuildContext context, TradeModel trade) {
|
||||
final tradeBloc = context.read<TradeBloc>();
|
||||
|
||||
TradeExecutionDialog.show(
|
||||
context,
|
||||
trade: trade,
|
||||
defaultSymbol: trade.symbol,
|
||||
isActive: true,
|
||||
onAccept: (dto) {
|
||||
tradeBloc.add(AcceptTradeProposalEvent(dto));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Einstellungen für ${trade.symbol} gespeichert.'),
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../models/trade_model.dart';
|
||||
import 'trade_detail_modal.dart';
|
||||
|
||||
/// Premium Design for the AI Auto Screener Recommendations in the Live Trades Feed.
|
||||
class ProposedAutoTradesCard extends StatelessWidget {
|
||||
final List<TradeModel> proposals;
|
||||
final Function(TradeModel) onAcceptProposal;
|
||||
|
||||
const ProposedAutoTradesCard({
|
||||
super.key,
|
||||
required this.proposals,
|
||||
required this.onAcceptProposal,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (proposals.isEmpty) {
|
||||
return GlassContainer(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryEmerald.withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryEmerald.withValues(alpha: 0.3),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(Icons.auto_awesome, color: AppTheme.primaryEmerald, size: 24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'KI Screener Aktiv',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Die KI durchsucht den Markt kontinuierlich nach hoch-profitablen Mustern. Aktuell keine neuen Empfehlungen.',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Top proposal
|
||||
final topProposal = proposals.first;
|
||||
final isBuy = topProposal.signalType.toUpperCase() == 'BUY' || topProposal.signalType.toUpperCase() == 'LONG';
|
||||
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: signalColor.withValues(alpha: 0.15),
|
||||
blurRadius: 30,
|
||||
spreadRadius: -5,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.white.withValues(alpha: 0.08),
|
||||
signalColor.withValues(alpha: 0.05),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Premium Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppTheme.primaryEmerald, AppTheme.accentCyan],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(color: AppTheme.accentCyan.withValues(alpha: 0.4), blurRadius: 12, spreadRadius: 2),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.bolt, color: Colors.white, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'KI Asset Empfehlung',
|
||||
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16, color: Colors.white, letterSpacing: 0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.amber.withValues(alpha: 0.2), blurRadius: 8),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, size: 14, color: Colors.amber),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Score: ${topProposal.winRate.toStringAsFixed(0)}%',
|
||||
style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Asset Details with Modern Typography
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: signalColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: signalColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
topProposal.signalType.toUpperCase(),
|
||||
style: TextStyle(color: signalColor, fontWeight: FontWeight.w900, fontSize: 14, letterSpacing: 1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
topProposal.symbol.isNotEmpty && topProposal.symbol != 'UNKNOWN'
|
||||
? topProposal.symbol
|
||||
: (topProposal.companyName.isNotEmpty && topProposal.companyName != 'UNKNOWN' ? topProposal.companyName : (topProposal.isin.isNotEmpty ? topProposal.isin : 'Aktie')),
|
||||
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 24, color: Colors.white, height: 1.1),
|
||||
),
|
||||
if (topProposal.companyName.isNotEmpty && topProposal.companyName != topProposal.symbol && topProposal.companyName != 'UNKNOWN')
|
||||
Text(
|
||||
topProposal.companyName,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (topProposal.reasoning.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.25),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.lightbulb_outline, size: 18, color: AppTheme.accentCyan.withValues(alpha: 0.8)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
topProposal.reasoning,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Premium Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppTheme.accentCyan.withValues(alpha: 0.5)),
|
||||
color: AppTheme.accentCyan.withValues(alpha: 0.1),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => TradeDetailModal.show(
|
||||
context,
|
||||
trade: topProposal,
|
||||
onAccept: () => onAcceptProposal(topProposal),
|
||||
),
|
||||
icon: Icon(Icons.analytics_outlined, color: AppTheme.accentCyan, size: 22),
|
||||
tooltip: 'KI-Analyse & Details',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.primaryEmerald.withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => onAcceptProposal(topProposal),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.insights, size: 20, color: Colors.black87),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Trade Setup Erstellen',
|
||||
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 15, letterSpacing: 0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../models/trade_acceptance_dto.dart';
|
||||
import '../models/trade_model.dart';
|
||||
|
||||
|
||||
class TradeAcceptanceDialog extends StatefulWidget {
|
||||
final TradeModel trade;
|
||||
final ThemePreset theme;
|
||||
final String userId;
|
||||
|
||||
const TradeAcceptanceDialog({
|
||||
super.key,
|
||||
required this.trade,
|
||||
required this.theme,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TradeAcceptanceDialog> createState() => _TradeAcceptanceDialogState();
|
||||
}
|
||||
|
||||
class _TradeAcceptanceDialogState extends State<TradeAcceptanceDialog> {
|
||||
late TextEditingController _entryPriceCtrl;
|
||||
late TextEditingController _positionSizeCtrl;
|
||||
late TextEditingController _leverageCtrl;
|
||||
late TextEditingController _entryFeeCtrl;
|
||||
late TextEditingController _exitFeeCtrl;
|
||||
late TextEditingController _quantityCtrl;
|
||||
late TextEditingController _stopLossCtrl;
|
||||
late TextEditingController _takeProfitCtrl;
|
||||
late TextEditingController _notesCtrl;
|
||||
bool _isRecurring = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_entryPriceCtrl = TextEditingController(text: widget.trade.entryPrice.toStringAsFixed(2));
|
||||
_positionSizeCtrl = TextEditingController(text: '1000');
|
||||
_leverageCtrl = TextEditingController(text: (widget.trade.maxLeverage > 0 ? widget.trade.maxLeverage : 1).toStringAsFixed(0));
|
||||
_stopLossCtrl = TextEditingController(text: widget.trade.stopLoss.toStringAsFixed(2));
|
||||
_takeProfitCtrl = TextEditingController(text: widget.trade.takeProfit.toStringAsFixed(2));
|
||||
_notesCtrl = TextEditingController();
|
||||
_entryFeeCtrl = TextEditingController(text: '0.00');
|
||||
_exitFeeCtrl = TextEditingController(text: '0.00');
|
||||
double price = widget.trade.entryPrice;
|
||||
_quantityCtrl = TextEditingController(text: (1000 / (price > 0 ? price : 1)).toStringAsFixed(4));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_entryPriceCtrl.dispose();
|
||||
_positionSizeCtrl.dispose();
|
||||
_leverageCtrl.dispose();
|
||||
_stopLossCtrl.dispose();
|
||||
_takeProfitCtrl.dispose();
|
||||
_notesCtrl.dispose();
|
||||
_entryFeeCtrl.dispose();
|
||||
_exitFeeCtrl.dispose();
|
||||
_quantityCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double? _parseNum(String text) {
|
||||
final cleaned = text.replaceAll(',', '.').trim();
|
||||
return double.tryParse(cleaned);
|
||||
}
|
||||
|
||||
void _recalcQuantity() {
|
||||
double posSize = _parseNum(_positionSizeCtrl.text) ?? 0;
|
||||
double price = _parseNum(_entryPriceCtrl.text) ?? 0;
|
||||
if (price > 0) {
|
||||
_quantityCtrl.text = (posSize / price).toStringAsFixed(4);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
width: 450,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.theme.cardSurface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: widget.theme.glassBorder, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.add_shopping_cart, color: widget.theme.primaryColor, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Trade Akzeptieren - ${widget.trade.symbol}',
|
||||
style: TextStyle(
|
||||
color: widget.theme.textPrimary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: Icon(Icons.close, color: widget.theme.textMuted),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildInputField('Kaufkurs / Entry Price', _entryPriceCtrl, Icons.attach_money, onChanged: (_) => _recalcQuantity()),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildInputField('Kapital (Position Size)', _positionSizeCtrl, Icons.account_balance_wallet, onChanged: (_) => _recalcQuantity())),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildInputField('Stückzahl (Quantity)', _quantityCtrl, Icons.format_list_numbered)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildInputField('Stop Loss', _stopLossCtrl, Icons.block)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildInputField('Take Profit', _takeProfitCtrl, Icons.trending_up)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildInputField('Hebel (Leverage)', _leverageCtrl, Icons.speed)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: Text('Sparplan', style: TextStyle(color: widget.theme.textPrimary, fontSize: 14)),
|
||||
value: _isRecurring,
|
||||
activeColor: widget.theme.primaryColor,
|
||||
checkColor: Colors.white,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_isRecurring = val ?? false;
|
||||
});
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInputField('Notizen', _notesCtrl, Icons.edit),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final dto = TradeAcceptanceDto(
|
||||
userId: widget.userId,
|
||||
tradeId: widget.trade.id,
|
||||
analysisId: widget.trade.analysisId,
|
||||
isin: widget.trade.isin,
|
||||
symbol: widget.trade.symbol,
|
||||
actualEntryPrice: _parseNum(_entryPriceCtrl.text),
|
||||
positionSize: _parseNum(_positionSizeCtrl.text),
|
||||
leverageUsed: _parseNum(_leverageCtrl.text) ?? 1.0,
|
||||
entryFee: _parseNum(_entryFeeCtrl.text) ?? 0.0,
|
||||
exitFee: _parseNum(_exitFeeCtrl.text) ?? 0.0,
|
||||
quantity: _parseNum(_quantityCtrl.text),
|
||||
isRecurring: _isRecurring,
|
||||
executionTimestamp: DateTime.now().toUtc(),
|
||||
signalType: widget.trade.signalType,
|
||||
entryPrice: widget.trade.entryPrice,
|
||||
stopLoss: widget.trade.stopLoss,
|
||||
takeProfit: widget.trade.takeProfit,
|
||||
instrumentType: widget.trade.instrumentType,
|
||||
timeframe: widget.trade.timeframe,
|
||||
reasoning: widget.trade.reasoning,
|
||||
);
|
||||
Navigator.of(context).pop(dto);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.theme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text('Jetzt Ausführen', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputField(String label, TextEditingController controller, IconData icon, {Function(String)? onChanged}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: widget.theme.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: controller,
|
||||
style: TextStyle(color: widget.theme.textPrimary),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(icon, color: widget.theme.textMuted, size: 18),
|
||||
filled: true,
|
||||
fillColor: widget.theme.darkBackground.withValues(alpha: 0.5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../models/trade_model.dart';
|
||||
import 'trade_detail_modal.dart';
|
||||
|
||||
class TradeCard extends StatelessWidget {
|
||||
final TradeModel trade;
|
||||
final VoidCallback? onAccept;
|
||||
final VoidCallback? onClose;
|
||||
final VoidCallback? onSettings;
|
||||
|
||||
const TradeCard({
|
||||
super.key,
|
||||
required this.trade,
|
||||
this.onAccept,
|
||||
this.onClose,
|
||||
this.onSettings,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isBuy = trade.signalType == 'BUY';
|
||||
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
final isProposed = trade.isProposed;
|
||||
final isActive = trade.isActive;
|
||||
final isClosed = trade.isClosed;
|
||||
|
||||
final pnlAbs = trade.calculatedPnlAbs;
|
||||
final pnlPct = trade.calculatedPnlPct;
|
||||
final isPnlPos = pnlAbs >= 0;
|
||||
final pnlColor = isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
|
||||
final currPrice = trade.effectiveCurrentPrice;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => TradeDetailModal.show(
|
||||
context,
|
||||
trade: trade,
|
||||
onAccept: onAccept,
|
||||
onClose: onClose,
|
||||
),
|
||||
child: GlassContainer(
|
||||
margin: const EdgeInsets.only(bottom: 14),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Row: Signal, Symbol, Status & Live PnL
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: signalColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: signalColor.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isBuy ? Icons.trending_up : Icons.trending_down,
|
||||
size: 14,
|
||||
color: signalColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
trade.signalType,
|
||||
style: TextStyle(color: signalColor, fontWeight: FontWeight.bold, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
trade.companyName.isNotEmpty && trade.companyName != 'UNKNOWN'
|
||||
? trade.companyName
|
||||
: (trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN' ? trade.symbol : (trade.isin.isNotEmpty ? trade.isin : 'Aktie')),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
|
||||
),
|
||||
if (trade.isin.isNotEmpty || (trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN' && trade.symbol != trade.companyName))
|
||||
Text(
|
||||
trade.isin.isNotEmpty ? trade.isin : trade.symbol,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isActive || isClosed)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: pnlColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: pnlColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${isPnlPos ? '+' : ''}${pnlAbs.toStringAsFixed(2)} €',
|
||||
style: TextStyle(color: pnlColor, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
Text(
|
||||
'${isPnlPos ? '+' : ''}${pnlPct.toStringAsFixed(2)}%',
|
||||
style: TextStyle(color: pnlColor, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (trade.isRejected)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentRed.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Text(
|
||||
'ABGELEHNT',
|
||||
style: TextStyle(color: AppTheme.accentRed, fontWeight: FontWeight.bold, fontSize: 10),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: const Text(
|
||||
'VORSCHLAG',
|
||||
style: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontSize: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Price Metrics Grid with Live Kurs
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_priceItem(
|
||||
isActive || isClosed ? 'Ausführung' : 'Ziel-Einstieg',
|
||||
trade.actualEntryPrice > 0
|
||||
? '${trade.actualEntryPrice.toStringAsFixed(2)} €'
|
||||
: (trade.entryPrice > 0 ? '${trade.entryPrice.toStringAsFixed(2)} €' : '-'),
|
||||
Colors.white
|
||||
),
|
||||
_priceItem('Live-Kurs', '${currPrice.toStringAsFixed(2)} €', AppTheme.accentCyan),
|
||||
_priceItem('Stop-Loss', '${trade.stopLoss.toStringAsFixed(2)} €', AppTheme.accentRed),
|
||||
_priceItem('Take-Profit', '${trade.takeProfit.toStringAsFixed(2)} €', AppTheme.primaryEmerald),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (trade.reasoning.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
trade.reasoning,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Footer Action Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${trade.instrumentType.isNotEmpty ? trade.instrumentType : "Stock"} • ${trade.timeframe.isNotEmpty ? trade.timeframe : "1D"}${trade.leverageUsed > 1 ? " • ${trade.leverageUsed.toStringAsFixed(0)}x Hebel" : ""}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
||||
),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => TradeDetailModal.show(
|
||||
context,
|
||||
trade: trade,
|
||||
onAccept: onAccept,
|
||||
onClose: onClose,
|
||||
),
|
||||
icon: Icon(Icons.info_outline, size: 18, color: AppTheme.accentCyan),
|
||||
tooltip: 'KI-Begründung & Details',
|
||||
),
|
||||
if (isProposed && onAccept != null) ...[
|
||||
const SizedBox(width: 6),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onAccept,
|
||||
icon: const Icon(Icons.check_circle_outline, size: 16),
|
||||
label: const Text('Trade Übernehmen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isActive && onClose != null) ...[
|
||||
const SizedBox(width: 6),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onClose,
|
||||
icon: Icon(Icons.close, size: 14, color: AppTheme.accentRed),
|
||||
label: Text('Position Schließen', style: TextStyle(color: AppTheme.accentRed, fontSize: 12)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppTheme.accentRed.withValues(alpha: 0.5)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isActive && onSettings != null) ...[
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
onPressed: onSettings,
|
||||
icon: const Icon(Icons.settings, size: 16, color: Colors.white),
|
||||
tooltip: 'Einstellungen',
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: AppTheme.glassSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _priceItem(String label, String val, Color valColor) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
|
||||
const SizedBox(height: 2),
|
||||
Text(val, style: TextStyle(color: valColor, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../models/trade_model.dart';
|
||||
|
||||
class TradeDetailModal extends StatelessWidget {
|
||||
final TradeModel trade;
|
||||
final VoidCallback? onAccept;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const TradeDetailModal({
|
||||
super.key,
|
||||
required this.trade,
|
||||
this.onAccept,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required TradeModel trade,
|
||||
VoidCallback? onAccept,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => TradeDetailModal(
|
||||
trade: trade,
|
||||
onAccept: onAccept,
|
||||
onClose: onClose,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isBuy = trade.signalType == 'BUY';
|
||||
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
final pnlAbs = trade.calculatedPnlAbs;
|
||||
final pnlPct = trade.calculatedPnlPct;
|
||||
final isPnlPos = pnlAbs >= 0;
|
||||
final pnlColor = isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
final currPrice = trade.effectiveCurrentPrice;
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardSurface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
blurRadius: 25,
|
||||
spreadRadius: 5,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle Bar
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
|
||||
// Modal Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: signalColor.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: signalColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
trade.signalType,
|
||||
style: TextStyle(color: signalColor, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN'
|
||||
? trade.symbol
|
||||
: (trade.companyName.isNotEmpty && trade.companyName != 'UNKNOWN' ? trade.companyName : (trade.isin.isNotEmpty ? trade.isin : 'Aktie')),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.white),
|
||||
),
|
||||
if (trade.companyName.isNotEmpty && trade.companyName != trade.symbol && trade.companyName != 'UNKNOWN')
|
||||
Text(
|
||||
trade.companyName,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
|
||||
),
|
||||
if (trade.isin.isNotEmpty || trade.sector.isNotEmpty)
|
||||
Text(
|
||||
'${trade.isin.isNotEmpty ? trade.isin : ""}${trade.isin.isNotEmpty && trade.sector.isNotEmpty ? " • " : ""}${trade.sector.isNotEmpty ? trade.sector : ""}',
|
||||
style: TextStyle(color: AppTheme.textMuted.withValues(alpha: 0.7), fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.star, size: 14, color: Colors.amber),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${trade.winRate.toStringAsFixed(0)}% Win-Rate',
|
||||
style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Divider(color: Colors.white10, height: 1),
|
||||
|
||||
// Scrollable Content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Price Grid
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_metricItem(
|
||||
trade.isActive || trade.isClosed ? 'Ausführung' : 'Ziel-Einstieg',
|
||||
trade.actualEntryPrice > 0
|
||||
? '${trade.actualEntryPrice.toStringAsFixed(2)} €'
|
||||
: (trade.entryPrice > 0 ? '${trade.entryPrice.toStringAsFixed(2)} €' : '-'),
|
||||
Colors.white
|
||||
),
|
||||
_metricItem('Live-Kurs', '${currPrice.toStringAsFixed(2)} €', AppTheme.accentCyan),
|
||||
_metricItem('Stop-Loss', '${trade.stopLoss.toStringAsFixed(2)} €', AppTheme.accentRed),
|
||||
_metricItem('Take-Profit', '${trade.takeProfit.toStringAsFixed(2)} €', AppTheme.primaryEmerald),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (trade.isActive || trade.isClosed) ...[
|
||||
const SizedBox(height: 14),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: pnlColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: pnlColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Aktueller PnL:', style: TextStyle(color: Colors.white70, fontSize: 13)),
|
||||
Text(
|
||||
'${isPnlPos ? '+' : ''}${pnlAbs.toStringAsFixed(2)} € (${isPnlPos ? '+' : ''}${pnlPct.toStringAsFixed(2)}%)',
|
||||
style: TextStyle(color: pnlColor, fontWeight: FontWeight.bold, fontSize: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// AI Reasoning Section
|
||||
if (trade.reasoning.isNotEmpty) ...[
|
||||
_sectionTitle(Icons.auto_awesome, 'KI-Gesamteinschätzung & Begründung', AppTheme.primaryEmerald),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryEmerald.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.primaryEmerald.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Text(
|
||||
trade.reasoning,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
|
||||
// Technical Rationale Section
|
||||
if (trade.technicalRationale.isNotEmpty) ...[
|
||||
_sectionTitle(Icons.show_chart, 'Technische Analyse & Indikatoren', AppTheme.accentCyan),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.03),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
||||
),
|
||||
child: Text(
|
||||
trade.technicalRationale,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13, height: 1.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
|
||||
// Fundamental Rationale Section
|
||||
if (trade.fundamentalRationale.isNotEmpty) ...[
|
||||
_sectionTitle(Icons.account_balance, 'Fundamentale Bewertung', Colors.purpleAccent),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.03),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
||||
),
|
||||
child: Text(
|
||||
trade.fundamentalRationale,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13, height: 1.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
|
||||
// Risk Warning Section
|
||||
if (trade.riskWarning.isNotEmpty) ...[
|
||||
_sectionTitle(Icons.warning_amber_rounded, 'Risikohinweis & Marktumfeld', AppTheme.accentRed),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentRed.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
trade.riskWarning,
|
||||
style: TextStyle(color: AppTheme.accentRed, fontSize: 12, height: 1.4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
|
||||
// Trade Parameters Grid
|
||||
_sectionTitle(Icons.tune, 'Trade-Parameter & Instrument', Colors.white70),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.02),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.06)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_paramRow('Instrument Typ:', trade.instrumentType.isNotEmpty ? trade.instrumentType : 'Stock'),
|
||||
_paramRow('Zeithorizont:', trade.timeframe.isNotEmpty ? trade.timeframe : '1D'),
|
||||
if (trade.leverageUsed > 1) _paramRow('Hebel:', '${trade.leverageUsed.toStringAsFixed(0)}x'),
|
||||
if (trade.positionSize > 0) _paramRow('Positionsgröße:', '${trade.positionSize.toStringAsFixed(2)} €'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Footer Action Bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: Colors.white.withValues(alpha: 0.2)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text('Schließen', style: TextStyle(color: Colors.white70)),
|
||||
),
|
||||
),
|
||||
if (trade.isProposed && onAccept != null) ...[
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onAccept!();
|
||||
},
|
||||
icon: const Icon(Icons.check_circle_outline, size: 18),
|
||||
label: const Text('Trade Übernehmen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (trade.isActive && onClose != null) ...[
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onClose!();
|
||||
},
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
label: const Text('Position Schließen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentRed,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionTitle(IconData icon, String title, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _metricItem(String label, String value, Color color) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _paramRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 12)),
|
||||
Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:finlytic_app/core/theme/app_theme.dart';
|
||||
import 'package:finlytic_app/core/widgets/status_badge.dart';
|
||||
import '../../../../features/trades/models/trade_model.dart';
|
||||
import '../../../../features/trades/models/trade_acceptance_dto.dart';
|
||||
|
||||
class TradeExecutionDialog {
|
||||
static const double _defaultPositionSize = 1000.0;
|
||||
static const double _defaultLeverage = 1.0;
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required TradeModel trade,
|
||||
required String defaultSymbol,
|
||||
bool isActive = false,
|
||||
required Function(TradeAcceptanceDto dto) onAccept,
|
||||
Function(String tradeId)? onReject,
|
||||
}) {
|
||||
final initEntry = trade.actualEntryPrice > 0 ? trade.actualEntryPrice : (trade.entryPrice > 0 ? trade.entryPrice : 100.0);
|
||||
final initPos = trade.positionSize > 0 ? trade.positionSize : _defaultPositionSize;
|
||||
final initLev = trade.leverageUsed > 0 ? trade.leverageUsed : _defaultLeverage;
|
||||
final calcQty = (initEntry > 0 && initPos > 0) ? (initPos * initLev) / initEntry : 10.0;
|
||||
|
||||
// We don't have a direct quantity field in TradeModel, but we calculate it.
|
||||
// Let's use the explicit quantity if it exists, otherwise calculate it
|
||||
final initQty = trade.quantity > 0 ? trade.quantity : calcQty;
|
||||
|
||||
final actualEntryController = TextEditingController(text: initEntry.toStringAsFixed(2));
|
||||
final positionSizeController = TextEditingController(text: initPos.toStringAsFixed(2));
|
||||
final leverageController = TextEditingController(text: initLev.toStringAsFixed(1));
|
||||
final quantityController = TextEditingController(text: initQty.toStringAsFixed(4));
|
||||
|
||||
final entryFeeController = TextEditingController(text: trade.entryFee.toStringAsFixed(2));
|
||||
|
||||
final exitFeeController = TextEditingController(text: trade.exitFee.toStringAsFixed(2));
|
||||
|
||||
final slController = TextEditingController(text: trade.stopLoss.toString());
|
||||
final tpController = TextEditingController(text: trade.takeProfit.toString());
|
||||
|
||||
void recalculateQuantity() {
|
||||
final entry = double.tryParse(actualEntryController.text) ?? 0.0;
|
||||
final posSize = double.tryParse(positionSizeController.text) ?? 0.0;
|
||||
final lev = double.tryParse(leverageController.text) ?? 1.0;
|
||||
if (entry > 0 && posSize > 0) {
|
||||
final q = (posSize * lev) / entry;
|
||||
quantityController.text = q.toStringAsFixed(4);
|
||||
}
|
||||
}
|
||||
|
||||
actualEntryController.addListener(recalculateQuantity);
|
||||
positionSizeController.addListener(recalculateQuantity);
|
||||
leverageController.addListener(recalculateQuantity);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(isActive ? Icons.tune : Icons.edit_note_outlined, color: AppTheme.primaryEmerald, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(isActive ? 'Einstellungen für Trade #${trade.id}' : 'Trade-Ausführung & Parameter', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 580,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Trade-ID: ${trade.id} | Symbol/ISIN: ${trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol}', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final signal = trade.signalType.toUpperCase();
|
||||
final isLong = signal == 'BUY' || signal == 'LONG';
|
||||
final signalColor = isLong ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
|
||||
final entryZoneMin = trade.entryZoneMin;
|
||||
final entryZoneMax = trade.entryZoneMax;
|
||||
final entryPrice = trade.entryPrice;
|
||||
final stopLoss = trade.stopLoss;
|
||||
final takeProfit = trade.takeProfit;
|
||||
final takeProfitTargets = trade.takeProfitTargets;
|
||||
final crv = (takeProfit - entryPrice) / (entryPrice - stopLoss).abs();
|
||||
final maxLeverage = trade.maxLeverage;
|
||||
|
||||
final reasoning = trade.reasoning;
|
||||
final techRationale = trade.technicalRationale;
|
||||
final fundRationale = trade.fundamentalRationale;
|
||||
final riskWarning = trade.riskWarning;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: signalColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: signalColor, width: 1.5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
StatusBadge(label: isLong ? 'LONG / KAUFEN' : 'SHORT / VERKAUFEN', color: signalColor),
|
||||
const SizedBox(width: 8),
|
||||
if (trade.instrumentType.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(trade.instrumentType.toString(), style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (trade.winRate > 0) ...[
|
||||
Icon(Icons.bolt, size: 14, color: AppTheme.accentCyan),
|
||||
Text('Win-Rate: ${trade.winRate}%', style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Haltedauer: ${trade.timeframe.isNotEmpty ? trade.timeframe : '1-14 Tage'}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
Text('Risiko: ${trade.riskTolerance}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
if (trade.vixValue > 0)
|
||||
Text('VIX: ${_fmt(trade.vixValue)} (${trade.vixRegime})', style: TextStyle(color: Colors.orangeAccent, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const Divider(color: Colors.white12, height: 16),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTradeStat('Einstiegszone', entryZoneMin > 0 && entryZoneMax > 0 ? '€${_fmt(entryZoneMin)} - €${_fmt(entryZoneMax)}' : '€${_fmt(entryPrice)}', Colors.white),
|
||||
_buildTradeStat('Stop-Loss Target', '€${_fmt(stopLoss)}', AppTheme.accentRed),
|
||||
_buildTradeStat('Take-Profit Target', takeProfitTargets.isNotEmpty ? takeProfitTargets.map((t) => '€${_fmt(t)}').join(' / ') : '€${_fmt(takeProfit)}', AppTheme.primaryEmerald),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (crv > 0) _buildTradeStat('Chance-Risiko-Verh. (CRV)', '${_fmt(crv)}x', AppTheme.accentCyan),
|
||||
if (maxLeverage > 0) _buildTradeStat('Empf. Max Hebel', '${_fmt(maxLeverage)}x', Colors.orangeAccent),
|
||||
_buildTradeStat('Signal Typ', isLong ? 'LONG / BULLISH' : 'SHORT / BEARISH', signalColor),
|
||||
],
|
||||
),
|
||||
|
||||
if (reasoning.isNotEmpty || techRationale.isNotEmpty || fundRationale.isNotEmpty || riskWarning.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: Text('Ausführliche KI-Analyse & Begründung anzeigen', style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 12)),
|
||||
children: [
|
||||
if (reasoning.isNotEmpty) ...[
|
||||
_buildRationaleBlock('Gesamt-Strategie & KI-Entscheidung', reasoning, Colors.white70),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
if (techRationale.isNotEmpty) ...[
|
||||
_buildRationaleBlock('Technische Analyse', techRationale, Colors.white70),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
if (fundRationale.isNotEmpty) ...[
|
||||
_buildRationaleBlock('Fundamentale Analyse', fundRationale, Colors.white70),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
if (riskWarning.isNotEmpty)
|
||||
_buildRationaleBlock('Risikowarnung', riskWarning, AppTheme.accentRed),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Ihre Ausführungsdaten für das Depot:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: actualEntryController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Tatsächlicher Einstiegskurs (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: positionSizeController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Investitionsvolumen (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: leverageController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Genutzter Hebel (x)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: quantityController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Stückzahl (Autom. berechnet)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: entryFeeController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Einstiegsgebühr (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: exitFeeController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Ausstiegsgebühr (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: slController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Stop-Loss (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: tpController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Take-Profit (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
|
||||
),
|
||||
if (isActive)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final dto = TradeAcceptanceDto(
|
||||
userId: trade.userId,
|
||||
tradeId: trade.id,
|
||||
analysisId: trade.analysisId,
|
||||
isin: trade.isin,
|
||||
symbol: trade.symbol,
|
||||
actualEntryPrice: double.tryParse(actualEntryController.text) ?? trade.entryPrice,
|
||||
positionSize: double.tryParse(positionSizeController.text) ?? 1000.0,
|
||||
leverageUsed: double.tryParse(leverageController.text) ?? 1.0,
|
||||
entryFee: double.tryParse(entryFeeController.text) ?? 0.0,
|
||||
exitFee: double.tryParse(exitFeeController.text) ?? 0.0,
|
||||
quantity: double.tryParse(quantityController.text) ?? 0.0,
|
||||
executionTimestamp: DateTime.now().toUtc(),
|
||||
signalType: trade.signalType,
|
||||
entryPrice: trade.entryPrice,
|
||||
stopLoss: double.tryParse(slController.text) ?? trade.stopLoss,
|
||||
takeProfit: double.tryParse(tpController.text) ?? trade.takeProfit,
|
||||
instrumentType: trade.instrumentType,
|
||||
timeframe: trade.timeframe,
|
||||
reasoning: trade.reasoning,
|
||||
);
|
||||
onAccept(dto);
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
icon: const Icon(Icons.save, size: 16),
|
||||
label: const Text('Speichern'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
if (onReject != null)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
onReject(trade.id);
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
icon: Icon(Icons.cancel, color: AppTheme.accentRed, size: 16),
|
||||
label: Text('Trade Ablehnen', style: TextStyle(color: AppTheme.accentRed)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppTheme.accentRed),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final dto = TradeAcceptanceDto(
|
||||
userId: trade.userId,
|
||||
tradeId: trade.id,
|
||||
analysisId: trade.analysisId,
|
||||
isin: trade.isin,
|
||||
symbol: trade.symbol,
|
||||
actualEntryPrice: double.tryParse(actualEntryController.text) ?? trade.entryPrice,
|
||||
positionSize: double.tryParse(positionSizeController.text) ?? 1000.0,
|
||||
leverageUsed: double.tryParse(leverageController.text) ?? 1.0,
|
||||
entryFee: double.tryParse(entryFeeController.text) ?? 0.0,
|
||||
exitFee: double.tryParse(exitFeeController.text) ?? 0.0,
|
||||
quantity: double.tryParse(quantityController.text) ?? 0.0,
|
||||
executionTimestamp: DateTime.now().toUtc(),
|
||||
signalType: trade.signalType,
|
||||
entryPrice: trade.entryPrice,
|
||||
stopLoss: double.tryParse(slController.text) ?? trade.stopLoss,
|
||||
takeProfit: double.tryParse(tpController.text) ?? trade.takeProfit,
|
||||
instrumentType: trade.instrumentType,
|
||||
timeframe: trade.timeframe,
|
||||
reasoning: trade.reasoning,
|
||||
);
|
||||
onAccept(dto);
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
icon: const Icon(Icons.check_circle, size: 16),
|
||||
label: const Text('Trade Annehmen & Ausführen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static String _fmt(dynamic val) {
|
||||
if (val == null) return '0.00';
|
||||
if (val is double) {
|
||||
if (val > 100) return val.toStringAsFixed(1);
|
||||
return val.toStringAsFixed(2);
|
||||
}
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
static Widget _buildTradeStat(String label, String value, Color color) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: Colors.white54, fontSize: 11)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildRationaleBlock(String title, String content, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(content, style: const TextStyle(color: Colors.white70, fontSize: 12, height: 1.4)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
|
||||
/// Trade Performance Metrics header evaluating win rates, CRV, and potential.
|
||||
class TradePerformanceHeader extends StatelessWidget {
|
||||
const TradePerformanceHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GlassContainer(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_metricColumn('Trefferquote (Win Rate)', '78.4%', AppTheme.primaryEmerald),
|
||||
_metricColumn('Risiko-Ertrag (CRV)', '1 : 2.85', AppTheme.accentCyan),
|
||||
_metricColumn('Ø Potenzial', '+14.2%', AppTheme.primaryEmerald),
|
||||
_metricColumn('Signale 30T', '42 Active', AppTheme.textPrimary),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _metricColumn(String label, String value, Color valueColor) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: TextStyle(color: valueColor, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user