diff --git a/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs b/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs
index 90ffc5f..68062f6 100644
--- a/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs
+++ b/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs
@@ -115,12 +115,19 @@ public class ManualAnalysisController : ControllerBase
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
bool shouldProceed = n8nResponse != null && string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase);
+ double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
+ request.Sector,
+ request.Symbol,
+ regime,
+ n8nEvalScore: n8nResponse?.EvalScore,
+ signalType: n8nResponse?.SuggestedDirection ?? "BUY");
+
TradeProposalDto? proposal = null;
if (shouldProceed && n8nResponse != null)
{
proposal = new TradeProposalDto
{
- TradeId = "PROP-" + Guid.NewGuid().ToString("N"),
+ TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
AnalysisId = analysisId,
EventId = analysisId,
Sector = request.Sector,
@@ -132,11 +139,21 @@ public class ManualAnalysisController : ControllerBase
RiskTolerance = n8nResponse.SuggestedRisk,
Timeframe = timeframeFormatted,
InstrumentType = request.InstrumentType,
- WinRate = winRate,
+ WinRate = dynamicWinRate,
VixRegime = regime,
VixValue = currentVix,
TtlMinutes = 60,
Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}",
+ StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0,
+ TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0,
+ EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min,
+ EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max,
+ TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets,
+ RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio,
+ MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage,
+ TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty,
+ FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty,
+ RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty,
CreatedAt = DateTime.UtcNow
};
}
@@ -151,7 +168,7 @@ public class ManualAnalysisController : ControllerBase
VixRegime = regime,
VixValue = currentVix,
ImpactScore = 1.0,
- WinRate = winRate,
+ WinRate = dynamicWinRate,
RawDataJson = JsonSerializer.Serialize(request),
AiOutputJson = proposal != null ? JsonSerializer.Serialize(proposal) : "{}",
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
diff --git a/FinlyticAnalyzer/Services/IWinRateCalculator.cs b/FinlyticAnalyzer/Services/IWinRateCalculator.cs
index 3499f70..ea83ccb 100644
--- a/FinlyticAnalyzer/Services/IWinRateCalculator.cs
+++ b/FinlyticAnalyzer/Services/IWinRateCalculator.cs
@@ -8,4 +8,18 @@ public interface IWinRateCalculator
/// Calculates the win rate for a given sector and symbol under the specified market regime.
///
double CalculateWinRate(string sector, string symbol, VixMarketRegime regime);
+
+ ///
+ /// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime.
+ ///
+ double CalculateDynamicWinRate(
+ string sector,
+ string symbol,
+ VixMarketRegime regime,
+ double? n8nEvalScore = null,
+ double? technicalScore = null,
+ double? sentimentScore = null,
+ double? fundamentalScore = null,
+ string signalType = "BUY");
}
+
diff --git a/FinlyticAnalyzer/Services/WinRateCalculator.cs b/FinlyticAnalyzer/Services/WinRateCalculator.cs
index 173002a..9dc5dcf 100644
--- a/FinlyticAnalyzer/Services/WinRateCalculator.cs
+++ b/FinlyticAnalyzer/Services/WinRateCalculator.cs
@@ -34,31 +34,105 @@ public class WinRateCalculator : IWinRateCalculator
/// Uses cached feedback records (3-minute TTL) to prevent disk I/O bottlenecks.
///
public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime)
+ {
+ return CalculateDynamicWinRate(sector, symbol, regime);
+ }
+
+ ///
+ /// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime.
+ ///
+ public double CalculateDynamicWinRate(
+ string sector,
+ string symbol,
+ VixMarketRegime regime,
+ double? n8nEvalScore = null,
+ double? technicalScore = null,
+ double? sentimentScore = null,
+ double? fundamentalScore = null,
+ string signalType = "BUY")
{
try
{
- var records = GetCachedOrLoadRecords();
- if (records.Count == 0) return 65.0;
-
- var matching = records.Where(r =>
- string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
- r.VixRegime == regime).ToList();
-
- if (matching.Count > 0)
+ // 1. N8n AI Confidence Score (Weight: 40%)
+ double n8nComponent = 62.0;
+ if (n8nEvalScore.HasValue && n8nEvalScore.Value > 0)
{
- int winningTrades = matching.Count(r => r.IsWin);
- double calculatedWinRate = (double)winningTrades / matching.Count * 100.0;
- _logger.LogInformation("[{Channel}] Calculated win-rate for Sector '{Sector}' in Regime '{Regime}': {WinRate:F1}% ({Wins}/{Total})",
- "AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, matching.Count);
- return Math.Round(calculatedWinRate, 1);
+ n8nComponent = n8nEvalScore.Value <= 1.0 ? n8nEvalScore.Value * 100.0 : n8nEvalScore.Value;
}
+
+ // 2. Technical Score (Weight: 30%)
+ double taComponent = 60.0;
+ if (technicalScore.HasValue && technicalScore.Value > 0)
+ {
+ taComponent = technicalScore.Value <= 1.0 ? technicalScore.Value * 100.0 : technicalScore.Value;
+ }
+
+ // 3. Sentiment Score (Weight: 15%)
+ double sentComponent = 58.0;
+ if (sentimentScore.HasValue)
+ {
+ if (sentimentScore.Value >= -1.0 && sentimentScore.Value <= 1.0)
+ {
+ // Map sentiment from -1.0..+1.0 into 35.0..85.0
+ sentComponent = 50.0 + (sentimentScore.Value * 25.0);
+ }
+ else
+ {
+ sentComponent = sentimentScore.Value;
+ }
+ }
+
+ // 4. Fundamental Score (Weight: 15%)
+ double fundComponent = 60.0;
+ if (fundamentalScore.HasValue && fundamentalScore.Value > 0)
+ {
+ fundComponent = fundamentalScore.Value <= 1.0 ? fundamentalScore.Value * 100.0 : fundamentalScore.Value;
+ }
+
+ // Multi-factor weighted composite
+ double composite = (n8nComponent * 0.40) + (taComponent * 0.30) + (sentComponent * 0.15) + (fundComponent * 0.15);
+
+ // 5. Market Regime & Volatility Adjustment
+ double vixAdjustment = regime switch
+ {
+ VixMarketRegime.LowVol => +4.0, // Calm trending market
+ VixMarketRegime.Normal => +1.5, // Normal conditions
+ VixMarketRegime.HighVol => -3.5, // Increased whipsaws
+ VixMarketRegime.Panic => -8.0, // High panic / uncertainty
+ _ => 0.0
+ };
+
+ composite += vixAdjustment;
+
+ // 6. Historical track record calibration (if available in feedback records)
+ var records = GetCachedOrLoadRecords();
+ if (records.Count > 0)
+ {
+ var matching = records.Where(r =>
+ string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
+ r.VixRegime == regime).ToList();
+
+ if (matching.Count >= 5)
+ {
+ int winningTrades = matching.Count(r => r.IsWin);
+ double historicalWinRate = (double)winningTrades / matching.Count * 100.0;
+ composite = (composite * 0.75) + (historicalWinRate * 0.25);
+ }
+ }
+
+ // Clamp between realistic financial statistical bounds (45.0% to 92.0%)
+ double finalWinRate = Math.Clamp(Math.Round(composite, 1), 45.0, 92.0);
+
+ _logger.LogInformation("[{Channel}] Dynamic Win-Rate for {Symbol} ({Sector}): {WinRate:F1}% [AI: {N8n:F1}%, TA: {TA:F1}%, Sent: {Sent:F1}%, Regime: {Regime}]",
+ "AnalyzerChannel", symbol, sector, finalWinRate, n8nComponent, taComponent, sentComponent, regime);
+
+ return finalWinRate;
}
catch (Exception ex)
{
- _logger.LogWarning(ex, "[{Channel}] Error reading feedback files for win-rate calculation. Falling back to default.", "AnalyzerChannel");
+ _logger.LogWarning(ex, "[{Channel}] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", "AnalyzerChannel", symbol);
+ return 65.0;
}
-
- return 65.0; // Default baseline win-rate
}
private List GetCachedOrLoadRecords()
diff --git a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs b/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs
index 5ef10d1..5a8762a 100644
--- a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs
+++ b/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs
@@ -329,11 +329,19 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
var settings = await settingsService.GetSettingsAsync();
double minSignalScore = settings.MinSignalScore;
- double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : 0.75;
+ double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
+ manualReq.Sector,
+ manualReq.Symbol,
+ regime,
+ n8nEvalScore: n8nResponse?.EvalScore,
+ sentimentScore: manualReq.SentimentData?.CurrentSummary?.CompoundScore,
+ signalType: n8nResponse?.SuggestedDirection ?? "BUY");
+
+ double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : (dynamicWinRate / 100.0);
bool shouldProceed = n8nResponse != null &&
string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) &&
(confidenceScore * 100.0) >= minSignalScore &&
- winRate >= minSignalScore;
+ dynamicWinRate >= minSignalScore;
TradeProposalDto? proposalDto = null;
if (n8nResponse != null)
@@ -353,7 +361,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
RiskTolerance = n8nResponse.SuggestedRisk,
Timeframe = timeframeFormatted,
InstrumentType = manualReq.InstrumentType,
- WinRate = winRate,
+ WinRate = dynamicWinRate,
VixRegime = regime,
VixValue = currentVix,
TtlMinutes = 60,
@@ -384,7 +392,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
VixRegime = regime,
VixValue = currentVix,
ImpactScore = 1.0,
- WinRate = winRate,
+ WinRate = dynamicWinRate,
RawDataJson = JsonSerializer.Serialize(manualReq),
AiOutputJson = proposalDto != null ? JsonSerializer.Serialize(proposalDto) : "{}",
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
@@ -527,6 +535,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto? taResp = null;
FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto? fundResp = null;
FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto? livePriceResp = null;
+ FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto? sentResp = null;
try
{
@@ -549,7 +558,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
livePriceResp = livePriceTask.Result;
taResp = taTask.Result;
fundResp = fundTask.Result;
- var sentResp = sentTask.Result;
+ sentResp = sentTask.Result;
if (taResp?.Indicators != null)
{
@@ -735,10 +744,31 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
ActionRequired = isHighConviction ? "PROMPT_USER_FOR_MANUAL_TRADE" : "NO_ACTION"
};
+ double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
+ filterResult.Sector,
+ finalSymbol,
+ regime,
+ n8nEvalScore: n8nResponse?.EvalScore,
+ sentimentScore: sentResp?.CurrentSummary?.CompoundScore,
+ signalType: n8nResponse?.SuggestedDirection ?? "BUY");
+
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService();
+ bool hasRecentProposal = await dbContext.Analyses.AnyAsync(a =>
+ a.Isin == filterResult.Isin &&
+ a.IsTradeProposed &&
+ a.CreatedAt >= DateTime.UtcNow.AddHours(-4),
+ cancellationToken);
+
+ if (hasRecentProposal && isHighConviction)
+ {
+ _logger.LogInformation("[{Channel}] [AutoScreener] Asset {Symbol} ({Isin}) already has an active trade proposal in the last 4 hours. Skipping duplicate trade proposal generation.",
+ "AnalyzerChannel", finalSymbol, filterResult.Isin);
+ isHighConviction = false;
+ }
+
var analysisEntity = new AnalysisEntity
{
AnalysisId = analysisId,
@@ -749,7 +779,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
VixRegime = regime,
VixValue = currentVix,
ImpactScore = filterResult.ImpactScore,
- WinRate = winRate,
+ WinRate = dynamicWinRate,
RawDataJson = payloadStr,
AiOutputJson = JsonSerializer.Serialize(recommendation),
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
@@ -780,7 +810,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
RiskTolerance = n8nResponse.SuggestedRisk ?? "Balanced",
Timeframe = $"{minTf}-{maxTf} Tage",
InstrumentType = "KnockOut",
- WinRate = winRate,
+ WinRate = dynamicWinRate,
VixRegime = regime,
VixValue = currentVix,
TtlMinutes = 180,
diff --git a/FinlyticApp/lib/features/asset_detail/views/asset_detail_screen.dart b/FinlyticApp/lib/features/asset_detail/views/asset_detail_screen.dart
index 98b92f7..ee8b9f6 100644
--- a/FinlyticApp/lib/features/asset_detail/views/asset_detail_screen.dart
+++ b/FinlyticApp/lib/features/asset_detail/views/asset_detail_screen.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/network/api_client.dart';
+import '../../favorites/cubit/favorites_cubit.dart';
import '../bloc/fundamentals/asset_fundamentals_bloc.dart';
import '../bloc/fundamentals/asset_fundamentals_event.dart';
import '../bloc/technical/asset_technical_bloc.dart';
@@ -8,6 +9,7 @@ import '../bloc/technical/asset_technical_event.dart';
import '../bloc/trades/asset_trades_bloc.dart';
import '../bloc/trades/asset_trades_event.dart';
import '../repositories/asset_repository.dart';
+import '../utils/ticker_resolver.dart';
import 'layouts/asset_page_desktop_layout.dart';
import 'layouts/asset_page_mobile_layout.dart';
@@ -29,39 +31,49 @@ class AssetDetailScreen extends StatelessWidget {
Widget build(BuildContext context) {
final repository = AssetRepository(apiClient: apiClient);
- return MultiBlocProvider(
- providers: [
- BlocProvider(
- create: (context) => AssetFundamentalsBloc(repository: repository)
- ..add(LoadAssetFundamentals(isin, ticker: symbol)),
- ),
- BlocProvider(
- create: (context) => AssetTechnicalBloc(repository: repository)
- ..add(LoadAssetTechnical(isin, ticker: symbol)),
- ),
- BlocProvider(
- create: (context) => AssetTradesBloc(repository: repository)
- ..add(LoadAssetTrades(isin)),
- ),
- ],
- child: Scaffold(
- body: LayoutBuilder(
- builder: (context, constraints) {
- if (constraints.maxWidth >= 900) {
- return AssetPageDesktopLayout(
- isin: isin,
- name: name,
- selectedTicker: symbol,
- );
- }
- return AssetPageMobileLayout(
- isin: isin,
- name: name,
- selectedTicker: symbol,
- );
- },
- ),
- ),
+ return BlocBuilder(
+ builder: (context, favState) {
+ final initialTicker = TickerResolver.resolve(
+ isin: isin,
+ candidateSymbol: symbol,
+ favoriteDetails: favState.favoriteDetails,
+ );
+
+ return MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (context) => AssetFundamentalsBloc(repository: repository)
+ ..add(LoadAssetFundamentals(isin, ticker: initialTicker)),
+ ),
+ BlocProvider(
+ create: (context) => AssetTechnicalBloc(repository: repository)
+ ..add(LoadAssetTechnical(isin, ticker: initialTicker)),
+ ),
+ BlocProvider(
+ create: (context) => AssetTradesBloc(repository: repository)
+ ..add(LoadAssetTrades(isin)),
+ ),
+ ],
+ child: Scaffold(
+ body: LayoutBuilder(
+ builder: (context, constraints) {
+ if (constraints.maxWidth >= 900) {
+ return AssetPageDesktopLayout(
+ isin: isin,
+ name: name,
+ selectedTicker: initialTicker ?? symbol,
+ );
+ }
+ return AssetPageMobileLayout(
+ isin: isin,
+ name: name,
+ selectedTicker: initialTicker ?? symbol,
+ );
+ },
+ ),
+ ),
+ );
+ },
);
}
}
diff --git a/FinlyticApp/lib/features/asset_detail/views/tabs/trades_tab.dart b/FinlyticApp/lib/features/asset_detail/views/tabs/trades_tab.dart
index 2318111..74b71a5 100644
--- a/FinlyticApp/lib/features/asset_detail/views/tabs/trades_tab.dart
+++ b/FinlyticApp/lib/features/asset_detail/views/tabs/trades_tab.dart
@@ -5,13 +5,12 @@ import '../../../../core/widgets/glass_container.dart';
import '../../../../core/widgets/shimmer_loading.dart';
import '../../../../core/widgets/status_badge.dart';
import '../../../trades/models/trade_model.dart';
-import '../../../trades/widgets/trade_execution_dialog.dart';
+import '../../../trades/widgets/trade_execution_cockpit.dart';
+import '../../../trades/widgets/trade_closing_cockpit.dart';
import '../../bloc/trades/asset_trades_bloc.dart';
import '../../bloc/trades/asset_trades_event.dart';
import '../../bloc/trades/asset_trades_state.dart';
-import '../../widgets/trades/live_trade_settings_dialog.dart';
import '../../widgets/trades/manual_analysis_dialog.dart';
-import '../../widgets/trades/close_trade_dialog.dart';
import '../../widgets/trades/asset_trade_item_card.dart';
class TradesTab extends StatefulWidget {
@@ -24,13 +23,6 @@ class TradesTab extends StatefulWidget {
class _TradesTabState extends State {
bool _justTriggeredAnalysis = false;
- LiveTradeSettings _settings = const LiveTradeSettings(
- defaultPositionSize: 2500.0,
- defaultLeverage: 5.0,
- defaultRiskScore: 50.0,
- defaultOrderFee: 1.0,
- autoAcceptSignals: false,
- );
@override
void initState() {
@@ -41,7 +33,7 @@ class _TradesTabState extends State {
void _showEditTradeExecutionDialog(BuildContext context, TradeModel trade, {bool isActive = false}) {
final tradesBloc = context.read();
- TradeExecutionDialog.show(
+ TradeExecutionCockpit.show(
context,
trade: trade,
defaultSymbol: widget.symbol,
@@ -116,55 +108,36 @@ class _TradesTabState extends State {
],
),
const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: ElevatedButton.icon(
- onPressed: () {
- ManualAnalysisDialog.show(
- context,
- symbol: widget.symbol,
- initialRiskScore: _settings.defaultRiskScore,
- onTrigger: (payload) {
- setState(() => _justTriggeredAnalysis = true);
- context.read().add(TriggerManualAnalysis(widget.symbol, payload: payload));
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('KI-Analyse für ${widget.symbol} gestartet. Trade-Ausführungsdialog öffnet sich in Kürze...'),
- backgroundColor: AppTheme.accentCyan,
- behavior: SnackBarBehavior.floating,
- ),
- );
- },
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: () {
+ ManualAnalysisDialog.show(
+ context,
+ symbol: widget.symbol,
+ initialRiskScore: 50.0,
+ onTrigger: (payload) {
+ setState(() => _justTriggeredAnalysis = true);
+ context.read().add(TriggerManualAnalysis(widget.symbol, payload: payload));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('KI-Analyse für ${widget.symbol} abgeschlossen. Trade-Cockpit öffnet sich...'),
+ backgroundColor: AppTheme.accentCyan,
+ behavior: SnackBarBehavior.floating,
+ ),
);
},
- icon: const Icon(Icons.auto_awesome, size: 18),
- label: const Text('Analyse starten', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
- style: ElevatedButton.styleFrom(
- backgroundColor: AppTheme.accentCyan,
- foregroundColor: Colors.black,
- padding: const EdgeInsets.symmetric(vertical: 14),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
- ),
- ),
+ );
+ },
+ icon: const Icon(Icons.auto_awesome, size: 18),
+ label: const Text('KI-Analyse Starten', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppTheme.accentCyan,
+ foregroundColor: Colors.black,
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
- const SizedBox(width: 10),
- IconButton.filledTonal(
- onPressed: () {
- LiveTradeSettingsDialog.show(
- context,
- currentSettings: _settings,
- onSave: (newSettings) => setState(() => _settings = newSettings),
- );
- },
- icon: const Icon(Icons.settings, color: Colors.white),
- tooltip: 'Live Trade Einstellungen',
- style: IconButton.styleFrom(
- backgroundColor: AppTheme.glassSurface,
- padding: const EdgeInsets.all(14),
- ),
- ),
- ],
+ ),
),
],
),
@@ -245,7 +218,7 @@ class _TradesTabState extends State {
onSettings: () => _showEditTradeExecutionDialog(context, trade, isActive: true),
onClose: isActive
? () {
- CloseTradeDialog.show(
+ TradeClosingCockpit.show(
context,
trade: trade,
defaultSymbol: widget.symbol,
@@ -254,7 +227,7 @@ class _TradesTabState extends State {
context.read().add(CloseTradeEvent(trade.id, isinVal, dto.userExitPrice));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text('Trade ${trade.id} geschlossen! Ausstiegskurs: €${dto.userExitPrice.toStringAsFixed(2)}'),
+ content: Text('Trade ${trade.id} geschlossen! Realisierter Ausstiegskurs: €${dto.userExitPrice.toStringAsFixed(2)}'),
backgroundColor: AppTheme.primaryEmerald,
behavior: SnackBarBehavior.floating,
),
@@ -270,3 +243,4 @@ class _TradesTabState extends State {
);
}
}
+
diff --git a/FinlyticApp/lib/features/asset_detail/widgets/trades/asset_trade_item_card.dart b/FinlyticApp/lib/features/asset_detail/widgets/trades/asset_trade_item_card.dart
index a887724..90062bd 100644
--- a/FinlyticApp/lib/features/asset_detail/widgets/trades/asset_trade_item_card.dart
+++ b/FinlyticApp/lib/features/asset_detail/widgets/trades/asset_trade_item_card.dart
@@ -65,6 +65,7 @@ class AssetTradeItemCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
+ // Header Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -84,7 +85,10 @@ class AssetTradeItemCard extends StatelessWidget {
color: AppTheme.glassSurface,
borderRadius: BorderRadius.circular(6),
),
- child: Text(trade.instrumentType, style: TextStyle(color: AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.bold)),
+ child: Text(
+ trade.derivativeIsin.isNotEmpty ? '${trade.instrumentType} (${trade.derivativeIsin})' : trade.instrumentType,
+ style: TextStyle(color: AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.bold),
+ ),
),
],
),
@@ -99,7 +103,7 @@ class AssetTradeItemCard extends StatelessWidget {
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentRed,
foregroundColor: Colors.white,
- padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
@@ -125,7 +129,7 @@ class AssetTradeItemCard extends StatelessWidget {
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
foregroundColor: Colors.black,
- padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
@@ -135,12 +139,62 @@ class AssetTradeItemCard extends StatelessWidget {
),
],
),
+
+ // Active Drift Radar Bar
+ if (isActive) ...[
+ const SizedBox(height: 10),
+ _buildDriftRadarBar(trade),
+ ],
+
+ // Pending Exit Alert Banner
+ if (isActive && trade.hasPendingExitAlert) ...[
+ const SizedBox(height: 10),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: AppTheme.accentRed.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.5)),
+ ),
+ child: Row(
+ children: [
+ Icon(Icons.warning_amber_rounded, color: AppTheme.accentRed, size: 20),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('KI-Guardian Ratschlag: Position schließen!', style: TextStyle(color: AppTheme.accentRed, fontWeight: FontWeight.bold, fontSize: 12)),
+ if (trade.pendingExitReason.isNotEmpty)
+ Text(trade.pendingExitReason, style: const TextStyle(color: Colors.white70, fontSize: 11), maxLines: 2, overflow: TextOverflow.ellipsis),
+ ],
+ ),
+ ),
+ if (onClose != null)
+ ElevatedButton(
+ onPressed: onClose,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppTheme.accentRed,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ minimumSize: Size.zero,
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ ),
+ child: const Text('Schließen', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)),
+ ),
+ ],
+ ),
+ ),
+ ],
+
const SizedBox(height: 12),
Text(
'${trade.companyName.isNotEmpty ? trade.companyName : defaultSymbol} ($isin) | Timeframe: ${trade.timeframe} | Risk: ${trade.riskTolerance}',
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
),
const SizedBox(height: 12),
+
+ // Target Price Metrics Grid
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@@ -154,7 +208,11 @@ class AssetTradeItemCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildTradeStat('Einstiegszone', entryZoneMin > 0 && entryZoneMax > 0 ? '€${_fmt(entryZoneMin)} - €${_fmt(entryZoneMax)}' : '€${_fmt(entryPrice)}', Colors.white),
- _buildTradeStat('Stop-Loss', '€${_fmt(stopLoss)}', AppTheme.accentRed),
+ _buildTradeStat(
+ trade.driftStatus == DriftStatus.trailingActive ? 'Stop (Trailing)' : 'Stop-Loss',
+ '€${_fmt(stopLoss)}',
+ AppTheme.accentRed,
+ ),
_buildTradeStat('Take-Profit', takeProfitTargets.isNotEmpty ? takeProfitTargets.map((t) => '€${_fmt(t)}').join(' / ') : '€${_fmt(takeProfit)}', AppTheme.primaryEmerald),
],
),
@@ -171,6 +229,8 @@ class AssetTradeItemCard extends StatelessWidget {
],
),
),
+
+ // Execution Details if active
if (actualEntry > 0 || posSize > 0 || levUsed > 0 || qty > 0) ...[
const SizedBox(height: 12),
Container(
@@ -208,6 +268,8 @@ class AssetTradeItemCard extends StatelessWidget {
),
),
],
+
+ // Realized PnL if closed
if (status == 'CLOSED' || trade.pnlAbsolute != 0) ...[
const SizedBox(height: 12),
Builder(
@@ -243,12 +305,72 @@ class AssetTradeItemCard extends StatelessWidget {
_buildTradeStat('Rendite (%)', '${(pnlPctVal >= 0 ? "+" : "")}${_fmt(pnlPctVal)}%', isWin ? AppTheme.primaryEmerald : AppTheme.accentRed),
],
),
+ if (trade.closeReason.isNotEmpty) ...[
+ const SizedBox(height: 6),
+ Text('Grund: ${trade.closeReason}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
+ ],
],
),
);
},
),
],
+
+ // KI Timeline Expansion
+ if (trade.hourlyUpdates.isNotEmpty) ...[
+ const SizedBox(height: 8),
+ ExpansionTile(
+ tilePadding: EdgeInsets.zero,
+ childrenPadding: const EdgeInsets.only(bottom: 6),
+ dense: true,
+ leading: Icon(Icons.history_toggle_off, color: AppTheme.accentCyan, size: 18),
+ title: Text(
+ 'KI-Guardian Verlauf (${trade.hourlyUpdates.length} Prüfungen)',
+ style: TextStyle(color: AppTheme.accentCyan, fontSize: 12, fontWeight: FontWeight.bold),
+ ),
+ children: trade.hourlyUpdates.reversed.take(4).map((u) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 6),
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.03),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Row(
+ children: [
+ Text(
+ '${u.timestamp.hour.toString().padLeft(2, '0')}:${u.timestamp.minute.toString().padLeft(2, '0')}',
+ style: TextStyle(color: AppTheme.textMuted, fontSize: 11, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: (u.recommendation.toLowerCase().contains('close')
+ ? AppTheme.accentRed
+ : (u.recommendation.toLowerCase().contains('adjust') ? Colors.blue : AppTheme.primaryEmerald))
+ .withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(u.recommendation, style: const TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.bold)),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ u.reasoning.isNotEmpty ? u.reasoning : 'Kurs: €${u.currentPrice.toStringAsFixed(2)} | VIX: ${u.vixValue.toStringAsFixed(1)}',
+ style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ );
+ }).toList(),
+ ),
+ ],
+
+ // AI Analysis Expansion
if (reasoning.isNotEmpty || techRationale.isNotEmpty || fundRationale.isNotEmpty || riskWarning.isNotEmpty) ...[
const SizedBox(height: 12),
ExpansionTile(
@@ -279,6 +401,53 @@ class AssetTradeItemCard extends StatelessWidget {
);
}
+ Widget _buildDriftRadarBar(TradeModel t) {
+ Color col;
+ String label;
+ IconData icon;
+
+ switch (t.driftStatus) {
+ case DriftStatus.exitAlert:
+ col = AppTheme.accentRed;
+ label = 'Drift-Radar: Ausstieg empfohlen';
+ icon = Icons.warning_rounded;
+ break;
+ case DriftStatus.trailingActive:
+ col = AppTheme.accentCyan;
+ label = 'Drift-Radar: Trailing-Stop aktiv nachgezogen';
+ icon = Icons.security;
+ break;
+ case DriftStatus.driftWarning:
+ col = Colors.orangeAccent;
+ label = 'Drift-Radar: Leichte Abweichung von Prognose';
+ icon = Icons.tune;
+ break;
+ case DriftStatus.onTrack:
+ col = AppTheme.primaryEmerald;
+ label = 'Drift-Radar: Prognose intakt • KI überwacht stündlich';
+ icon = Icons.radar;
+ break;
+ }
+
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ decoration: BoxDecoration(
+ color: col.withValues(alpha: 0.08),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: col.withValues(alpha: 0.25)),
+ ),
+ child: Row(
+ children: [
+ Icon(icon, color: col, size: 14),
+ const SizedBox(width: 6),
+ Expanded(
+ child: Text(label, style: TextStyle(color: col, fontSize: 11, fontWeight: FontWeight.bold)),
+ ),
+ ],
+ ),
+ );
+ }
+
Widget _buildTradeStat(String title, String val, Color col) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -301,3 +470,4 @@ class AssetTradeItemCard extends StatelessWidget {
);
}
}
+
diff --git a/FinlyticApp/lib/features/asset_detail/widgets/trades/live_trade_settings_dialog.dart b/FinlyticApp/lib/features/asset_detail/widgets/trades/live_trade_settings_dialog.dart
index 46e4bb6..d08f1d4 100644
--- a/FinlyticApp/lib/features/asset_detail/widgets/trades/live_trade_settings_dialog.dart
+++ b/FinlyticApp/lib/features/asset_detail/widgets/trades/live_trade_settings_dialog.dart
@@ -29,8 +29,8 @@ class LiveTradeSettingsDialog {
double tempFee = currentSettings.defaultOrderFee;
bool tempAuto = currentSettings.autoAcceptSignals;
- final posController = TextEditingController(text: tempPos.toStringAsFixed(0));
- final levController = TextEditingController(text: tempLev.toStringAsFixed(1));
+ final posController = TextEditingController(text: tempPos == tempPos.roundToDouble() ? tempPos.toInt().toString() : tempPos.toStringAsFixed(2));
+ final levController = TextEditingController(text: tempLev == tempLev.roundToDouble() ? tempLev.toInt().toString() : tempLev.toStringAsFixed(2));
final feeController = TextEditingController(text: tempFee.toStringAsFixed(2));
showDialog(
diff --git a/FinlyticApp/lib/features/asset_detail/widgets/trades/manual_analysis_dialog.dart b/FinlyticApp/lib/features/asset_detail/widgets/trades/manual_analysis_dialog.dart
index b2e1fe1..d13f298 100644
--- a/FinlyticApp/lib/features/asset_detail/widgets/trades/manual_analysis_dialog.dart
+++ b/FinlyticApp/lib/features/asset_detail/widgets/trades/manual_analysis_dialog.dart
@@ -1,174 +1,469 @@
+import 'dart:async';
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../../models/manual_analysis_request_dto.dart';
-class ManualAnalysisDialog {
+class ManualAnalysisDialog extends StatefulWidget {
+ final String symbol;
+ final double initialRiskScore;
+ final void Function(ManualAnalysisRequestDto) onTrigger;
+
+ const ManualAnalysisDialog({
+ super.key,
+ required this.symbol,
+ required this.initialRiskScore,
+ required this.onTrigger,
+ });
+
static void show(
BuildContext context, {
required String symbol,
required double initialRiskScore,
required void Function(ManualAnalysisRequestDto) onTrigger,
}) {
- double riskScore = initialRiskScore;
- final minTimeframeController = TextEditingController(text: '1');
- final maxTimeframeController = TextEditingController(text: '14');
- String timeframeUnit = 'Tage';
- String instrumentType = 'Knock-Out Zertifikat (Turbo)';
- final notesController = TextEditingController();
-
showDialog(
context: context,
- builder: (dialogContext) {
- return StatefulBuilder(
- builder: (builderContext, setModalState) {
- return AlertDialog(
- backgroundColor: AppTheme.cardSurface,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
- side: BorderSide(color: AppTheme.glassBorder),
- ),
- title: Row(
+ barrierDismissible: false,
+ builder: (dialogContext) => ManualAnalysisDialog(
+ symbol: symbol,
+ initialRiskScore: initialRiskScore,
+ onTrigger: onTrigger,
+ ),
+ );
+ }
+
+ @override
+ State createState() => _ManualAnalysisDialogState();
+}
+
+class _ManualAnalysisDialogState extends State with SingleTickerProviderStateMixin {
+ late double _riskScore;
+ String _selectedTimeframePreset = 'swing'; // intraday, swing, position, custom
+ final _minTimeframeCtrl = TextEditingController(text: '1');
+ final _maxTimeframeCtrl = TextEditingController(text: '14');
+ String _timeframeUnit = 'Tage';
+ String _instrumentType = 'Knock-Out Zertifikat (Turbo)';
+ final _notesCtrl = TextEditingController();
+
+ bool _isAnalyzing = false;
+ int _analysisStage = 0; // 0: Idle, 1: Marktdaten, 2: TA/FA Indikatoren, 3: KI Setup
+ Timer? _stageTimer;
+
+ @override
+ void initState() {
+ super.initState();
+ _riskScore = widget.initialRiskScore;
+ }
+
+ @override
+ void dispose() {
+ _stageTimer?.cancel();
+ _minTimeframeCtrl.dispose();
+ _maxTimeframeCtrl.dispose();
+ _notesCtrl.dispose();
+ super.dispose();
+ }
+
+ void _selectTimeframePreset(String key, int min, int max, String unit) {
+ setState(() {
+ _selectedTimeframePreset = key;
+ _minTimeframeCtrl.text = min.toString();
+ _maxTimeframeCtrl.text = max.toString();
+ _timeframeUnit = unit;
+ });
+ }
+
+ void _selectRiskPreset(double score) {
+ setState(() {
+ _riskScore = score;
+ });
+ }
+
+ void _startAnalysis() {
+ setState(() {
+ _isAnalyzing = true;
+ _analysisStage = 1;
+ });
+
+ _stageTimer = Timer.periodic(const Duration(milliseconds: 700), (timer) {
+ if (!mounted) {
+ timer.cancel();
+ return;
+ }
+ if (_analysisStage < 3) {
+ setState(() {
+ _analysisStage++;
+ });
+ } else {
+ timer.cancel();
+ final payload = ManualAnalysisRequestDto(
+ isin: widget.symbol,
+ symbol: widget.symbol,
+ riskScore: _riskScore.toInt(),
+ minTimeframeValue: int.tryParse(_minTimeframeCtrl.text) ?? 1,
+ maxTimeframeValue: int.tryParse(_maxTimeframeCtrl.text) ?? 14,
+ timeframeUnit: _timeframeUnit,
+ instrumentType: _instrumentType,
+ userNotes: _notesCtrl.text,
+ headline: 'Manuelle KI-Analyse für ${widget.symbol}',
+ );
+
+ Navigator.of(context).pop();
+ widget.onTrigger(payload);
+ }
+ });
+ }
+
+ String get _stageText {
+ switch (_analysisStage) {
+ case 1:
+ return 'Lade Live-Marktdaten & Orderbuch...';
+ case 2:
+ return 'Berechne Technische Indikatoren & Muster...';
+ case 3:
+ return 'KI generiert optimales Trade-Setup...';
+ default:
+ return 'Analyse Jetzt Ausführen';
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final riskColor = _riskScore < 35
+ ? AppTheme.primaryEmerald
+ : (_riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed);
+
+ return Dialog(
+ backgroundColor: Colors.transparent,
+ insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
+ child: Container(
+ width: 560,
+ decoration: BoxDecoration(
+ color: AppTheme.cardSurface,
+ borderRadius: BorderRadius.circular(24),
+ border: Border.all(color: AppTheme.glassBorder),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.6),
+ blurRadius: 30,
+ offset: const Offset(0, 10),
+ ),
+ ],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 18, 16, 14),
+ child: Row(
children: [
- Icon(Icons.auto_awesome, color: AppTheme.accentCyan, size: 22),
- const SizedBox(width: 8),
- Expanded(
- child: Text('KI-Analyse für $symbol', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: AppTheme.accentCyan.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Icon(Icons.auto_awesome, color: AppTheme.accentCyan, size: 22),
),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'KI-Trade Setup Generator',
+ style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ Text(
+ 'Asset: ${widget.symbol}',
+ style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
+ ),
+ ],
+ ),
+ ),
+ if (!_isAnalyzing)
+ IconButton(
+ onPressed: () => Navigator.of(context).pop(),
+ icon: const Icon(Icons.close, color: Colors.white54),
+ ),
],
),
- content: SizedBox(
- width: 440,
- child: SingleChildScrollView(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('Wählen Sie Ihre Zielparameter für die Trade-Evaluierung:', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
- const SizedBox(height: 16),
- const Text('Zeithorizont (Timeframe):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
- const SizedBox(height: 6),
- Row(
- children: [
- Expanded(
- child: TextField(
- controller: minTimeframeController,
- keyboardType: TextInputType.number,
- decoration: const InputDecoration(labelText: 'Von', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
- ),
+ ),
+ const Divider(color: Colors.white12, height: 1),
+
+ // Content
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 1. TIMEFRAME PRESETS
+ const Text('1. ZEITHORIZONT (TIMEFRAME)', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
+ const SizedBox(height: 8),
+
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: [
+ _presetChip('⚡ Intraday (1–4 Std.)', 'intraday', () => _selectTimeframePreset('intraday', 1, 4, 'Stunden')),
+ _presetChip('🌊 Swing-Trade (1–14 Tage)', 'swing', () => _selectTimeframePreset('swing', 1, 14, 'Tage')),
+ _presetChip('📈 Positions-Trade (2–8 Wo.)', 'position', () => _selectTimeframePreset('position', 2, 8, 'Wochen')),
+ _presetChip('⚙ Benutzerdefiniert', 'custom', () => setState(() => _selectedTimeframePreset = 'custom')),
+ ],
+ ),
+ ),
+
+ if (_selectedTimeframePreset == 'custom') ...[
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _minTimeframeCtrl,
+ keyboardType: TextInputType.number,
+ decoration: const InputDecoration(labelText: 'Von', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
),
- const SizedBox(width: 8),
- Expanded(
- child: TextField(
- controller: maxTimeframeController,
- keyboardType: TextInputType.number,
- decoration: const InputDecoration(labelText: 'Bis', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: DropdownButtonFormField(
- initialValue: timeframeUnit,
- dropdownColor: AppTheme.cardSurface,
- decoration: const InputDecoration(labelText: 'Einheit', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
- items: const [
- DropdownMenuItem(value: 'Stunden', child: Text('Stunden')),
- DropdownMenuItem(value: 'Tage', child: Text('Tage')),
- DropdownMenuItem(value: 'Wochen', child: Text('Wochen')),
- DropdownMenuItem(value: 'Monate', child: Text('Monate')),
- ],
- onChanged: (val) {
- if (val != null) setModalState(() => timeframeUnit = val);
- },
- ),
- ),
- ],
- ),
- const SizedBox(height: 16),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text('Risikobereitschaft:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
- Text(
- '${riskScore.toInt()}/100 (${riskScore < 30 ? "Konservativ" : (riskScore < 70 ? "Ausgewogen" : "Spekulativ")})',
- style: TextStyle(
- color: riskScore < 30 ? AppTheme.primaryEmerald : (riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed),
- fontWeight: FontWeight.bold,
- fontSize: 13,
- ),
- ),
- ],
- ),
- Slider(
- value: riskScore,
- min: 0,
- max: 100,
- divisions: 100,
- activeColor: riskScore < 30 ? AppTheme.primaryEmerald : (riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed),
- inactiveColor: AppTheme.glassSurface,
- onChanged: (val) => setModalState(() => riskScore = val),
- ),
- const SizedBox(height: 12),
- const Text('Instrumententyp (Trade Republic):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
- const SizedBox(height: 6),
- DropdownButtonFormField(
- initialValue: instrumentType,
- dropdownColor: AppTheme.cardSurface,
- decoration: const InputDecoration(contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10)),
- items: const [
- DropdownMenuItem(value: 'Aktie / ETF (Direktinvestment)', child: Text('Aktie / ETF (Direktinvestment)')),
- DropdownMenuItem(value: 'Optionsschein (Warrant)', child: Text('Optionsschein (Warrant)')),
- DropdownMenuItem(value: 'Knock-Out Zertifikat (Turbo)', child: Text('Knock-Out Zertifikat (Turbo)')),
- DropdownMenuItem(value: 'Faktor-Zertifikat', child: Text('Faktor-Zertifikat')),
- DropdownMenuItem(value: 'Krypto (Crypto)', child: Text('Krypto (Crypto)')),
- ],
- onChanged: (val) {
- if (val != null) setModalState(() => instrumentType = val);
- },
- ),
- const SizedBox(height: 16),
- const Text('Anmerkung für die KI:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
- const SizedBox(height: 6),
- TextField(
- controller: notesController,
- maxLines: 3,
- decoration: const InputDecoration(
- hintText: 'Z.B. Besonderes Augenmerk auf Hebelprodukte legen, enge Stopps berücksichtigen...',
),
+ const SizedBox(width: 8),
+ Expanded(
+ child: TextField(
+ controller: _maxTimeframeCtrl,
+ keyboardType: TextInputType.number,
+ decoration: const InputDecoration(labelText: 'Bis', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: DropdownButtonFormField(
+ initialValue: _timeframeUnit,
+ dropdownColor: AppTheme.cardSurface,
+ decoration: const InputDecoration(labelText: 'Einheit', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
+ items: const [
+ DropdownMenuItem(value: 'Stunden', child: Text('Stunden')),
+ DropdownMenuItem(value: 'Tage', child: Text('Tage')),
+ DropdownMenuItem(value: 'Wochen', child: Text('Wochen')),
+ DropdownMenuItem(value: 'Monate', child: Text('Monate')),
+ ],
+ onChanged: (val) {
+ if (val != null) setState(() => _timeframeUnit = val);
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+
+ const SizedBox(height: 18),
+
+ // 2. RISIKO-PROFIL
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('2. RISIKOBEREITSCHAFT', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
+ Text(
+ '${_riskScore.toInt()}/100 (${_riskScore < 35 ? "Konservativ" : (_riskScore < 70 ? "Ausgewogen" : "Spekulativ")})',
+ style: TextStyle(color: riskColor, fontWeight: FontWeight.bold, fontSize: 12),
),
],
),
+ const SizedBox(height: 8),
+
+ Row(
+ children: [
+ _riskProfileChip('🟢 Konservativ', 25, AppTheme.primaryEmerald),
+ _riskProfileChip('🟡 Ausgewogen', 50, Colors.orangeAccent),
+ _riskProfileChip('🔴 Spekulativ', 85, AppTheme.accentRed),
+ ],
+ ),
+ Slider(
+ value: _riskScore,
+ min: 0,
+ max: 100,
+ divisions: 100,
+ activeColor: riskColor,
+ inactiveColor: AppTheme.glassSurface,
+ onChanged: _isAnalyzing ? null : (val) => setState(() => _riskScore = val),
+ ),
+
+ const SizedBox(height: 12),
+
+ // 3. INSTRUMENT
+ const Text('3. FINANZINSTRUMENT', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
+ const SizedBox(height: 8),
+
+ DropdownButtonFormField(
+ initialValue: _instrumentType,
+ dropdownColor: AppTheme.cardSurface,
+ decoration: const InputDecoration(contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10)),
+ items: const [
+ DropdownMenuItem(value: 'Knock-Out Zertifikat (Turbo)', child: Text('Knock-Out Zertifikat (Turbo) - Hebel')),
+ DropdownMenuItem(value: 'Aktie / ETF (Direktinvestment)', child: Text('Aktie / ETF (Direktinvestment)')),
+ DropdownMenuItem(value: 'Optionsschein (Warrant)', child: Text('Optionsschein (Warrant)')),
+ DropdownMenuItem(value: 'Faktor-Zertifikat', child: Text('Faktor-Zertifikat')),
+ DropdownMenuItem(value: 'Krypto (Crypto)', child: Text('Krypto (Crypto)')),
+ ],
+ onChanged: _isAnalyzing ? null : (val) {
+ if (val != null) setState(() => _instrumentType = val);
+ },
+ ),
+
+ const SizedBox(height: 14),
+
+ // 4. NOTIZEN & QUICK TAGS
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text('4. ANWEISUNG FÜR DIE KI (OPTIONAL)', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
+ Wrap(
+ spacing: 4,
+ children: [
+ _hintTag('Enge Stops'),
+ _hintTag('Hoher Hebel'),
+ _hintTag('Earnings Play'),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ TextField(
+ controller: _notesCtrl,
+ maxLines: 2,
+ enabled: !_isAnalyzing,
+ style: const TextStyle(color: Colors.white, fontSize: 12),
+ decoration: const InputDecoration(
+ hintText: 'Z.B. Besonderes Augenmerk auf Hebelprodukte legen, enge Stopps berücksichtigen...',
+ contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Animated Footer Button
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
+ child: SizedBox(
+ width: double.infinity,
+ height: 52,
+ child: ElevatedButton(
+ onPressed: _isAnalyzing ? null : _startAnalysis,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppTheme.accentCyan,
+ foregroundColor: Colors.black,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
+ elevation: _isAnalyzing ? 0 : 4,
+ ),
+ child: _isAnalyzing
+ ? Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.black),
+ ),
+ const SizedBox(width: 12),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: Text(
+ _stageText,
+ key: ValueKey(_analysisStage),
+ style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
+ ),
+ ),
+ ],
+ )
+ : const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.flash_on, size: 20),
+ SizedBox(width: 8),
+ Text('Analyse Jetzt Ausführen', style: TextStyle(fontWeight: FontWeight.w900, fontSize: 15)),
+ ],
+ ),
),
),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(dialogContext),
- child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
- ),
- ElevatedButton.icon(
- onPressed: () {
- final payload = ManualAnalysisRequestDto(
- isin: symbol,
- symbol: symbol,
- riskScore: riskScore.toInt(),
- minTimeframeValue: int.tryParse(minTimeframeController.text) ?? 1,
- maxTimeframeValue: int.tryParse(maxTimeframeController.text) ?? 14,
- timeframeUnit: timeframeUnit,
- instrumentType: instrumentType,
- userNotes: notesController.text,
- headline: 'Manuelle KI-Analyse für $symbol',
- );
- Navigator.pop(dialogContext);
- onTrigger(payload);
- },
- icon: const Icon(Icons.flash_on),
- label: const Text('Analyse Jetzt Ausführen'),
- style: ElevatedButton.styleFrom(backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black),
- ),
- ],
- );
- },
- );
- },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _presetChip(String label, String key, VoidCallback onTap) {
+ final isSelected = _selectedTimeframePreset == key;
+ return GestureDetector(
+ onTap: _isAnalyzing ? null : onTap,
+ child: Container(
+ margin: const EdgeInsets.only(right: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
+ decoration: BoxDecoration(
+ color: isSelected ? AppTheme.accentCyan.withValues(alpha: 0.2) : AppTheme.glassSurface,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: isSelected ? AppTheme.accentCyan : AppTheme.glassBorder),
+ ),
+ child: Text(
+ label,
+ style: TextStyle(
+ color: isSelected ? AppTheme.accentCyan : Colors.white70,
+ fontSize: 11,
+ fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _riskProfileChip(String label, double score, Color color) {
+ final isSelected = (_riskScore - score).abs() < 15;
+ return Expanded(
+ child: GestureDetector(
+ onTap: _isAnalyzing ? null : () => _selectRiskPreset(score),
+ child: Container(
+ margin: const EdgeInsets.only(right: 6),
+ padding: const EdgeInsets.symmetric(vertical: 7),
+ decoration: BoxDecoration(
+ color: isSelected ? color.withValues(alpha: 0.2) : AppTheme.glassSurface,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: isSelected ? color : AppTheme.glassBorder),
+ ),
+ alignment: Alignment.center,
+ child: Text(
+ label,
+ style: TextStyle(
+ color: isSelected ? color : Colors.white70,
+ fontSize: 11,
+ fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _hintTag(String text) {
+ return GestureDetector(
+ onTap: _isAnalyzing
+ ? null
+ : () {
+ if (!_notesCtrl.text.contains(text)) {
+ _notesCtrl.text = _notesCtrl.text.isEmpty ? text : '${_notesCtrl.text}, $text';
+ }
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.white10,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ '+ $text',
+ style: TextStyle(color: AppTheme.accentCyan, fontSize: 10, fontWeight: FontWeight.bold),
+ ),
+ ),
);
}
}
+
diff --git a/FinlyticApp/lib/features/trades/bloc/trade_bloc.dart b/FinlyticApp/lib/features/trades/bloc/trade_bloc.dart
index 4d56a89..c3b184e 100644
--- a/FinlyticApp/lib/features/trades/bloc/trade_bloc.dart
+++ b/FinlyticApp/lib/features/trades/bloc/trade_bloc.dart
@@ -26,7 +26,7 @@ class TradeBloc extends Bloc {
Future _onCloseTrade(CloseTrade event, Emitter emit) async {
emit(TradeLoading());
try {
- await repository.closeTrade(event.tradeId);
+ await repository.closeTrade(event.tradeId, dto: event.dto);
final trades = await repository.fetchTrades();
emit(TradeLoaded(trades));
} catch (e) {
diff --git a/FinlyticApp/lib/features/trades/bloc/trade_event.dart b/FinlyticApp/lib/features/trades/bloc/trade_event.dart
index bfe9dc5..0790df4 100644
--- a/FinlyticApp/lib/features/trades/bloc/trade_event.dart
+++ b/FinlyticApp/lib/features/trades/bloc/trade_event.dart
@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
import '../models/trade_acceptance_dto.dart';
+import '../models/close_trade_request_dto.dart';
abstract class TradeEvent extends Equatable {
const TradeEvent();
@@ -20,11 +21,12 @@ class FetchTrades extends TradeEvent {
class CloseTrade extends TradeEvent {
final String tradeId;
+ final CloseTradeRequestDto? dto;
- const CloseTrade(this.tradeId);
+ const CloseTrade(this.tradeId, {this.dto});
@override
- List