feat(app): responsive asset detail layout, full width chart, reactive hero header, shimmer loaders and enriched fundamentals

This commit is contained in:
2026-08-14 23:57:03 +02:00
parent f94e3b8164
commit 1d244b338a
22 changed files with 1950 additions and 1074 deletions
@@ -29,6 +29,16 @@ class FundamentalDataModel extends Equatable {
final double? evToEbitda;
final double? evToRevenue;
final double? totalRevenue;
final double? revenueGrowthYoY;
final double? grossProfit;
final double? ebitda;
final double? dilutedEps;
final double? totalCash;
final double? totalDebt;
final double? operatingCashFlow;
final double? freeCashFlow;
final double? grossMargin;
final double? operatingMargin;
final double? netProfitMargin;
@@ -86,6 +96,15 @@ class FundamentalDataModel extends Equatable {
this.psRatio,
this.evToEbitda,
this.evToRevenue,
this.totalRevenue,
this.revenueGrowthYoY,
this.grossProfit,
this.ebitda,
this.dilutedEps,
this.totalCash,
this.totalDebt,
this.operatingCashFlow,
this.freeCashFlow,
this.grossMargin,
this.operatingMargin,
this.netProfitMargin,
@@ -128,14 +147,121 @@ class FundamentalDataModel extends Equatable {
return double.tryParse(val.toString());
}
final assetMap = json['asset'] is Map<String, dynamic> ? json['asset'] as Map<String, dynamic> : null;
final fundMap = json['fundamentals'] is Map<String, dynamic> ? json['fundamentals'] as Map<String, dynamic> : null;
String extractTickerStr(dynamic val) {
if (val == null) return '';
if (val is Map<String, dynamic>) {
return val['ticker']?.toString() ?? '';
}
return val.toString();
}
String? extractExchangeStr(dynamic val) {
if (val == null) return null;
if (val is Map<String, dynamic>) {
return val['exchange']?.toString();
}
return null;
}
final isinVal = assetMap?['isin']?.toString() ?? json['isin']?.toString() ?? '';
final primaryTickerVal = extractTickerStr(assetMap?['primaryTicker'] ?? json['primaryTicker']);
final tickerVal = extractTickerStr(fundMap?['ticker'] ?? json['ticker']).isNotEmpty
? extractTickerStr(fundMap?['ticker'] ?? json['ticker'])
: primaryTickerVal;
final companyNameVal = assetMap?['name']?.toString() ?? json['companyName']?.toString() ?? json['name']?.toString() ?? tickerVal;
final businessSummaryVal = assetMap?['description']?.toString() ?? json['businessSummary']?.toString() ?? json['description']?.toString();
final exchangeVal = extractExchangeStr(fundMap?['ticker']) ??
extractExchangeStr(assetMap?['primaryTicker']) ??
json['exchange']?.toString();
final rawTickers = assetMap?['availableTickers'] ?? json['availableTickers'];
List<TickerModel> availableTickersList = [];
if (rawTickers is List) {
availableTickersList = rawTickers.map((t) {
if (t is Map<String, dynamic>) {
return TickerModel.fromJson(t);
} else {
return TickerModel(ticker: t.toString());
}
}).toList();
}
// Revenue & Margins Derivation
final totalRev = parseNullableDouble(fundMap?['totalRevenue'] ?? json['totalRevenue']);
final grossProf = parseNullableDouble(fundMap?['grossProfit'] ?? json['grossProfit']);
double? grossMarginVal = parseNullableDouble(fundMap?['grossMargin'] ?? json['grossMargin']);
if (grossMarginVal == null && grossProf != null) {
if (grossProf <= 1.0 && grossProf >= 0.0) {
grossMarginVal = grossProf;
} else if (totalRev != null && totalRev > 0) {
grossMarginVal = grossProf / totalRev;
}
}
// Enterprise Value to Revenue
final evVal = parseNullableDouble(fundMap?['enterpriseValue'] ?? json['enterpriseValue']);
double? evToRevVal = parseNullableDouble(fundMap?['evToRevenue'] ?? fundMap?['enterpriseValueToRevenue'] ?? json['evToRevenue']);
if (evToRevVal == null && evVal != null && totalRev != null && totalRev > 0) {
evToRevVal = evVal / totalRev;
}
// Event Dates (Ex-Dividend & Next Earnings)
String? exDividendDateVal = json['exDividendDate']?.toString() ?? fundMap?['exDividendDate']?.toString();
String? nextEarningsDateVal = json['nextEarningsDate']?.toString() ?? fundMap?['nextEarningsDate']?.toString();
final rawEvents = json['events'];
if (rawEvents is List && rawEvents.isNotEmpty) {
final now = DateTime.now();
final parsedEvents = <Map<String, dynamic>>[];
for (final ev in rawEvents) {
if (ev is Map<String, dynamic>) {
final dtStr = ev['date']?.toString();
final dt = dtStr != null ? DateTime.tryParse(dtStr) : null;
if (dt != null) {
parsedEvents.add({
'type': ev['type']?.toString().toUpperCase() ?? '',
'date': dt,
'dateStr': dtStr,
});
}
}
}
if (exDividendDateVal == null) {
final dividendEvents = parsedEvents.where((e) => e['type'] == 'DIVIDEND').toList()
..sort((a, b) => (a['date'] as DateTime).compareTo(b['date'] as DateTime));
final futureDividends = dividendEvents.where((e) => (e['date'] as DateTime).isAfter(now)).toList();
if (futureDividends.isNotEmpty) {
exDividendDateVal = futureDividends.first['dateStr'] as String;
} else if (dividendEvents.isNotEmpty) {
exDividendDateVal = dividendEvents.last['dateStr'] as String;
}
}
if (nextEarningsDateVal == null) {
final earningsEvents = parsedEvents.where((e) => e['type'] == 'EARNINGS_RELEASE' || e['type'] == 'EARNINGS_CALL').toList()
..sort((a, b) => (a['date'] as DateTime).compareTo(b['date'] as DateTime));
final futureEarnings = earningsEvents.where((e) => (e['date'] as DateTime).isAfter(now)).toList();
if (futureEarnings.isNotEmpty) {
nextEarningsDateVal = futureEarnings.first['dateStr'] as String;
} else if (earningsEvents.isNotEmpty) {
nextEarningsDateVal = earningsEvents.last['dateStr'] as String;
}
}
}
return FundamentalDataModel(
isin: json['isin']?.toString() ?? '',
primaryTicker: json['primaryTicker']?.toString() ?? '',
ticker: json['ticker']?.toString() ?? '',
companyName: json['companyName']?.toString() ?? '',
exchange: json['exchange']?.toString(),
isin: isinVal,
primaryTicker: primaryTickerVal,
ticker: tickerVal,
companyName: companyNameVal,
exchange: exchangeVal,
tradingCurrency: json['tradingCurrency']?.toString(),
businessSummary: json['businessSummary']?.toString(),
businessSummary: businessSummaryVal,
sector: json['sector']?.toString(),
industry: json['industry']?.toString(),
country: json['country']?.toString(),
@@ -143,56 +269,62 @@ class FundamentalDataModel extends Equatable {
currentPrice: parseDouble(json['currentPrice']),
dayChangeAbsolute: parseDouble(json['dayChangeAbsolute']),
dayChangePercent: parseDouble(json['dayChangePercent']),
fiftyTwoWeekHigh: parseDouble(json['fiftyTwoWeekHigh']),
fiftyTwoWeekLow: parseDouble(json['fiftyTwoWeekLow']),
marketCapitalization: parseDouble(json['marketCapitalization'] ?? json['marketCap']),
enterpriseValue: parseDouble(json['enterpriseValue']),
peRatioTrailing: parseNullableDouble(json['peRatioTrailing'] ?? json['peRatio']),
peRatioForward: parseNullableDouble(json['peRatioForward']),
pegRatio: parseNullableDouble(json['pegRatio']),
pbRatio: parseNullableDouble(json['pbRatio']),
psRatio: parseNullableDouble(json['psRatio']),
evToEbitda: parseNullableDouble(json['evToEbitda']),
evToRevenue: parseNullableDouble(json['evToRevenue']),
grossMargin: parseNullableDouble(json['grossMargin']),
operatingMargin: parseNullableDouble(json['operatingMargin']),
netProfitMargin: parseNullableDouble(json['netProfitMargin']),
returnOnEquity: parseNullableDouble(json['returnOnEquity']),
returnOnAssets: parseNullableDouble(json['returnOnAssets']),
returnOnInvestedCapital: parseNullableDouble(json['returnOnInvestedCapital']),
debtToEquity: parseNullableDouble(json['debtToEquity']),
currentRatio: parseNullableDouble(json['currentRatio']),
quickRatio: parseNullableDouble(json['quickRatio']),
interestCoverage: parseNullableDouble(json['interestCoverage']),
dividendYield: parseNullableDouble(json['dividendYield']),
payoutRatio: parseNullableDouble(json['payoutRatio']),
exDividendDate: json['exDividendDate']?.toString(),
nextEarningsDate: json['nextEarningsDate']?.toString(),
percentHeldByInstitutions: parseNullableDouble(json['percentHeldByInstitutions']),
percentHeldByInsiders: parseNullableDouble(json['percentHeldByInsiders']),
shortRatio: parseNullableDouble(json['shortRatio']),
shortPercentOfFloat: parseNullableDouble(json['shortPercentOfFloat']),
consensusRating: json['consensusRating']?.toString(),
priceTargetLow: parseNullableDouble(json['priceTargetLow']),
priceTargetHigh: parseNullableDouble(json['priceTargetHigh']),
priceTargetMedian: parseNullableDouble(json['priceTargetMedian']),
priceTargetMean: parseNullableDouble(json['priceTargetMean']),
fiftyTwoWeekHigh: parseDouble(fundMap?['fiftyTwoWeekHigh'] ?? json['fiftyTwoWeekHigh']),
fiftyTwoWeekLow: parseDouble(fundMap?['fiftyTwoWeekLow'] ?? json['fiftyTwoWeekLow']),
marketCapitalization: parseDouble(fundMap?['marketCap'] ?? json['marketCapitalization'] ?? json['marketCap']),
enterpriseValue: parseDouble(fundMap?['enterpriseValue'] ?? json['enterpriseValue']),
peRatioTrailing: parseNullableDouble(fundMap?['trailingPe'] ?? json['peRatioTrailing'] ?? json['peRatio']),
peRatioForward: parseNullableDouble(fundMap?['forwardPe'] ?? json['peRatioForward']),
pegRatio: parseNullableDouble(fundMap?['pegRatio'] ?? json['pegRatio']),
pbRatio: parseNullableDouble(fundMap?['priceToBook'] ?? json['pbRatio']),
psRatio: parseNullableDouble(fundMap?['priceToSales'] ?? json['psRatio']),
evToEbitda: parseNullableDouble(fundMap?['evToEbitda'] ?? json['evToEbitda']),
evToRevenue: evToRevVal,
totalRevenue: totalRev,
revenueGrowthYoY: parseNullableDouble(fundMap?['revenueGrowthYoY'] ?? json['revenueGrowthYoY']),
grossProfit: grossProf,
ebitda: parseNullableDouble(fundMap?['ebitda'] ?? json['ebitda']),
dilutedEps: parseNullableDouble(fundMap?['dilutedEps'] ?? json['dilutedEps']),
totalCash: parseNullableDouble(fundMap?['totalCash'] ?? json['totalCash']),
totalDebt: parseNullableDouble(fundMap?['totalDebt'] ?? json['totalDebt']),
operatingCashFlow: parseNullableDouble(fundMap?['operatingCashFlow'] ?? json['operatingCashFlow']),
freeCashFlow: parseNullableDouble(fundMap?['freeCashFlow'] ?? json['freeCashFlow']),
grossMargin: grossMarginVal,
operatingMargin: parseNullableDouble(fundMap?['operatingMargin'] ?? fundMap?['operatingIncome'] ?? json['operatingMargin']),
netProfitMargin: parseNullableDouble(fundMap?['netProfitMargin'] ?? fundMap?['netIncome'] ?? json['netProfitMargin']),
returnOnEquity: parseNullableDouble(fundMap?['returnOnEquity'] ?? json['returnOnEquity']),
returnOnAssets: parseNullableDouble(fundMap?['returnOnAssets'] ?? json['returnOnAssets']),
returnOnInvestedCapital: parseNullableDouble(fundMap?['returnOnInvestedCapital'] ?? json['returnOnInvestedCapital']),
debtToEquity: parseNullableDouble(fundMap?['debtToEquity'] ?? json['debtToEquity']),
currentRatio: parseNullableDouble(fundMap?['currentRatio'] ?? json['currentRatio']),
quickRatio: parseNullableDouble(fundMap?['quickRatio'] ?? json['quickRatio']),
interestCoverage: parseNullableDouble(fundMap?['interestCoverage'] ?? json['interestCoverage']),
dividendYield: parseNullableDouble(fundMap?['forwardDividendYield'] ?? json['dividendYield']),
payoutRatio: parseNullableDouble(fundMap?['payoutRatio'] ?? json['payoutRatio']),
exDividendDate: exDividendDateVal,
nextEarningsDate: nextEarningsDateVal,
percentHeldByInstitutions: parseNullableDouble(fundMap?['percentHeldByInstitutions'] ?? json['percentHeldByInstitutions']),
percentHeldByInsiders: parseNullableDouble(fundMap?['percentHeldByInsiders'] ?? json['percentHeldByInsiders']),
shortRatio: parseNullableDouble(fundMap?['shortRatio'] ?? json['shortRatio']),
shortPercentOfFloat: parseNullableDouble(fundMap?['shortPercentOfFloat'] ?? json['shortPercentOfFloat']),
consensusRating: (fundMap?['consensusRating'] ?? json['consensusRating'])?.toString(),
priceTargetLow: parseNullableDouble(fundMap?['priceTargetLow'] ?? json['priceTargetLow']),
priceTargetHigh: parseNullableDouble(fundMap?['priceTargetHigh'] ?? json['priceTargetHigh']),
priceTargetMedian: parseNullableDouble(fundMap?['priceTargetMedian'] ?? json['priceTargetMedian']),
priceTargetMean: parseNullableDouble(fundMap?['priceTargetMean'] ?? json['priceTargetMean']),
executives: (json['executives'] as List?)
?.map((e) => CompanyExecutiveModel.fromJson(e))
?.map((e) => CompanyExecutiveModel.fromJson(e is Map<String, dynamic> ? e : {}))
.toList() ??
[],
financialStatements: (json['financialStatements'] as List?)
?.map((e) => FinancialStatementModel.fromJson(e))
?.map((e) => FinancialStatementModel.fromJson(e is Map<String, dynamic> ? e : {}))
.toList() ??
[],
estimates: (json['estimates'] as List?)
?.map((e) => ForwardEstimateModel.fromJson(e))
.toList() ??
[],
availableTickers: (json['availableTickers'] as List?)
?.map((e) => TickerModel.fromJson(e))
?.map((e) => ForwardEstimateModel.fromJson(e is Map<String, dynamic> ? e : {}))
.toList() ??
[],
availableTickers: availableTickersList,
);
}
@@ -322,11 +454,30 @@ class CompanyExecutiveModel extends Equatable {
});
factory CompanyExecutiveModel.fromJson(Map<String, dynamic> json) {
double? compVal;
if (json['compensation'] != null) {
compVal = double.tryParse(json['compensation'].toString());
} else if (json['payment'] != null) {
final pStr = json['payment'].toString().trim().toUpperCase().replaceAll('\$', '').replaceAll('', '').replaceAll('£', '').replaceAll(',', '').replaceAll(' ', '');
if (pStr.endsWith('M')) {
final numPart = double.tryParse(pStr.substring(0, pStr.length - 1));
if (numPart != null) compVal = numPart * 1e6;
} else if (pStr.endsWith('K')) {
final numPart = double.tryParse(pStr.substring(0, pStr.length - 1));
if (numPart != null) compVal = numPart * 1e3;
} else if (pStr.endsWith('B')) {
final numPart = double.tryParse(pStr.substring(0, pStr.length - 1));
if (numPart != null) compVal = numPart * 1e9;
} else {
compVal = double.tryParse(pStr);
}
}
return CompanyExecutiveModel(
name: json['name']?.toString() ?? '',
title: json['title']?.toString() ?? '',
age: json['age'] != null ? int.tryParse(json['age'].toString()) : null,
compensation: json['compensation'] != null ? double.tryParse(json['compensation'].toString()) : null,
compensation: compVal,
);
}
@@ -545,7 +696,7 @@ class TickerModel extends Equatable {
required this.ticker,
this.exchange,
this.tradingCurrency,
required this.currentPrice,
this.currentPrice = 0.0,
});
factory TickerModel.fromJson(Map<String, dynamic> json) {
@@ -0,0 +1,180 @@
import 'package:equatable/equatable.dart';
import '../../trades/models/trade_model.dart';
class ExecutionPlanModel extends Equatable {
final double stopLoss;
final List<double> takeProfitTargets;
final double riskRewardRatio;
final double maxLeverage;
const ExecutionPlanModel({
this.stopLoss = 0.0,
this.takeProfitTargets = const [],
this.riskRewardRatio = 0.0,
this.maxLeverage = 1.0,
});
factory ExecutionPlanModel.fromJson(Map<String, dynamic> json) {
double parseDbl(dynamic v) => (v as num?)?.toDouble() ?? 0.0;
return ExecutionPlanModel(
stopLoss: parseDbl(json['stopLoss']),
takeProfitTargets: (json['takeProfitTargets'] as List<dynamic>? ?? []).map((e) => parseDbl(e)).toList(),
riskRewardRatio: parseDbl(json['riskRewardRatio']),
maxLeverage: parseDbl(json['maxLeverage']) == 0 ? 1.0 : parseDbl(json['maxLeverage']),
);
}
@override
List<Object?> get props => [stopLoss, takeProfitTargets, riskRewardRatio, maxLeverage];
}
class DetailedAnalysisModel extends Equatable {
final String technicalRationale;
final String fundamentalRationale;
final String riskWarning;
const DetailedAnalysisModel({
this.technicalRationale = '',
this.fundamentalRationale = '',
this.riskWarning = '',
});
factory DetailedAnalysisModel.fromJson(Map<String, dynamic> json) {
return DetailedAnalysisModel(
technicalRationale: json['technicalRationale']?.toString() ?? '',
fundamentalRationale: json['fundamentalRationale']?.toString() ?? '',
riskWarning: json['riskWarning']?.toString() ?? '',
);
}
@override
List<Object?> get props => [technicalRationale, fundamentalRationale, riskWarning];
}
class N8nAnalysisResponseDto extends Equatable {
final String aiDecision; // "Proceed", "Rejected", "Hold"
final String aiReasoning;
final int evalScore;
final String suggestedDirection; // "Long", "Short"
final String suggestedRisk;
final String suggestedTimeframe;
final ExecutionPlanModel? executionPlan;
final DetailedAnalysisModel? detailedAnalysis;
const N8nAnalysisResponseDto({
this.aiDecision = 'Rejected',
this.aiReasoning = '',
this.evalScore = 0,
this.suggestedDirection = 'Long',
this.suggestedRisk = 'Moderate',
this.suggestedTimeframe = '1D',
this.executionPlan,
this.detailedAnalysis,
});
factory N8nAnalysisResponseDto.fromJson(Map<String, dynamic> json) {
ExecutionPlanModel? execPlan;
if (json['executionPlan'] != null && json['executionPlan'] is Map<String, dynamic>) {
execPlan = ExecutionPlanModel.fromJson(json['executionPlan']);
}
DetailedAnalysisModel? detailAnalysis;
if (json['detailedAnalysis'] != null && json['detailedAnalysis'] is Map<String, dynamic>) {
detailAnalysis = DetailedAnalysisModel.fromJson(json['detailedAnalysis']);
}
return N8nAnalysisResponseDto(
aiDecision: json['aiDecision']?.toString() ?? 'Rejected',
aiReasoning: json['aiReasoning']?.toString() ?? '',
evalScore: (json['evalScore'] as num?)?.toInt() ?? 0,
suggestedDirection: json['suggestedDirection']?.toString() ?? 'Long',
suggestedRisk: json['suggestedRisk']?.toString() ?? 'Moderate',
suggestedTimeframe: json['suggestedTimeframe']?.toString() ?? '1D',
executionPlan: execPlan,
detailedAnalysis: detailAnalysis,
);
}
@override
List<Object?> get props => [
aiDecision,
aiReasoning,
evalScore,
suggestedDirection,
suggestedRisk,
suggestedTimeframe,
executionPlan,
detailedAnalysis,
];
}
class ManualAnalysisResponseDto extends Equatable {
final String analysisId;
final bool isTradeProposed;
final String status;
final String recommendation; // "RECOMMENDED", "NOT_RECOMMENDED"
final N8nAnalysisResponseDto? n8nResponse;
final TradeModel? proposal;
final String message;
const ManualAnalysisResponseDto({
required this.analysisId,
this.isTradeProposed = false,
this.status = 'Success',
this.recommendation = 'NOT_RECOMMENDED',
this.n8nResponse,
this.proposal,
this.message = '',
});
factory ManualAnalysisResponseDto.fromJson(Map<String, dynamic> json) {
N8nAnalysisResponseDto? n8n;
if (json['n8nResponse'] != null && json['n8nResponse'] is Map<String, dynamic>) {
n8n = N8nAnalysisResponseDto.fromJson(json['n8nResponse']);
}
TradeModel? prop;
if (json['proposal'] != null && json['proposal'] is Map<String, dynamic>) {
prop = TradeModel.fromJson(json['proposal']);
} else if (n8n != null) {
final exec = n8n.executionPlan;
final det = n8n.detailedAnalysis;
final isProceed = n8n.aiDecision.toLowerCase() == 'proceed';
final analysisIdStr = (json['analysisId'] ?? json['AnalysisId'])?.toString() ?? '';
final tradeIdStr = 'PROP-${analysisIdStr.length > 10 ? analysisIdStr.substring(0, 10).toUpperCase() : 'MANUAL'}';
prop = TradeModel(
id: tradeIdStr,
analysisId: analysisIdStr,
symbol: (json['symbol'] ?? json['Symbol'])?.toString() ?? '',
isin: (json['isin'] ?? json['Isin'])?.toString() ?? '',
status: isProceed ? 'Proposed' : 'Rejected',
signalType: n8n.suggestedDirection.toUpperCase() == 'SHORT' ? 'SELL' : 'BUY',
entryPrice: 0.0,
stopLoss: exec?.stopLoss ?? 0.0,
takeProfit: (exec?.takeProfitTargets.isNotEmpty ?? false) ? exec!.takeProfitTargets.first : 0.0,
reasoning: n8n.aiReasoning,
technicalRationale: det?.technicalRationale ?? '',
fundamentalRationale: det?.fundamentalRationale ?? '',
riskWarning: det?.riskWarning ?? '',
takeProfitTargets: exec?.takeProfitTargets ?? const [],
maxLeverage: exec?.maxLeverage ?? 1.0,
riskTolerance: n8n.suggestedRisk,
timeframe: n8n.suggestedTimeframe,
);
}
return ManualAnalysisResponseDto(
analysisId: (json['analysisId'] ?? json['AnalysisId'])?.toString() ?? '',
isTradeProposed: json['isTradeProposed'] == true || json['IsTradeProposed'] == true,
status: (json['status'] ?? json['Status'])?.toString() ?? 'Success',
recommendation: (json['recommendation'] ?? json['Recommendation'])?.toString() ?? 'NOT_RECOMMENDED',
n8nResponse: n8n,
proposal: prop,
message: (json['message'] ?? json['Message'])?.toString() ?? '',
);
}
@override
List<Object?> get props => [analysisId, isTradeProposed, status, recommendation, n8nResponse, proposal, message];
}
@@ -106,11 +106,16 @@ class StrategySignalModel extends Equatable {
});
factory StrategySignalModel.fromJson(Map<String, dynamic> json) {
final rawDir = (json['direction'] ?? json['signalType'] ?? json['type'])?.toString().toUpperCase() ?? 'BUY';
final sigDir = (rawDir == 'BUY' || rawDir == 'SELL') ? rawDir : 'BUY';
final sigTitle = (json['title'] ?? json['type'] ?? json['description'])?.toString() ?? 'Signal';
final dateStr = (json['timestamp'] ?? json['date'] ?? json['time'])?.toString();
return StrategySignalModel(
title: json['title']?.toString() ?? '',
date: DateTime.tryParse(json['date']?.toString() ?? '') ?? DateTime.now(),
title: sigTitle,
date: dateStr != null ? (DateTime.tryParse(dateStr) ?? DateTime.now()) : DateTime.now(),
price: (json['price'] as num?)?.toDouble() ?? 0.0,
type: json['type']?.toString() ?? 'BUY',
type: sigDir,
);
}
@@ -123,33 +128,76 @@ class PatternPoint extends Equatable {
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());
factory PatternPoint.fromJson(Map<String, dynamic> json) => PatternPoint(DateTime.tryParse(json['time']?.toString() ?? '') ?? DateTime.now(), (json['price'] as num?)?.toDouble() ?? 0.0);
@override
List<Object?> get props => [time, price];
}
class ChartPatternModel extends Equatable {
final String type;
final List<PatternPoint> upperLine;
final List<PatternPoint> lowerLine;
class BreakoutSignalModel extends Equatable {
final String direction; // "UP", "DOWN"
final double targetPrice;
final double potentialPercent;
const ChartPatternModel({required this.type, required this.upperLine, required this.lowerLine});
const BreakoutSignalModel({
required this.direction,
required this.targetPrice,
required this.potentialPercent,
});
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(),
factory BreakoutSignalModel.fromJson(Map<String, dynamic> json) {
return BreakoutSignalModel(
direction: (json['direction'] ?? json['Direction'])?.toString() ?? 'UP',
targetPrice: (json['targetPrice'] ?? json['TargetPrice'] as num?)?.toDouble() ?? 0.0,
potentialPercent: (json['potentialPercent'] ?? json['PotentialPercent'] as num?)?.toDouble() ?? 0.0,
);
}
@override
List<Object?> get props => [type, upperLine, lowerLine];
List<Object?> get props => [direction, targetPrice, potentialPercent];
}
class ChartPatternModel extends Equatable {
final String type;
final String description;
final double confidencePercent;
final BreakoutSignalModel? breakoutSignal;
final List<PatternPoint> upperLine;
final List<PatternPoint> lowerLine;
const ChartPatternModel({
required this.type,
this.description = '',
this.confidencePercent = 0.0,
this.breakoutSignal,
required this.upperLine,
required this.lowerLine,
});
factory ChartPatternModel.fromJson(Map<String, dynamic> json) {
BreakoutSignalModel? breakout;
final bJson = json['breakoutSignal'] ?? json['BreakoutSignal'];
if (bJson != null && bJson is Map<String, dynamic>) {
breakout = BreakoutSignalModel.fromJson(bJson);
}
return ChartPatternModel(
type: json['type']?.toString() ?? json['Type']?.toString() ?? 'Pattern',
description: json['description']?.toString() ?? json['Description']?.toString() ?? '',
confidencePercent: (json['confidencePercent'] ?? json['ConfidencePercent'] as num?)?.toDouble() ?? 0.0,
breakoutSignal: breakout,
upperLine: (json['upperLine'] as List<dynamic>? ?? []).map((e) => PatternPoint.fromJson(e as Map<String, dynamic>)).toList(),
lowerLine: (json['lowerLine'] as List<dynamic>? ?? []).map((e) => PatternPoint.fromJson(e as Map<String, dynamic>)).toList(),
);
}
@override
List<Object?> get props => [type, description, confidencePercent, breakoutSignal, upperLine, lowerLine];
}
class TechnicalAnalysisModel extends Equatable {
final String symbol;
final String currency;
final String trend;
final String rsi;
final String macd;
@@ -167,6 +215,7 @@ class TechnicalAnalysisModel extends Equatable {
const TechnicalAnalysisModel({
required this.symbol,
this.currency = 'EUR',
required this.trend,
required this.rsi,
required this.macd,
@@ -211,6 +260,7 @@ class TechnicalAnalysisModel extends Equatable {
return TechnicalAnalysisModel(
symbol: json['symbol']?.toString() ?? json['isin']?.toString() ?? json['ticker']?.toString() ?? '',
currency: json['currency']?.toString() ?? 'EUR',
trend: parsedTrend,
rsi: lastInd?.rsi14?.toStringAsFixed(1) ?? 'N/A',
macd: lastInd?.macdHistogram?.toStringAsFixed(2) ?? lastInd?.macdLine?.toStringAsFixed(2) ?? 'N/A',
@@ -231,6 +281,7 @@ class TechnicalAnalysisModel extends Equatable {
Map<String, dynamic> toJson() {
return {
'symbol': symbol,
'currency': currency,
'trend': trend,
'rsi': rsi,
'macd': macd,
@@ -246,7 +297,7 @@ class TechnicalAnalysisModel extends Equatable {
@override
List<Object?> get props => [
symbol, trend, rsi, macd, overallSignal, sma50, sma200, vix,
symbol, currency, trend, rsi, macd, overallSignal, sma50, sma200, vix,
sp500Trend, dxy, stopLossAtr, candles, indicators, patterns, signals
];
}