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
@@ -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),
),
),
],