refactor: save current workspace state including FinlyticAnalyzer fixes, FinlyticApp trade route alignment, and DTO audit documentation

This commit is contained in:
2026-08-12 18:30:42 +02:00
parent a9553e9fbf
commit 3d8af3940b
163 changed files with 3421 additions and 1751 deletions
@@ -1,40 +0,0 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:finlytic_app/features/asset_detail/repositories/asset_repository.dart';
import 'asset_detail_event.dart';
import 'asset_detail_state.dart';
class AssetDetailBloc extends Bloc<AssetDetailEvent, AssetDetailState> {
final AssetRepository repository;
AssetDetailBloc({required this.repository}) : super(AssetDetailInitial()) {
on<LoadAssetData>(_onLoadAssetData);
on<ForceRefreshAssetData>(_onForceRefreshAssetData);
}
Future<void> _onLoadAssetData(LoadAssetData event, Emitter<AssetDetailState> emit) async {
emit(AssetDetailLoading());
try {
final results = await Future.wait([
repository.getFundamentalData(event.symbol),
repository.getTechnicalAnalysis(event.symbol),
]);
emit(AssetDetailLoaded(
fundamentalData: results[0] as dynamic,
technicalAnalysis: results[1] as dynamic,
));
} catch (e) {
emit(AssetDetailError("Fehler beim Laden der Asset-Daten."));
}
}
Future<void> _onForceRefreshAssetData(ForceRefreshAssetData event, Emitter<AssetDetailState> emit) async {
try {
await repository.forceRefreshFundamentalData(event.symbol);
// Optional: re-load after a delay, or rely on MQTT/SignalR to push the new data.
} catch (e) {
// ignore
}
}
}
@@ -1,26 +0,0 @@
import 'package:equatable/equatable.dart';
abstract class AssetDetailEvent extends Equatable {
const AssetDetailEvent();
@override
List<Object?> get props => [];
}
class LoadAssetData extends AssetDetailEvent {
final String symbol;
const LoadAssetData(this.symbol);
@override
List<Object?> get props => [symbol];
}
class ForceRefreshAssetData extends AssetDetailEvent {
final String symbol;
const ForceRefreshAssetData(this.symbol);
@override
List<Object?> get props => [symbol];
}
@@ -1,33 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:finlytic_app/features/asset_detail/models/fundamental_data_model.dart';
import 'package:finlytic_app/features/asset_detail/models/technical_analysis_model.dart';
abstract class AssetDetailState extends Equatable {
const AssetDetailState();
@override
List<Object?> get props => [];
}
class AssetDetailInitial extends AssetDetailState {}
class AssetDetailLoading extends AssetDetailState {}
class AssetDetailLoaded extends AssetDetailState {
final FundamentalDataModel? fundamentalData;
final TechnicalAnalysisModel? technicalAnalysis;
const AssetDetailLoaded({this.fundamentalData, this.technicalAnalysis});
@override
List<Object?> get props => [fundamentalData, technicalAnalysis];
}
class AssetDetailError extends AssetDetailState {
final String message;
const AssetDetailError(this.message);
@override
List<Object?> get props => [message];
}
@@ -2,6 +2,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'asset_header_event.dart';
import 'asset_header_state.dart';
import '../../repositories/asset_repository.dart';
import '../../models/asset_model.dart';
class AssetHeaderBloc extends Bloc<AssetHeaderEvent, AssetHeaderState> {
final AssetRepository repository;
@@ -10,8 +11,28 @@ class AssetHeaderBloc extends Bloc<AssetHeaderEvent, AssetHeaderState> {
final prevData = state is AssetHeaderLoaded ? (state as AssetHeaderLoaded).data : (state is AssetHeaderLoading ? (state as AssetHeaderLoading).previousData : null);
emit(AssetHeaderLoading(previousData: prevData));
try {
final data = await repository.getAssetHeader(event.isin, exchange: event.exchange, ticker: event.ticker);
emit(AssetHeaderLoaded(data));
final fundamentals = await repository.getAssetFundamentals(event.isin, event.forceRefresh, ticker: event.ticker);
if (fundamentals != null) {
final assetModel = AssetModel(
isin: fundamentals.isin,
symbol: fundamentals.primaryTicker.isNotEmpty ? fundamentals.primaryTicker : fundamentals.isin,
name: fundamentals.companyName,
currentPrice: fundamentals.currentPrice,
currency: fundamentals.tradingCurrency ?? 'EUR',
exchange: fundamentals.exchange ?? 'XETRA',
exchanges: [], // Can be populated if needed
tickers: fundamentals.availableTickers.map((t) => AssetTickerOption(
ticker: t.ticker,
exchange: t.exchange ?? 'Unknown',
tradingCurrency: t.tradingCurrency ?? fundamentals.tradingCurrency ?? 'EUR',
currentPrice: t.currentPrice,
)).toList(),
image: '/api/v1/logo/${fundamentals.isin}',
);
emit(AssetHeaderLoaded(assetModel));
} else {
emit(AssetHeaderError('Failed to load asset header data'));
}
} catch (e) {
emit(AssetHeaderError(e.toString()));
}
@@ -58,6 +58,7 @@ class FundamentalDataModel extends Equatable {
final List<CompanyExecutiveModel> executives;
final List<FinancialStatementModel> financialStatements;
final List<ForwardEstimateModel> estimates;
final List<TickerModel> availableTickers;
const FundamentalDataModel({
required this.isin,
@@ -111,6 +112,7 @@ class FundamentalDataModel extends Equatable {
required this.executives,
required this.financialStatements,
required this.estimates,
this.availableTickers = const [],
});
factory FundamentalDataModel.fromJson(Map<String, dynamic> json) {
@@ -187,6 +189,10 @@ class FundamentalDataModel extends Equatable {
?.map((e) => ForwardEstimateModel.fromJson(e))
.toList() ??
[],
availableTickers: (json['availableTickers'] as List?)
?.map((e) => TickerModel.fromJson(e))
.toList() ??
[],
);
}
@@ -242,6 +248,7 @@ class FundamentalDataModel extends Equatable {
'executives': executives.map((e) => e.toJson()).toList(),
'financialStatements': financialStatements.map((e) => e.toJson()).toList(),
'estimates': estimates.map((e) => e.toJson()).toList(),
'availableTickers': availableTickers.map((e) => e.toJson()).toList(),
};
}
@@ -297,6 +304,7 @@ class FundamentalDataModel extends Equatable {
executives,
financialStatements,
estimates,
availableTickers,
];
}
@@ -526,3 +534,38 @@ class ForwardEstimateModel extends Equatable {
@override
List<Object?> get props => [period, expectedRevenue, expectedEps, expectedGrowthRate];
}
class TickerModel extends Equatable {
final String ticker;
final String? exchange;
final String? tradingCurrency;
final double currentPrice;
const TickerModel({
required this.ticker,
this.exchange,
this.tradingCurrency,
required this.currentPrice,
});
factory TickerModel.fromJson(Map<String, dynamic> json) {
return TickerModel(
ticker: json['ticker']?.toString() ?? '',
exchange: json['exchange']?.toString(),
tradingCurrency: json['tradingCurrency']?.toString(),
currentPrice: json['currentPrice'] != null ? double.tryParse(json['currentPrice'].toString()) ?? 0.0 : 0.0,
);
}
Map<String, dynamic> toJson() {
return {
'ticker': ticker,
'exchange': exchange,
'tradingCurrency': tradingCurrency,
'currentPrice': currentPrice,
};
}
@override
List<Object?> get props => [ticker, exchange, tradingCurrency, currentPrice];
}
@@ -118,6 +118,36 @@ class StrategySignalModel extends Equatable {
List<Object?> get props => [title, date, price, type];
}
class PatternPoint extends Equatable {
final DateTime time;
final double price;
const PatternPoint(this.time, this.price);
factory PatternPoint.fromJson(Map<String, dynamic> json) => PatternPoint(DateTime.tryParse(json['time'] ?? '') ?? DateTime.now(), (json['price'] as num).toDouble());
@override
List<Object?> get props => [time, price];
}
class ChartPatternModel extends Equatable {
final String type;
final List<PatternPoint> upperLine;
final List<PatternPoint> lowerLine;
const ChartPatternModel({required this.type, required this.upperLine, required this.lowerLine});
factory ChartPatternModel.fromJson(Map<String, dynamic> json) {
return ChartPatternModel(
type: json['type']?.toString() ?? 'Pattern',
upperLine: (json['upperLine'] as List<dynamic>? ?? []).map((e) => PatternPoint.fromJson(e)).toList(),
lowerLine: (json['lowerLine'] as List<dynamic>? ?? []).map((e) => PatternPoint.fromJson(e)).toList(),
);
}
@override
List<Object?> get props => [type, upperLine, lowerLine];
}
class TechnicalAnalysisModel extends Equatable {
final String symbol;
final String trend;
@@ -132,7 +162,7 @@ class TechnicalAnalysisModel extends Equatable {
final double? stopLossAtr;
final List<CandleModel> candles;
final List<IndicatorModel> indicators;
final List<String> patterns;
final List<ChartPatternModel> patterns;
final List<StrategySignalModel> signals;
const TechnicalAnalysisModel({
@@ -164,20 +194,33 @@ class TechnicalAnalysisModel extends Equatable {
var signalsList = rawSignals.map((s) => StrategySignalModel.fromJson(s as Map<String, dynamic>)).toList();
var rawPatterns = json['patterns'] as List<dynamic>? ?? [];
var patternsList = rawPatterns.map((p) => p.toString()).toList();
var patternsList = rawPatterns.map((p) => ChartPatternModel.fromJson(p as Map<String, dynamic>)).toList();
final lastInd = indicatorsList.isNotEmpty ? indicatorsList.last : null;
final regime = json['marketRegime'] as Map<String, dynamic>?;
String parsedTrend = lastInd?.supertrendDirection ?? 'Neutral';
if (parsedTrend.toUpperCase() == 'BUY') parsedTrend = 'Bullisch ▲';
if (parsedTrend.toUpperCase() == 'SELL') parsedTrend = 'Bearisch ▼';
String parsedSignal = 'HOLD';
if (signalsList.isNotEmpty) {
parsedSignal = signalsList.last.type.toUpperCase();
}
return TechnicalAnalysisModel(
symbol: json['symbol']?.toString() ?? json['isin']?.toString() ?? json['ticker']?.toString() ?? '',
trend: json['trend']?.toString() ?? json['Trend']?.toString() ?? 'Bullisch ▲',
rsi: json['rsi']?.toString() ?? json['Rsi']?.toString() ?? '58.7',
macd: json['macd']?.toString() ?? json['Macd']?.toString() ?? '0.45',
overallSignal: json['overallSignal']?.toString() ?? json['OverallSignal']?.toString() ?? 'HOLD',
sma50: json['sma50']?.toString() ?? json['Sma50']?.toString() ?? '49.50',
sma200: json['sma200']?.toString() ?? json['Sma200']?.toString() ?? '42.50',
vix: (json['vix'] as num?)?.toDouble() ?? 16.5,
sp500Trend: json['sp500Trend']?.toString() ?? 'Bullish',
dxy: (json['dxy'] as num?)?.toDouble() ?? 104.2,
stopLossAtr: (json['stopLossAtr'] as num?)?.toDouble(),
trend: parsedTrend,
rsi: lastInd?.rsi14?.toStringAsFixed(1) ?? 'N/A',
macd: lastInd?.macdHistogram?.toStringAsFixed(2) ?? lastInd?.macdLine?.toStringAsFixed(2) ?? 'N/A',
overallSignal: parsedSignal,
sma50: lastInd?.sma50?.toStringAsFixed(2) ?? 'N/A',
sma200: lastInd?.sma200?.toStringAsFixed(2) ?? 'N/A',
vix: (regime?['vixValue'] as num?)?.toDouble() ?? 16.5,
sp500Trend: regime?['marketTrend']?.toString() ?? 'Bullish',
dxy: (regime?['dxyValue'] as num?)?.toDouble() ?? 104.2,
stopLossAtr: lastInd?.recommendedStopLoss,
candles: candlesList,
indicators: indicatorsList,
patterns: patternsList,
@@ -1,5 +1,5 @@
import 'package:finlytic_app/core/network/api_client.dart';
import 'package:finlytic_app/features/asset_detail/models/asset_model.dart';
import 'package:finlytic_app/features/asset_detail/models/fundamental_data_model.dart';
import 'package:finlytic_app/features/asset_detail/models/technical_analysis_model.dart';
import 'package:finlytic_app/features/asset_detail/models/manual_analysis_request_dto.dart';
@@ -11,56 +11,9 @@ class AssetRepository {
AssetRepository({required this.apiClient});
Future<void> forceRefreshFundamentalData(String symbol) async {
try {
await apiClient.post('/api/v1/assets/$symbol/refresh');
} catch (e) {
print('Error forcing refresh: $e');
}
}
Future<FundamentalDataModel?> getFundamentalData(String symbol) async {
try {
final res = await apiClient.get('/api/v1/assets/fundamentals/$symbol');
if (res.statusCode == 200 && res.data != null) {
return FundamentalDataModel.fromJson(res.data);
}
} catch (e) {
print('Error fetching fundamentals for $symbol: $e');
}
return null;
}
Future<TechnicalAnalysisModel?> getTechnicalAnalysis(String symbol) async {
try {
final res = await apiClient.get('/api/v1/ta/$symbol');
if (res.statusCode == 200 && res.data != null) {
return TechnicalAnalysisModel.fromJson(res.data);
}
} catch (e) {
print('Error fetching TA for $symbol: $e');
}
return null;
}
Future<AssetModel?> getAssetHeader(String isin, {String? exchange, String? ticker}) async {
try {
String url = '/api/v1/assets/header/$isin?';
if (ticker != null && ticker.isNotEmpty) {
url += 'ticker=$ticker';
}
final res = await apiClient.get(url);
if (res.statusCode == 200 && res.data != null) {
return AssetModel.fromJson(res.data);
}
} catch (e) {
print('Error fetching asset header for $isin: $e');
}
return null;
}
Future<FundamentalDataModel?> getAssetFundamentals(String isin, bool forceRefresh, {String? ticker}) async {
try {
String url = '/api/v1/assets/fundamentals/$isin?forceRefresh=$forceRefresh';
String url = '/api/v1/assets/$isin/fundamentals?forceRefresh=$forceRefresh';
if (ticker != null && ticker.isNotEmpty) {
url += '&ticker=$ticker';
}
@@ -76,7 +29,7 @@ class AssetRepository {
Future<TechnicalAnalysisModel?> getAssetTechnical(String isin, bool forceRefresh, {String? ticker}) async {
try {
String url = '/api/v1/ta/$isin?forceRefresh=$forceRefresh';
String url = '/api/v1/assets/$isin/technicals?forceRefresh=$forceRefresh';
if (ticker != null && ticker.isNotEmpty) {
url += '&ticker=$ticker';
}
@@ -92,7 +45,7 @@ class AssetRepository {
Future<List<TradeModel>> getAssetTrades(String isin, String? status) async {
try {
String url = '/api/v1/trades?isin=$isin';
String url = '/api/v1/user/trades?isin=$isin';
if (status != null) url += '&status=$status';
final res = await apiClient.get(url);
if (res.statusCode == 200 && res.data != null) {
@@ -8,7 +8,10 @@ class PatternExplanations {
'bias': 'BULLISH',
'description': 'Ein bullisches Fortsetzungsmuster, das durch eine horizontale Widerstandslinie oben und eine steigende Unterstützungslinie unten gekennzeichnet ist.',
'significance': 'Käufer werden bei jedem Rücksetzer aggressiver (höhere Tiefs). Ein Ausbruch über die obere Widerstandslinie signalisiert eine starke Fortsetzung des Aufwärtstrends.',
'action': 'Kauf-Order / Breakout-Trade beim Ausbruch über den horizontalen Widerstand mit Stop-Loss knapp unter der steigenden Trendlinie.',
'action': 'Kauf-Order / Breakout-Trade beim Ausbruch über den horizontalen Widerstand.',
'reliability': 'Hoch',
'target': 'Höhe des Dreiecks an der Basis, addiert zum Ausbruchsniveau.',
'stop_loss': 'Knapp unter der unteren (steigenden) Trendlinie.',
},
'DESCENDING_TRIANGLE': {
'title': 'Fallendes Dreieck (Descending Triangle)',
@@ -16,13 +19,19 @@ class PatternExplanations {
'description': 'Ein bärisches Fortsetzungsmuster mit einer horizontalen Unterstützungslinie unten und fallenden Hochs oben.',
'significance': 'Verkäufer drücken den Kurs bei jeder Erholung schneller nach unten. Ein Bruch der unteren Unterstützung führt meist zu dynamischen Abverkäufen.',
'action': 'Short-Trade oder Verkauf bei Durchbruch der unteren Unterstützungslinie.',
'reliability': 'Hoch',
'target': 'Höhe des Dreiecks an der Basis, subtrahiert vom Ausbruchsniveau.',
'stop_loss': 'Knapp über der oberen (fallenden) Trendlinie.',
},
'HEAD_AND_SHOULDERS': {
'title': 'Kopf-Schulter-Formation (Head & Shoulders)',
'bias': 'BEARISH',
'description': 'Klassisches Umkehrmuster bestehend aus drei Höchstständen: der mittleren höchsten Spitze (Kopf) und zwei kleineren Höchstständen links und rechts (Schultern).',
'significance': 'Ein nachhaltiger Bruch der Nackenlinie (Neckline) markiert das Ende eines Aufwärtstrends und den Beginn einer Bärenphase.',
'action': 'Verkauf/Short-Position beim Bruch der Nackenlinie mit Kursziel entsprechend der Distanz zwischen Kopf und Nackenlinie.',
'action': 'Verkauf/Short-Position beim Bruch der Nackenlinie.',
'reliability': 'Sehr Hoch',
'target': 'Distanz zwischen Kopf und Nackenlinie, vom Ausbruchspunkt der Nackenlinie nach unten projiziert.',
'stop_loss': 'Knapp über der rechten Schulter.',
},
'INVERSE_HEAD_AND_SHOULDERS': {
'title': 'Umgekehrte Kopf-Schulter-Formation',
@@ -30,6 +39,9 @@ class PatternExplanations {
'description': 'Bullisches Bodenbildungsmuster nach einem Abwärtstrend mit drei Tiefspunkten.',
'significance': 'Signalisiert das Ende des Abwärtstrends und den Beginn eines neuen Bullenmarktes.',
'action': 'Kauf bei Ausbruch über die obere Nackenlinie.',
'reliability': 'Sehr Hoch',
'target': 'Distanz zwischen Kopf (tiefster Punkt) und Nackenlinie, vom Ausbruchspunkt nach oben projiziert.',
'stop_loss': 'Knapp unter der rechten Schulter.',
},
'BULL_FLAG': {
'title': 'Bullische Flagge (Bull Flag)',
@@ -37,6 +49,9 @@ class PatternExplanations {
'description': 'Kurze Konsolidierung gegen den übergeordneten starken Aufwärtstrend (Fahnenstange).',
'significance': 'Zeigt eine temporäre Gewinnmitnahme vor der nächsten Welle nach oben.',
'action': 'Kauf beim Ausbruch aus der oberen Begrenzung des Flaggenkanals.',
'reliability': 'Hoch',
'target': 'Länge des vorherigen Aufwärtstrends (Fahnenstange), angesetzt am Ausbruchspunkt der Flagge.',
'stop_loss': 'Unterhalb des unteren Randes der Flagge.',
},
'BEAR_FLAG': {
'title': 'Bärische Flagge (Bear Flag)',
@@ -44,6 +59,9 @@ class PatternExplanations {
'description': 'Kurze Aufwärtskonsolidierung in einem steilen Abwärtstrend.',
'significance': 'Signalisiert eine Fortsetzung des steilen Abverkaufs.',
'action': 'Short-Position bei Durchbrechen der unteren Flaggenkante.',
'reliability': 'Hoch',
'target': 'Länge des vorherigen Abwärtstrends (Fahnenstange), angesetzt am Ausbruchspunkt der Flagge.',
'stop_loss': 'Oberhalb des oberen Randes der Flagge.',
},
'DOUBLE_BOTTOM': {
'title': 'Doppelboden (W-Formation)',
@@ -51,6 +69,9 @@ class PatternExplanations {
'description': 'Zwei aufeinanderfolgende Tiefpunkte auf etwa gleichem Kursniveau.',
'significance': 'Starke Unterstützung auf dem Tiefststand wurde zweimal erfolgreich verteidigt. Ausbruch über das Zwischenhoch bestätigt W-Boden.',
'action': 'Kauf bei Überschreiten des W-Zwischenhochs.',
'reliability': 'Mittel bis Hoch',
'target': 'Distanz zwischen dem Tief und dem Zwischenhoch, auf das Zwischenhoch addiert.',
'stop_loss': 'Knapp unter den beiden Tiefpunkten.',
},
'DOUBLE_TOP': {
'title': 'Doppeltopp (M-Formation)',
@@ -58,6 +79,9 @@ class PatternExplanations {
'description': 'Zwei markante Höchststände auf ähnlicher Höhe, die nicht durchbrochen werden konnten.',
'significance': 'Widerstandszone ist zu stark für die Bullen. Bruch des Zwischen-Tiefs leitet Trendwende ein.',
'action': 'Verkauf/Short bei Bruch des Zwischentiefs.',
'reliability': 'Mittel bis Hoch',
'target': 'Distanz zwischen dem Hoch und dem Zwischentief, vom Zwischentief subtrahiert.',
'stop_loss': 'Knapp über den beiden Höchstständen.',
},
'CHANNEL': {
'title': 'Trendkanal (Trading Channel)',
@@ -65,6 +89,9 @@ class PatternExplanations {
'description': 'Parallele obere und untere Trendlinien, zwischen denen der Kurs Oszilliert.',
'significance': 'Erlaubt Swing-Trading zwischen den Kanallinien oder Breakout-Trading beim Ausbruch.',
'action': 'Kauf an der Unterkante, Verkauf an der Oberkante oder Breakout-Trading.',
'reliability': 'Mittel',
'target': 'Die gegenüberliegende Kanallinie (beim Swing-Trading) oder die Kanalbreite (beim Ausbruch).',
'stop_loss': 'Außerhalb des Kanals auf der entgegengesetzten Seite des Einstiegs.',
},
'SUPPORT_RESISTANCE': {
'title': 'Unterstützungs- & Widerstandslinien',
@@ -72,9 +99,24 @@ class PatternExplanations {
'description': 'Preisniveaus, an denen historisch gehäuft Kauf- oder Verkaufsinteresse auftrat.',
'significance': 'Wichtige Marken für Stop-Loss Platzierungen und Kursziele.',
'action': 'Trading an Key-Levels mit engem Risikomanagement.',
'reliability': 'Variabel',
'target': 'Das nächste große Unterstützungs- oder Widerstandslevel.',
'stop_loss': 'Knapp jenseits der gebrochenen Linie (im Falle eines Fehlausbruchs).',
},
};
static Color getColorForPattern(String patternType) {
const colors = [
Colors.amberAccent,
Colors.cyanAccent,
Colors.purpleAccent,
Colors.pinkAccent,
Colors.lightGreenAccent,
Colors.orangeAccent,
];
return colors[patternType.hashCode.abs() % colors.length];
}
static void showPatternDetails(BuildContext context, String rawPatternType) {
final key = dictionary.keys.firstWhere(
(k) => rawPatternType.toUpperCase().contains(k) || k.contains(rawPatternType.toUpperCase()),
@@ -87,6 +129,9 @@ class PatternExplanations {
'description': 'Ein vom FinlyticAnalyzer erkanntes technisches Chart-Muster ($rawPatternType).',
'significance': 'Trendlinien und Schlüssel-Zonen zur Bestimmung von Ein- und Ausstiegssignalen.',
'action': 'Nutzen Sie Stopp-Orders und beachten Sie den übergeordneten Markt-Trend.',
'reliability': 'Unbekannt',
'target': 'Abhängig vom spezifischen Muster und der Volatilität.',
'stop_loss': 'Immer an lokalen Unterstützungs- oder Widerstandszonen platzieren.',
};
final isBullish = info['bias'] == 'BULLISH';
@@ -144,9 +189,37 @@ class PatternExplanations {
Text(info['significance']!, style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4)),
const SizedBox(height: 14),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Zuverlässigkeit:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 13)),
const SizedBox(height: 2),
Text(info['reliability']!, style: TextStyle(color: AppTheme.textSecondary, fontSize: 13)),
],
),
),
],
),
const SizedBox(height: 14),
const Text('Kursziel (Take Profit):', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 13)),
const SizedBox(height: 4),
Text(info['target']!, style: TextStyle(color: AppTheme.primaryEmerald, fontSize: 13, height: 1.4)),
const SizedBox(height: 14),
const Text('Stop-Loss Platzierung:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 13)),
const SizedBox(height: 4),
Text(info['stop_loss']!, style: TextStyle(color: AppTheme.accentRed, fontSize: 13, height: 1.4)),
const SizedBox(height: 18),
const Text('Empfohlene Trading-Handlung:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 13)),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.glassSurface,
@@ -2,11 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/network/api_client.dart';
import '../bloc/fundamentals/asset_fundamentals_bloc.dart';
import '../bloc/fundamentals/asset_fundamentals_event.dart';
import '../bloc/header/asset_header_bloc.dart';
import '../bloc/header/asset_header_event.dart';
import '../bloc/technical/asset_technical_bloc.dart';
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';
@@ -14,13 +12,17 @@ import 'layouts/asset_page_desktop_layout.dart';
import 'layouts/asset_page_mobile_layout.dart';
class AssetDetailScreen extends StatelessWidget {
final String symbol;
final String isin;
final String? name;
final String? symbol;
final ApiClient apiClient;
const AssetDetailScreen({
super.key,
required this.symbol,
required this.isin,
this.symbol,
required this.apiClient,
required this.name,
});
@override
@@ -30,25 +32,35 @@ class AssetDetailScreen extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => AssetHeaderBloc(repository: repository)..add(LoadAssetHeader(symbol)),
create: (context) => AssetHeaderBloc(repository: repository)
..add(LoadAssetHeader(isin, ticker: symbol)),
),
BlocProvider(
create: (context) => AssetFundamentalsBloc(repository: repository)..add(LoadAssetFundamentals(symbol)),
create: (context) => AssetFundamentalsBloc(repository: repository),
),
BlocProvider(
create: (context) => AssetTechnicalBloc(repository: repository)..add(LoadAssetTechnical(symbol)),
create: (context) => AssetTechnicalBloc(repository: repository),
),
BlocProvider(
create: (context) => AssetTradesBloc(repository: repository)..add(LoadAssetTrades(symbol)),
create: (context) => AssetTradesBloc(repository: repository)
..add(LoadAssetTrades(isin)),
),
],
child: Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 900) {
return AssetPageDesktopLayout(symbol: symbol);
return AssetPageDesktopLayout(
isin: isin,
name: name,
selectedTicker: symbol,
);
}
return AssetPageMobileLayout(symbol: symbol);
return AssetPageMobileLayout(
isin: isin,
name: name,
selectedTicker: symbol,
);
},
),
),
@@ -17,15 +17,19 @@ import '../tabs/technical_tab.dart';
import '../tabs/trades_tab.dart';
class AssetPageDesktopLayout extends StatefulWidget {
final String symbol;
final String isin;
final String? name;
final String? selectedTicker;
const AssetPageDesktopLayout({super.key, required this.symbol});
const AssetPageDesktopLayout(
{super.key, required this.isin, this.selectedTicker, this.name});
@override
State<AssetPageDesktopLayout> createState() => _AssetPageDesktopLayoutState();
}
class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout> with SingleTickerProviderStateMixin {
class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout>
with SingleTickerProviderStateMixin {
late TabController _tabController;
String? _selectedExchange;
String? _selectedTicker;
@@ -47,21 +51,29 @@ class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout> with Si
_selectedExchange = newExchange;
_selectedTicker = newTicker;
});
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.symbol, exchange: newExchange, ticker: newTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, ticker: newTicker, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, ticker: newTicker, forceRefresh: false));
context.read<AssetHeaderBloc>().add(
LoadAssetHeader(widget.isin, exchange: newExchange, ticker: newTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin,
ticker: newTicker, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
ticker: newTicker, forceRefresh: false));
final favCubit = context.read<FavoritesCubit>();
if (favCubit.state.isFavorite(widget.symbol)) {
favCubit.updateFavoriteTicker(widget.symbol, newTicker);
if (favCubit.state.isFavorite(widget.isin)) {
favCubit.updateFavoriteTicker(widget.isin, newTicker);
}
}
void _handleForceRefresh() {
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.symbol, forceRefresh: true, exchange: _selectedExchange, ticker: _selectedTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTradesBloc>().add(LoadAssetTrades(widget.symbol));
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.isin,
forceRefresh: true,
exchange: _selectedExchange,
ticker: _selectedTicker));
// AssetFundamentalsBloc is omitted here because AssetHeaderBloc already triggers forceRefresh=true
// for fundamentals, and the listener below will fetch the updated data with forceRefresh=false.
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTradesBloc>().add(LoadAssetTrades(widget.isin));
}
@override
@@ -73,91 +85,108 @@ class _AssetPageDesktopLayoutState extends State<AssetPageDesktopLayout> with Si
if (state is AssetHeaderLoaded && state.data != null) {
if (_selectedTicker == null) {
setState(() {
_selectedTicker = state.data!.symbol;
_selectedExchange = state.data!.exchange;
_selectedTicker = widget.selectedTicker;
//_selectedExchange = state.data!.exchange;
});
// Re-trigger fundamentals and TA with resolved ticker
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, ticker: state.data!.symbol, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, ticker: state.data!.symbol, forceRefresh: false));
}
// Re-trigger fundamentals and TA with resolved ticker whenever header loads (e.g. after force refresh)
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
}
},
child: LayoutBuilder(
builder: (context, constraints) {
final height = constraints.maxHeight.isFinite ? constraints.maxHeight : MediaQuery.of(context).size.height;
return SizedBox(
height: height,
width: double.infinity,
child: Column(
children: [
AssetHeroHeader(
symbol: widget.symbol,
selectedExchange: _selectedExchange,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Panel (Chart Focus)
Expanded(
flex: 5,
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
final height = constraints.maxHeight.isFinite
? constraints.maxHeight
: MediaQuery.of(context).size.height;
return SizedBox(
height: height,
width: double.infinity,
child: Column(
children: [
AssetHeroHeader(
isin: widget.isin,
name: widget.name ?? widget.isin,
symbol: _selectedTicker ?? widget.selectedTicker,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Panel (Chart Focus)
Expanded(
flex: 5,
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
isDesktopLeftPanel: true),
),
child: TechnicalTab(symbol: _selectedTicker ?? widget.symbol, isDesktopLeftPanel: true),
),
),
// Right Panel (Tabs for fundamentals/trades)
Expanded(
flex: 3,
child: Container(
margin: const EdgeInsets.only(top: 16, right: 16, bottom: 16),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: Column(
children: [
TabBar(
controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
tabs: const [
Tab(text: 'OVERVIEW'),
Tab(text: 'TRADES'),
],
),
Expanded(
child: TabBarView(
// Right Panel (Tabs for fundamentals/trades)
Expanded(
flex: 3,
child: Container(
margin: const EdgeInsets.only(
top: 16, right: 16, bottom: 16),
decoration: BoxDecoration(
color: theme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.glassBorder),
),
child: Column(
children: [
TabBar(
controller: _tabController,
children: [
FundamentalsTab(symbol: _selectedTicker ?? widget.symbol),
TradesTab(symbol: widget.symbol),
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: theme.glassBorder,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 13),
tabs: const [
Tab(text: 'OVERVIEW'),
Tab(text: 'TRADES'),
],
),
),
],
Expanded(
child: TabBarView(
controller: _tabController,
children: [
FundamentalsTab(
isin: widget.isin,
symbol: _selectedTicker,
),
TradesTab(symbol: widget.isin),
],
),
),
],
),
),
),
),
],
],
),
),
),
],
),
);
},
),
],
),
);
},
),
);
}
}
@@ -17,22 +17,26 @@ import '../tabs/technical_tab.dart';
import '../tabs/trades_tab.dart';
class AssetPageMobileLayout extends StatefulWidget {
final String symbol;
final String isin;
final String? name;
final String? selectedTicker;
const AssetPageMobileLayout({super.key, required this.symbol});
const AssetPageMobileLayout(
{super.key, required this.isin, this.selectedTicker, this.name});
@override
State<AssetPageMobileLayout> createState() => _AssetPageMobileLayoutState();
}
class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout> with SingleTickerProviderStateMixin {
class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout>
with SingleTickerProviderStateMixin {
late TabController _tabController;
String? _selectedExchange;
String? _selectedTicker;
@override
void initState() {
super.initState();
//_selectedTicker = widget.selectedTicker;
_tabController = TabController(length: 3, vsync: this);
}
@@ -44,24 +48,30 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout> with Sing
void _handleExchangeChanged(String newExchange, String newTicker) {
setState(() {
_selectedExchange = newExchange;
//_selectedExchange = newExchange;
_selectedTicker = newTicker;
});
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.symbol, exchange: newExchange, ticker: newTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, ticker: newTicker, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, ticker: newTicker, forceRefresh: false));
context.read<AssetHeaderBloc>().add(
LoadAssetHeader(widget.isin, exchange: newExchange, ticker: newTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin,
ticker: newTicker, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
ticker: newTicker, forceRefresh: false));
final favCubit = context.read<FavoritesCubit>();
if (favCubit.state.isFavorite(widget.symbol)) {
favCubit.updateFavoriteTicker(widget.symbol, newTicker);
if (favCubit.state.isFavorite(widget.isin)) {
favCubit.updateFavoriteTicker(widget.isin, newTicker);
}
}
void _handleForceRefresh() {
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.symbol, forceRefresh: true, exchange: _selectedExchange, ticker: _selectedTicker));
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTradesBloc>().add(LoadAssetTrades(widget.symbol));
context.read<AssetHeaderBloc>().add(LoadAssetHeader(widget.isin,
forceRefresh: true, ticker: _selectedTicker));
// AssetFundamentalsBloc is omitted here because AssetHeaderBloc already triggers forceRefresh=true
// for fundamentals, and the listener below will fetch the updated data with forceRefresh=false.
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.isin,
ticker: _selectedTicker, forceRefresh: true));
context.read<AssetTradesBloc>().add(LoadAssetTrades(widget.isin));
}
@override
@@ -73,56 +83,70 @@ class _AssetPageMobileLayoutState extends State<AssetPageMobileLayout> with Sing
if (state is AssetHeaderLoaded && state.data != null) {
if (_selectedTicker == null) {
setState(() {
_selectedTicker = state.data!.symbol;
_selectedExchange = state.data!.exchange;
_selectedTicker = widget.selectedTicker;
//_selectedExchange = state.data!.exchange;
});
// Re-trigger fundamentals and TA with resolved ticker
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, ticker: state.data!.symbol, forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, ticker: state.data!.symbol, forceRefresh: false));
}
// Re-trigger fundamentals and TA with resolved ticker whenever header loads (e.g. after force refresh)
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(
widget.isin,
ticker: _selectedTicker,
forceRefresh: false));
}
},
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: AssetHeroHeader(
symbol: widget.symbol,
selectedExchange: _selectedExchange,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
TabBar(
controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: Colors.transparent,
labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
tabs: const [
Tab(text: 'OVERVIEW'),
Tab(text: 'TECHNICAL'),
Tab(text: 'TRADES'),
],
return [
SliverToBoxAdapter(
child: AssetHeroHeader(
isin: widget.isin,
name: widget.name ?? widget.isin,
symbol: _selectedTicker ?? widget.selectedTicker,
onExchangeChanged: _handleExchangeChanged,
onForceRefresh: _handleForceRefresh,
),
theme.cardSurface,
),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
FundamentalsTab(symbol: _selectedTicker ?? widget.symbol),
TechnicalTab(symbol: _selectedTicker ?? widget.symbol),
TradesTab(symbol: widget.symbol),
],
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
TabBar(
controller: _tabController,
labelColor: theme.primaryColor,
unselectedLabelColor: theme.textMuted,
indicatorColor: theme.primaryColor,
dividerColor: Colors.transparent,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 13),
tabs: const [
Tab(text: 'OVERVIEW'),
Tab(text: 'TECHNICAL'),
Tab(text: 'TRADES'),
],
),
theme.cardSurface,
),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
FundamentalsTab(
isin: widget.isin,
symbol: _selectedTicker,
),
TechnicalTab(
isin: widget.isin,
symbol: _selectedTicker,
),
TradesTab(symbol: widget.isin),
],
),
),
),
);
}
}
@@ -135,11 +159,13 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@override
double get minExtent => _tabBar.preferredSize.height;
@override
double get maxExtent => _tabBar.preferredSize.height;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: _backgroundColor,
child: _tabBar,
@@ -10,8 +10,9 @@ import '../../bloc/fundamentals/asset_fundamentals_state.dart';
import '../../utils/metric_explanations.dart';
class FundamentalsTab extends StatefulWidget {
final String symbol;
const FundamentalsTab({super.key, required this.symbol});
final String isin;
final String? symbol;
const FundamentalsTab({super.key, this.symbol, required this.isin});
@override
State<FundamentalsTab> createState() => _FundamentalsTabState();
@@ -24,15 +25,11 @@ class _FundamentalsTabState extends State<FundamentalsTab> {
@override
void initState() {
super.initState();
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, forceRefresh: false));
}
@override
void didUpdateWidget(covariant FundamentalsTab oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.symbol != widget.symbol) {
context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, forceRefresh: false));
}
}
@override
@@ -55,7 +52,7 @@ class _FundamentalsTabState extends State<FundamentalsTab> {
Text('Fehler beim Laden der Fundamentaldaten: ${state.message}', style: const TextStyle(color: Colors.white70)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, forceRefresh: true)),
onPressed: () => context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin, ticker: widget.symbol, forceRefresh: true)),
icon: const Icon(Icons.refresh),
label: const Text('Erneut versuchen'),
),
@@ -515,7 +512,7 @@ class _FundamentalsTabState extends State<FundamentalsTab> {
Text('Für dieses Asset wurden noch keine Bilanz- oder Bewertungskennzahlen erfasst.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.symbol, forceRefresh: true)),
onPressed: () => context.read<AssetFundamentalsBloc>().add(LoadAssetFundamentals(widget.isin, ticker: widget.symbol, forceRefresh: true)),
icon: const Icon(Icons.download),
label: const Text('Daten von Backend abrufen'),
),
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/glass_container.dart';
@@ -11,13 +12,15 @@ import '../../utils/pattern_explanations.dart';
import '../../widgets/chart/candlestick_chart.dart';
class TechnicalTab extends StatefulWidget {
final String symbol;
final String isin;
final String? symbol;
final bool isDesktopLeftPanel;
const TechnicalTab({
super.key,
required this.symbol,
this.symbol,
this.isDesktopLeftPanel = false,
required this.isin,
});
@override
@@ -38,15 +41,11 @@ class _TechnicalTabState extends State<TechnicalTab> {
@override
void initState() {
super.initState();
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, forceRefresh: false));
}
@override
void didUpdateWidget(covariant TechnicalTab oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.symbol != widget.symbol) {
context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, forceRefresh: false));
}
}
@override
@@ -54,7 +53,8 @@ class _TechnicalTabState extends State<TechnicalTab> {
return BlocBuilder<AssetTechnicalBloc, AssetTechnicalState>(
builder: (context, state) {
if (state is AssetTechnicalLoading) {
return Center(child: CircularProgressIndicator(color: AppTheme.primaryEmerald));
return Center(
child: CircularProgressIndicator(color: AppTheme.primaryEmerald));
}
if (state is AssetTechnicalError) {
@@ -66,10 +66,13 @@ class _TechnicalTabState extends State<TechnicalTab> {
children: [
Icon(Icons.show_chart, color: AppTheme.accentRed, size: 48),
const SizedBox(height: 12),
Text('Fehler beim Laden der Technischen Analyse: ${state.message}', style: const TextStyle(color: Colors.white70)),
Text(
'Fehler beim Laden der Technischen Analyse: ${state.message}',
style: const TextStyle(color: Colors.white70)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => context.read<AssetTechnicalBloc>().add(LoadAssetTechnical(widget.symbol, forceRefresh: true)),
onPressed: () => context.read<AssetTechnicalBloc>().add(
LoadAssetTechnical(widget.isin, ticker: widget.symbol, forceRefresh: true)),
icon: const Icon(Icons.refresh),
label: const Text('Erneut versuchen'),
),
@@ -87,10 +90,40 @@ class _TechnicalTabState extends State<TechnicalTab> {
List<IndicatorModel> indicators = [];
if (data != null) {
candles = data.candles.map((c) => CandleModel(time: c.timestamp, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume)).toList();
patterns = []; // Since data.patterns is a List of Strings, we don't have point coordinates to draw them on the chart
signals = data.signals.map((s) => StrategySignalModel(type: 'strategy', timestamp: s.date, direction: s.type, price: s.price, description: s.title)).toList();
indicators = data.indicators.map((i) => IndicatorModel(timestamp: i.timestamp, ema20: i.ema20, sma50: i.sma50, sma200: i.sma200, supertrendUpper: i.supertrendUpper, supertrendLower: i.supertrendLower, supertrendDirection: i.supertrendDirection)).toList();
candles = data.candles
.map((c) => CandleModel(
time: c.timestamp,
open: c.open,
high: c.high,
low: c.low,
close: c.close,
volume: c.volume))
.toList();
patterns = data.patterns
.map((p) => ChartPatternModel(
type: p.type,
upperLine: p.upperLine.map((pt) => PatternPoint(pt.time, pt.price)).toList(),
lowerLine: p.lowerLine.map((pt) => PatternPoint(pt.time, pt.price)).toList(),
))
.toList();
signals = data.signals
.map((s) => StrategySignalModel(
type: 'strategy',
timestamp: s.date,
direction: s.type,
price: s.price,
description: s.title))
.toList();
indicators = data.indicators
.map((i) => IndicatorModel(
timestamp: i.timestamp,
ema20: i.ema20,
sma50: i.sma50,
sma200: i.sma200,
supertrendUpper: i.supertrendUpper,
supertrendLower: i.supertrendLower,
supertrendDirection: i.supertrendDirection))
.toList();
}
// Filter patterns according to individual checkbox states
@@ -105,22 +138,47 @@ class _TechnicalTabState extends State<TechnicalTab> {
children: [
// Glassmorphic Indicator & Pattern Control Ribbon
GlassContainer(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildIndicatorChip('EMA (20)', _showEma, (v) => setState(() => _showEma = v), Colors.blueAccent),
_buildIndicatorChip(
'EMA (20)',
_showEma,
(v) => setState(() => _showEma = v),
Colors.blueAccent),
const SizedBox(width: 6),
_buildIndicatorChip('SMA (50)', _showSma50, (v) => setState(() => _showSma50 = v), Colors.orangeAccent),
_buildIndicatorChip(
'SMA (50)',
_showSma50,
(v) => setState(() => _showSma50 = v),
Colors.orangeAccent),
const SizedBox(width: 6),
_buildIndicatorChip('SMA (200)', _showSma200, (v) => setState(() => _showSma200 = v), Colors.redAccent),
_buildIndicatorChip(
'SMA (200)',
_showSma200,
(v) => setState(() => _showSma200 = v),
Colors.redAccent),
const SizedBox(width: 6),
_buildIndicatorChip('Supertrend', _showSupertrend, (v) => setState(() => _showSupertrend = v), AppTheme.primaryEmerald),
_buildIndicatorChip(
'Supertrend',
_showSupertrend,
(v) => setState(() => _showSupertrend = v),
AppTheme.primaryEmerald),
const SizedBox(width: 6),
_buildIndicatorChip('Alle Muster', _showPatterns, (v) => setState(() => _showPatterns = v), Colors.amberAccent),
_buildIndicatorChip(
'Alle Muster',
_showPatterns,
(v) => setState(() => _showPatterns = v),
Colors.amberAccent),
const SizedBox(width: 6),
_buildIndicatorChip('Signale', _showSignals, (v) => setState(() => _showSignals = v), AppTheme.accentCyan),
_buildIndicatorChip(
'Signale',
_showSignals,
(v) => setState(() => _showSignals = v),
AppTheme.accentCyan),
],
),
),
@@ -156,24 +214,42 @@ class _TechnicalTabState extends State<TechnicalTab> {
children: [
Row(
children: [
Icon(Icons.architecture_outlined, color: AppTheme.primaryEmerald, size: 20),
Icon(Icons.architecture_outlined,
color: AppTheme.primaryEmerald, size: 20),
const SizedBox(width: 8),
const Text('Erkannte Chart-Muster & Signale', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
const Text('Erkannte Chart-Muster & Signale',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white)),
],
),
if (patterns.isNotEmpty)
TextButton.icon(
onPressed: () {
setState(() {
if (_disabledPatternIndices.length == patterns.length) {
if (_disabledPatternIndices.length ==
patterns.length) {
_disabledPatternIndices.clear();
} else {
_disabledPatternIndices.addAll(List.generate(patterns.length, (i) => i));
_disabledPatternIndices.addAll(
List.generate(
patterns.length, (i) => i));
}
});
},
icon: Icon(_disabledPatternIndices.isEmpty ? Icons.deselect : Icons.select_all, size: 16, color: Colors.amberAccent),
label: Text(_disabledPatternIndices.isEmpty ? 'Alle abwählen' : 'Alle anwählen', style: const TextStyle(color: Colors.amberAccent, fontSize: 12)),
icon: Icon(
_disabledPatternIndices.isEmpty
? Icons.deselect
: Icons.select_all,
size: 16,
color: Colors.amberAccent),
label: Text(
_disabledPatternIndices.isEmpty
? 'Alle abwählen'
: 'Alle anwählen',
style: const TextStyle(
color: Colors.amberAccent, fontSize: 12)),
),
],
),
@@ -182,18 +258,33 @@ class _TechnicalTabState extends State<TechnicalTab> {
GlassContainer(
padding: const EdgeInsets.all(16),
child: Center(
child: Text('Zurzeit wurden keine akuten Formationen oder Strategie-Signale identifiziert.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12)),
child: Text(
'Zurzeit wurden keine akuten Formationen oder Strategie-Signale identifiziert.',
style: TextStyle(
color: AppTheme.textMuted, fontSize: 12)),
),
)
else ...[
if (patterns.isNotEmpty) ...[
Text('Formationen & Trendlinien (Mit Checkbox im Chart schalten):', style: TextStyle(color: AppTheme.textSecondary, fontWeight: FontWeight.w600, fontSize: 13)),
Text(
'Formationen & Trendlinien (Mit Checkbox im Chart schalten):',
style: TextStyle(
color: AppTheme.textSecondary,
fontWeight: FontWeight.w600,
fontSize: 13)),
const SizedBox(height: 6),
...List.generate(patterns.length, (index) => _buildPatternCard(patterns[index], index)),
...List.generate(
patterns.length,
(index) =>
_buildPatternCard(patterns[index], index)),
const SizedBox(height: 12),
],
if (signals.isNotEmpty) ...[
Text('Strategie-Signale:', style: TextStyle(color: AppTheme.textSecondary, fontWeight: FontWeight.w600, fontSize: 13)),
Text('Strategie-Signale:',
style: TextStyle(
color: AppTheme.textSecondary,
fontWeight: FontWeight.w600,
fontSize: 13)),
const SizedBox(height: 6),
...signals.map((s) => _buildSignalCard(s)),
],
@@ -208,7 +299,8 @@ class _TechnicalTabState extends State<TechnicalTab> {
}
return Center(
child: Text('Keine technisches Indikatoren verfügbar', style: TextStyle(color: AppTheme.textMuted)),
child: Text('Keine technisches Indikatoren verfügbar',
style: TextStyle(color: AppTheme.textMuted)),
);
},
);
@@ -216,6 +308,21 @@ class _TechnicalTabState extends State<TechnicalTab> {
Widget _buildPatternCard(ChartPatternModel pattern, int index) {
final isEnabled = !_disabledPatternIndices.contains(index);
final patternColor = PatternExplanations.getColorForPattern(pattern.type);
final allPoints = [...pattern.upperLine, ...pattern.lowerLine];
DateTime? startDate;
DateTime? endDate;
if (allPoints.isNotEmpty) {
allPoints.sort((a, b) => a.time.compareTo(b.time));
startDate = allPoints.first.time;
endDate = allPoints.last.time;
}
final dateFormat = DateFormat('dd.MM.yy');
final dateStr = startDate != null && endDate != null
? '${dateFormat.format(startDate)} - ${dateFormat.format(endDate)}'
: 'Unbekannt';
return Padding(
padding: const EdgeInsets.only(bottom: 8),
@@ -226,9 +333,10 @@ class _TechnicalTabState extends State<TechnicalTab> {
// Checkbox for individual pattern toggling on the chart
Checkbox(
value: isEnabled,
activeColor: Colors.amberAccent,
activeColor: patternColor,
checkColor: Colors.black,
side: BorderSide(color: Colors.amberAccent.withValues(alpha: 0.6)),
side:
BorderSide(color: patternColor.withValues(alpha: 0.6)),
onChanged: (bool? val) {
setState(() {
if (val == true) {
@@ -241,19 +349,27 @@ class _TechnicalTabState extends State<TechnicalTab> {
),
Expanded(
child: InkWell(
onTap: () => PatternExplanations.showPatternDetails(context, pattern.type),
onTap: () => PatternExplanations.showPatternDetails(
context, pattern.type),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
padding:
const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isEnabled ? Colors.amberAccent.withValues(alpha: 0.15) : AppTheme.glassSurface,
color: isEnabled
? patternColor.withValues(alpha: 0.15)
: AppTheme.glassSurface,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.polyline_outlined, color: isEnabled ? Colors.amberAccent : AppTheme.textMuted, size: 20),
child: Icon(Icons.polyline_outlined,
color: isEnabled
? patternColor
: AppTheme.textMuted,
size: 20),
),
const SizedBox(width: 12),
Expanded(
@@ -266,26 +382,33 @@ class _TechnicalTabState extends State<TechnicalTab> {
pattern.type,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isEnabled ? Colors.white : AppTheme.textMuted,
color: isEnabled
? Colors.white
: AppTheme.textMuted,
fontSize: 14,
decoration: isEnabled ? null : TextDecoration.lineThrough,
decoration: isEnabled
? null
: TextDecoration.lineThrough,
),
),
const SizedBox(width: 6),
Icon(Icons.info_outline, size: 14, color: AppTheme.textMuted),
Icon(Icons.info_outline,
size: 14, color: AppTheme.textMuted),
],
),
const SizedBox(height: 4),
Text(
'Formationspunkte: Oberer Trendkanal (${pattern.upperLine.length} Pkt.) / Unterer Trendkanal (${pattern.lowerLine.length} Pkt.)',
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
'Zeitraum: $dateStr\n'
'Linien: Oben (${pattern.upperLine.length} Pkt.) / Unten (${pattern.lowerLine.length} Pkt.)',
style: TextStyle(
color: AppTheme.textMuted, fontSize: 11, height: 1.3),
),
],
),
),
StatusBadge(
label: isEnabled ? 'AKTIV' : 'AUS',
color: isEnabled ? Colors.amberAccent : AppTheme.textMuted,
color:
isEnabled ? patternColor : AppTheme.textMuted,
),
],
),
@@ -314,7 +437,8 @@ class _TechnicalTabState extends State<TechnicalTab> {
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(isBuy ? Icons.north_east : Icons.south_east, color: color, size: 20),
child: Icon(isBuy ? Icons.north_east : Icons.south_east,
color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
@@ -323,13 +447,26 @@ class _TechnicalTabState extends State<TechnicalTab> {
children: [
Row(
children: [
Text(signal.type.toUpperCase(), style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 14)),
Text(signal.type.toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 14)),
const SizedBox(width: 8),
Text('@ €${signal.price.toStringAsFixed(2)}', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
Text('@ €${signal.price.toStringAsFixed(2)}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13)),
],
),
const SizedBox(height: 4),
Text(signal.description.isNotEmpty ? signal.description : 'Strategisches Kaufsignal ausgelöst durch technische Indikatoren.', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
Text(
signal.description.isNotEmpty
? signal.description
: 'Strategisches Kaufsignal ausgelöst durch technische Indikatoren.',
style: TextStyle(
color: AppTheme.textSecondary, fontSize: 12)),
],
),
),
@@ -340,13 +477,18 @@ class _TechnicalTabState extends State<TechnicalTab> {
);
}
Widget _buildIndicatorChip(String label, bool isSelected, ValueChanged<bool> onChanged, Color color) {
Widget _buildIndicatorChip(String label, bool isSelected,
ValueChanged<bool> onChanged, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
FilterChip(
selected: isSelected,
label: Text(label, style: TextStyle(color: isSelected ? Colors.black : color, fontSize: 11, fontWeight: FontWeight.bold)),
label: Text(label,
style: TextStyle(
color: isSelected ? Colors.black : color,
fontSize: 11,
fontWeight: FontWeight.bold)),
selectedColor: color,
backgroundColor: color.withValues(alpha: 0.15),
side: BorderSide(color: color.withValues(alpha: 0.4)),
@@ -357,7 +499,8 @@ class _TechnicalTabState extends State<TechnicalTab> {
onTap: () => MetricExplanations.show(context, label),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Icon(Icons.info_outline, size: 14, color: AppTheme.textMuted),
child:
Icon(Icons.info_outline, size: 14, color: AppTheme.textMuted),
),
),
],
@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../../utils/pattern_explanations.dart';
class CandleModel {
final DateTime time;
@@ -518,16 +519,16 @@ class _CandlePainter extends CustomPainter {
}
if (showEma && !firstEma20) {
canvas.drawPath(ema20Path, Paint()..color = theme.primaryColor..style = PaintingStyle.stroke..strokeWidth = 1.5);
canvas.drawPath(ema20Path, Paint()..color = Colors.blueAccent..style = PaintingStyle.stroke..strokeWidth = 1.5);
}
if (showSma50 && !firstSma50) {
canvas.drawPath(sma50Path, Paint()..color = Colors.orangeAccent..style = PaintingStyle.stroke..strokeWidth = 1.5);
}
if (showSma200 && !firstSma200) {
canvas.drawPath(sma200Path, Paint()..color = Colors.purpleAccent..style = PaintingStyle.stroke..strokeWidth = 2.0);
canvas.drawPath(sma200Path, Paint()..color = Colors.redAccent..style = PaintingStyle.stroke..strokeWidth = 2.0);
}
if (showSupertrend && !firstSupertrend) {
canvas.drawPath(supertrendPath, Paint()..color = Colors.lightBlueAccent..style = PaintingStyle.stroke..strokeWidth = 2.0);
canvas.drawPath(supertrendPath, Paint()..color = AppTheme.primaryEmerald..style = PaintingStyle.stroke..strokeWidth = 2.0);
}
if (showPatterns) {
@@ -702,12 +703,13 @@ class _CandlePainter extends CustomPainter {
}
void _drawPatterns(Canvas canvas, double Function(DateTime) getX, double Function(double) getY) {
final paint = Paint()
..color = Colors.orangeAccent
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
for (var pattern in patterns) {
final color = PatternExplanations.getColorForPattern(pattern.type);
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
void drawLine(List<PatternPoint> points) {
if (points.length < 2) return;
final path = Path();
@@ -6,19 +6,19 @@ import '../../../../shared/widgets/favorite_star_button.dart';
import '../../bloc/header/asset_header_bloc.dart';
import '../../bloc/header/asset_header_state.dart';
import '../../models/asset_model.dart';
import 'package:url_launcher/url_launcher.dart';
class AssetHeroHeader extends StatelessWidget {
final String symbol;
final String isin;
final String name;
final String? symbol;
final void Function(String exchange, String ticker)? onExchangeChanged;
final VoidCallback? onForceRefresh;
final String? selectedExchange;
const AssetHeroHeader({
super.key,
required this.symbol,
this.onExchangeChanged,
this.onForceRefresh,
this.selectedExchange,
this.onForceRefresh, required this.isin, required this.name, this.symbol,
});
@override
@@ -27,10 +27,8 @@ class AssetHeroHeader extends StatelessWidget {
return BlocBuilder<AssetHeaderBloc, AssetHeaderState>(
builder: (context, state) {
String name = symbol;
double? price;
String currency = 'EUR';
String currentExchange = selectedExchange ?? 'XETRA';
List<AssetTickerOption> tickerOptions = [
AssetTickerOption(ticker: 'Loading', exchange: 'Loading', tradingCurrency: '', currentPrice: 0.0)
];
@@ -43,10 +41,9 @@ class AssetHeroHeader extends StatelessWidget {
}
if (asset != null) {
name = asset.name.isNotEmpty ? asset.name : symbol;
//name = asset.name.isNotEmpty ? asset.name : symbol;
currency = asset.currency.isNotEmpty ? asset.currency : 'EUR';
price = asset.currentPrice;
currentExchange = selectedExchange ?? asset.exchange;
if (asset.tickers.isNotEmpty) {
tickerOptions = asset.tickers;
@@ -54,7 +51,7 @@ class AssetHeroHeader extends StatelessWidget {
}
final selectedOption = tickerOptions.firstWhere(
(t) => t.exchange == currentExchange,
(t) => t.ticker == symbol || t.exchange == symbol,
orElse: () => tickerOptions.first,
);
@@ -89,7 +86,7 @@ class AssetHeroHeader extends StatelessWidget {
),
const SizedBox(width: 8),
],
AssetLogoWidget(symbolOrName: symbol, size: 48),
AssetLogoWidget(symbolOrName: isin, imageUrl: asset?.image, size: 48),
const SizedBox(width: 16),
Expanded(
child: Column(
@@ -106,7 +103,7 @@ class AssetHeroHeader extends StatelessWidget {
),
const SizedBox(height: 2),
SelectableText(
symbol,
isin,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@@ -122,13 +119,24 @@ class AssetHeroHeader extends StatelessWidget {
),
Row(
children: [
IconButton(
tooltip: 'Open in Yahoo Finance',
icon: Icon(Icons.open_in_new, color: theme.textSecondary),
onPressed: () async {
final url = Uri.parse('https://finance.yahoo.com/quote/${selectedOption.ticker}');
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
},
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Force Refresh Data',
icon: Icon(Icons.refresh, color: theme.primaryColor),
onPressed: onForceRefresh,
),
const SizedBox(width: 8),
FavoriteStarButton(symbol: symbol, identifier: symbol, name: name),
FavoriteStarButton(symbol: symbol, identifier: isin, name: name),
],
),
],
@@ -142,7 +150,7 @@ class AssetHeroHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'LIVE PRICE',
'AKTUELLER PREIS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
@@ -180,15 +188,15 @@ class AssetHeroHeader extends StatelessWidget {
),
// Interactive Ticker & Exchange Selector Dropdown
PopupMenuButton<String>(
initialValue: selectedOption.exchange,
initialValue: selectedOption.ticker,
tooltip: 'Select Exchange & Ticker',
onSelected: (newExchange) {
onSelected: (newTicker) {
if (onExchangeChanged != null) {
final opt = tickerOptions.firstWhere(
(t) => t.exchange == newExchange,
(t) => t.ticker == newTicker,
orElse: () => tickerOptions.first,
);
onExchangeChanged!(newExchange, opt.ticker);
onExchangeChanged!(opt.exchange, opt.ticker);
}
},
itemBuilder: (context) {
@@ -196,10 +204,10 @@ class AssetHeroHeader extends StatelessWidget {
final ex = opt.exchange;
final tick = opt.ticker;
final label = '$tick ($ex)';
final isSelected = ex == currentExchange;
final isSelected = tick == symbol || ex == symbol;
return PopupMenuItem<String>(
value: ex,
value: tick,
child: Row(
children: [
Icon(