refactor: save current workspace state including FinlyticAnalyzer fixes, FinlyticApp trade route alignment, and DTO audit documentation
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user