feat(fundamentals): multi-ticker DB caching, parallel html scraper, and frontend mappings
This commit is contained in:
@@ -191,13 +191,17 @@ class FundamentalDataModel extends Equatable {
|
||||
}
|
||||
|
||||
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;
|
||||
final rawGrossProfit = parseNullableDouble(fundMap?['grossProfit'] ?? json['grossProfit']);
|
||||
double? grossMarginVal = parseNullableDouble(fundMap?['grossMargins'] ?? fundMap?['grossMargin'] ?? json['grossMargin']);
|
||||
double? grossProfVal = rawGrossProfit;
|
||||
if (rawGrossProfit != null) {
|
||||
if (rawGrossProfit <= 1.0 && rawGrossProfit >= 0.0) {
|
||||
grossMarginVal ??= rawGrossProfit;
|
||||
if (totalRev != null && totalRev > 0) {
|
||||
grossProfVal = rawGrossProfit * totalRev;
|
||||
}
|
||||
} else if (totalRev != null && totalRev > 0) {
|
||||
grossMarginVal = grossProf / totalRev;
|
||||
grossMarginVal ??= rawGrossProfit / totalRev;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +211,50 @@ class FundamentalDataModel extends Equatable {
|
||||
evToRevVal = evVal / totalRev;
|
||||
}
|
||||
|
||||
String? exDivDateStr = fundMap?['exDividendDate']?.toString() ?? json['exDividendDate']?.toString();
|
||||
String? nextEarningsDateStr = fundMap?['nextEarningsDate']?.toString() ?? json['nextEarningsDate']?.toString();
|
||||
|
||||
final rawEvents = json['events'];
|
||||
if (rawEvents is List) {
|
||||
final now = DateTime.now();
|
||||
|
||||
final divEvents = rawEvents.whereType<Map<String, dynamic>>().where((e) {
|
||||
final t = e['type']?.toString().toUpperCase() ?? '';
|
||||
return t == 'DIVIDEND' || t == 'EX_DIVIDEND';
|
||||
}).toList();
|
||||
|
||||
if (exDivDateStr == null && divEvents.isNotEmpty) {
|
||||
divEvents.sort((a, b) {
|
||||
final da = DateTime.tryParse(a['date']?.toString() ?? '') ?? DateTime(1970);
|
||||
final db = DateTime.tryParse(b['date']?.toString() ?? '') ?? DateTime(1970);
|
||||
return da.compareTo(db);
|
||||
});
|
||||
final upcoming = divEvents.firstWhere((e) {
|
||||
final d = DateTime.tryParse(e['date']?.toString() ?? '');
|
||||
return d != null && d.isAfter(now.subtract(const Duration(days: 7)));
|
||||
}, orElse: () => divEvents.last);
|
||||
exDivDateStr = upcoming['date']?.toString();
|
||||
}
|
||||
|
||||
final earningsEvents = rawEvents.whereType<Map<String, dynamic>>().where((e) {
|
||||
final t = e['type']?.toString().toUpperCase() ?? '';
|
||||
return t.contains('EARNINGS');
|
||||
}).toList();
|
||||
|
||||
if (nextEarningsDateStr == null && earningsEvents.isNotEmpty) {
|
||||
earningsEvents.sort((a, b) {
|
||||
final da = DateTime.tryParse(a['date']?.toString() ?? '') ?? DateTime(1970);
|
||||
final db = DateTime.tryParse(b['date']?.toString() ?? '') ?? DateTime(1970);
|
||||
return da.compareTo(db);
|
||||
});
|
||||
final upcoming = earningsEvents.firstWhere((e) {
|
||||
final d = DateTime.tryParse(e['date']?.toString() ?? '');
|
||||
return d != null && d.isAfter(now.subtract(const Duration(days: 1)));
|
||||
}, orElse: () => earningsEvents.last);
|
||||
nextEarningsDateStr = upcoming['date']?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return FundamentalDataModel(
|
||||
isin: isinVal,
|
||||
primaryTicker: primaryTickerVal,
|
||||
@@ -228,25 +276,25 @@ class FundamentalDataModel extends Equatable {
|
||||
fiftyTwoWeekLow: parseNullableDouble(fundMap?['fiftyTwoWeekLow'] ?? json['fiftyTwoWeekLow']),
|
||||
marketCapitalization: parseNullableDouble(fundMap?['marketCap'] ?? fundMap?['marketCapitalization'] ?? json['marketCapitalization']),
|
||||
enterpriseValue: evVal,
|
||||
peRatioTrailing: parseNullableDouble(fundMap?['trailingPE'] ?? fundMap?['peRatioTrailing'] ?? json['peRatioTrailing']),
|
||||
peRatioForward: parseNullableDouble(fundMap?['forwardPE'] ?? fundMap?['peRatioForward'] ?? json['peRatioForward']),
|
||||
peRatioTrailing: parseNullableDouble(fundMap?['trailingPe'] ?? fundMap?['trailingPE'] ?? fundMap?['peRatioTrailing'] ?? json['peRatioTrailing'] ?? json['trailingPe']),
|
||||
peRatioForward: parseNullableDouble(fundMap?['forwardPe'] ?? fundMap?['forwardPE'] ?? fundMap?['peRatioForward'] ?? json['peRatioForward'] ?? json['forwardPe']),
|
||||
pegRatio: parseNullableDouble(fundMap?['pegRatio'] ?? json['pegRatio']),
|
||||
pbRatio: parseNullableDouble(fundMap?['priceToBook'] ?? fundMap?['pbRatio'] ?? json['pbRatio']),
|
||||
psRatio: parseNullableDouble(fundMap?['priceToSalesTrailing12Months'] ?? fundMap?['psRatio'] ?? json['psRatio']),
|
||||
evToEbitda: parseNullableDouble(fundMap?['enterpriseToEbitda'] ?? fundMap?['evToEbitda'] ?? json['evToEbitda']),
|
||||
psRatio: parseNullableDouble(fundMap?['priceToSales'] ?? fundMap?['priceToSalesTrailing12Months'] ?? fundMap?['psRatio'] ?? json['psRatio']),
|
||||
evToEbitda: parseNullableDouble(fundMap?['evToEbitda'] ?? fundMap?['enterpriseToEbitda'] ?? json['evToEbitda']),
|
||||
evToRevenue: evToRevVal,
|
||||
totalRevenue: totalRev,
|
||||
revenueGrowthYoY: parseNullableDouble(fundMap?['revenueGrowth'] ?? fundMap?['revenueGrowthYoY'] ?? json['revenueGrowthYoY']),
|
||||
grossProfit: grossProf,
|
||||
revenueGrowthYoY: parseNullableDouble(fundMap?['revenueGrowthYoY'] ?? fundMap?['revenueGrowth'] ?? json['revenueGrowthYoY']),
|
||||
grossProfit: grossProfVal,
|
||||
ebitda: parseNullableDouble(fundMap?['ebitda'] ?? json['ebitda']),
|
||||
dilutedEps: parseNullableDouble(fundMap?['trailingEps'] ?? fundMap?['dilutedEps'] ?? json['dilutedEps']),
|
||||
dilutedEps: parseNullableDouble(fundMap?['dilutedEps'] ?? fundMap?['trailingEps'] ?? json['dilutedEps']),
|
||||
totalCash: parseNullableDouble(fundMap?['totalCash'] ?? json['totalCash']),
|
||||
totalDebt: parseNullableDouble(fundMap?['totalDebt'] ?? json['totalDebt']),
|
||||
operatingCashFlow: parseNullableDouble(fundMap?['operatingCashflow'] ?? fundMap?['operatingCashFlow'] ?? json['operatingCashFlow']),
|
||||
freeCashFlow: parseNullableDouble(fundMap?['freeCashflow'] ?? fundMap?['freeCashFlow'] ?? json['freeCashFlow']),
|
||||
operatingCashFlow: parseNullableDouble(fundMap?['operatingCashFlow'] ?? fundMap?['operatingCashflow'] ?? json['operatingCashFlow']),
|
||||
freeCashFlow: parseNullableDouble(fundMap?['freeCashFlow'] ?? fundMap?['freeCashflow'] ?? json['freeCashFlow']),
|
||||
grossMargin: grossMarginVal,
|
||||
operatingMargin: parseNullableDouble(fundMap?['operatingMargins'] ?? fundMap?['operatingMargin'] ?? json['operatingMargin']),
|
||||
netProfitMargin: parseNullableDouble(fundMap?['profitMargins'] ?? fundMap?['netProfitMargin'] ?? json['netProfitMargin']),
|
||||
operatingMargin: parseNullableDouble(fundMap?['operatingIncome'] ?? fundMap?['operatingMargins'] ?? fundMap?['operatingMargin'] ?? json['operatingMargin']),
|
||||
netProfitMargin: parseNullableDouble(fundMap?['netIncome'] ?? fundMap?['profitMargins'] ?? fundMap?['netProfitMargin'] ?? json['netProfitMargin']),
|
||||
returnOnEquity: parseNullableDouble(fundMap?['returnOnEquity'] ?? json['returnOnEquity']),
|
||||
returnOnAssets: parseNullableDouble(fundMap?['returnOnAssets'] ?? json['returnOnAssets']),
|
||||
returnOnInvestedCapital: parseNullableDouble(fundMap?['returnOnInvestedCapital'] ?? json['returnOnInvestedCapital']),
|
||||
@@ -254,10 +302,10 @@ class FundamentalDataModel extends Equatable {
|
||||
currentRatio: parseNullableDouble(fundMap?['currentRatio'] ?? json['currentRatio']),
|
||||
quickRatio: parseNullableDouble(fundMap?['quickRatio'] ?? json['quickRatio']),
|
||||
interestCoverage: parseNullableDouble(fundMap?['interestCoverage'] ?? json['interestCoverage']),
|
||||
dividendYield: parseNullableDouble(fundMap?['dividendYield'] ?? json['dividendYield']),
|
||||
dividendYield: parseNullableDouble(fundMap?['forwardDividendYield'] ?? fundMap?['dividendYield'] ?? json['dividendYield']),
|
||||
payoutRatio: parseNullableDouble(fundMap?['payoutRatio'] ?? json['payoutRatio']),
|
||||
exDividendDate: fundMap?['exDividendDate']?.toString() ?? json['exDividendDate']?.toString(),
|
||||
nextEarningsDate: fundMap?['nextEarningsDate']?.toString() ?? json['nextEarningsDate']?.toString(),
|
||||
exDividendDate: exDivDateStr,
|
||||
nextEarningsDate: nextEarningsDateStr,
|
||||
percentHeldByInstitutions: parseNullableDouble(fundMap?['percentHeldByInstitutions'] ?? json['percentHeldByInstitutions']),
|
||||
percentHeldByInsiders: parseNullableDouble(fundMap?['percentHeldByInsiders'] ?? json['percentHeldByInsiders']),
|
||||
shortRatio: parseNullableDouble(fundMap?['shortRatio'] ?? json['shortRatio']),
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ class FundamentalCategoryPanels extends StatelessWidget {
|
||||
|
||||
String _fmtPercent(double? val) {
|
||||
if (val == null) return 'N/A';
|
||||
final p = (val.abs() <= 1.0 && val != 0.0) ? val * 100.0 : val;
|
||||
final p = (val.abs() <= 5.0 && val != 0.0) ? val * 100.0 : val;
|
||||
return '${p.toStringAsFixed(2)}%';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user