feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'evaluation_history_enums.dart';
|
||||
|
||||
/// Typed mirror of `FinlyticCore.Dtos.Trading.EvaluationHistoryEntryDto` — one row
|
||||
/// of `GET /api/v1/admin/evaluations`. Every score field is the real,
|
||||
/// already-computed value the server persisted (including the honest 0/default
|
||||
/// values recorded for [OutcomeReason.noTechnicalSetups]) — nothing here is
|
||||
/// fabricated client-side (Rules.md §4).
|
||||
class EvaluationHistoryEntryModel extends Equatable {
|
||||
final String id;
|
||||
final String isin;
|
||||
final String symbol;
|
||||
final double technicalScore;
|
||||
final double sentimentScore;
|
||||
final double fundamentalScore;
|
||||
final double compositeOpportunityScore;
|
||||
final double reliabilityBonus;
|
||||
final bool passedEarningsLockout;
|
||||
final int? daysToNextEarnings;
|
||||
final bool passedDividendGate;
|
||||
final int? daysToNextExDividend;
|
||||
final UniverseSource? universeSource;
|
||||
final DateTime? universeEnteredAtUtc;
|
||||
final bool passedSimulationVeto;
|
||||
final bool passedAiValidation;
|
||||
final String aiThesisSummary;
|
||||
final OutcomeReason outcomeReason;
|
||||
final TriggerSource triggerSource;
|
||||
final String? triggeredByUserId;
|
||||
final String? proposalId;
|
||||
final DateTime evaluatedAtUtc;
|
||||
|
||||
const EvaluationHistoryEntryModel({
|
||||
required this.id,
|
||||
required this.isin,
|
||||
required this.symbol,
|
||||
required this.technicalScore,
|
||||
required this.sentimentScore,
|
||||
required this.fundamentalScore,
|
||||
required this.compositeOpportunityScore,
|
||||
required this.reliabilityBonus,
|
||||
required this.passedEarningsLockout,
|
||||
this.daysToNextEarnings,
|
||||
required this.passedDividendGate,
|
||||
this.daysToNextExDividend,
|
||||
this.universeSource,
|
||||
this.universeEnteredAtUtc,
|
||||
required this.passedSimulationVeto,
|
||||
required this.passedAiValidation,
|
||||
required this.aiThesisSummary,
|
||||
required this.outcomeReason,
|
||||
required this.triggerSource,
|
||||
this.triggeredByUserId,
|
||||
this.proposalId,
|
||||
required this.evaluatedAtUtc,
|
||||
});
|
||||
|
||||
/// True exactly when this evaluation resulted in a trade proposal.
|
||||
bool get hasProposal => proposalId != null && proposalId!.isNotEmpty;
|
||||
|
||||
factory EvaluationHistoryEntryModel.fromJson(Map<String, dynamic> json) {
|
||||
double parseDbl(dynamic val) {
|
||||
if (val == null) return 0.0;
|
||||
if (val is num) return val.toDouble();
|
||||
return double.tryParse(val.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
DateTime parseDate(dynamic val) {
|
||||
if (val == null) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
return DateTime.tryParse(val.toString())?.toUtc() ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
}
|
||||
|
||||
return EvaluationHistoryEntryModel(
|
||||
id: json['id']?.toString() ?? '',
|
||||
isin: json['isin']?.toString() ?? '',
|
||||
symbol: json['symbol']?.toString() ?? '',
|
||||
technicalScore: parseDbl(json['technicalScore']),
|
||||
sentimentScore: parseDbl(json['sentimentScore']),
|
||||
fundamentalScore: parseDbl(json['fundamentalScore']),
|
||||
compositeOpportunityScore: parseDbl(json['compositeOpportunityScore']),
|
||||
reliabilityBonus: parseDbl(json['reliabilityBonus']),
|
||||
passedEarningsLockout: json['passedEarningsLockout'] == true,
|
||||
daysToNextEarnings: json['daysToNextEarnings'] is num ? (json['daysToNextEarnings'] as num).toInt() : null,
|
||||
passedDividendGate: json['passedDividendGate'] == true,
|
||||
daysToNextExDividend: json['daysToNextExDividend'] is num ? (json['daysToNextExDividend'] as num).toInt() : null,
|
||||
universeSource: UniverseSource.fromJson(json['universeSource']?.toString()),
|
||||
universeEnteredAtUtc: json['universeEnteredAtUtc'] == null ? null : parseDate(json['universeEnteredAtUtc']),
|
||||
passedSimulationVeto: json['passedSimulationVeto'] == true,
|
||||
passedAiValidation: json['passedAiValidation'] == true,
|
||||
aiThesisSummary: json['aiThesisSummary']?.toString() ?? '',
|
||||
outcomeReason: OutcomeReason.fromJson(json['outcomeReason']?.toString()),
|
||||
triggerSource: TriggerSource.fromJson(json['triggerSource']?.toString()),
|
||||
triggeredByUserId: json['triggeredByUserId']?.toString(),
|
||||
proposalId: json['proposalId']?.toString(),
|
||||
evaluatedAtUtc: parseDate(json['evaluatedAtUtc']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
isin,
|
||||
symbol,
|
||||
technicalScore,
|
||||
sentimentScore,
|
||||
fundamentalScore,
|
||||
compositeOpportunityScore,
|
||||
reliabilityBonus,
|
||||
passedEarningsLockout,
|
||||
daysToNextEarnings,
|
||||
passedDividendGate,
|
||||
daysToNextExDividend,
|
||||
universeSource,
|
||||
universeEnteredAtUtc,
|
||||
passedSimulationVeto,
|
||||
passedAiValidation,
|
||||
aiThesisSummary,
|
||||
outcomeReason,
|
||||
triggerSource,
|
||||
triggeredByUserId,
|
||||
proposalId,
|
||||
evaluatedAtUtc,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
|
||||
/// Mirrors `FinlyticCore.Dtos.Trading.OutcomeReason` (`TradeEnums.cs`), which the
|
||||
/// `/api/v1/admin/evaluations` endpoint serializes as a `JsonStringEnumConverter`
|
||||
/// string using the exact C# member name (e.g. `"Approved"`, `"BelowScoreThreshold"`).
|
||||
///
|
||||
/// [unknown] is the fallback both for the server's own `Unknown = 0` default (an
|
||||
/// honest "we don't know" rather than a fabricated reason, Rules.md §4) and for any
|
||||
/// future server-side member this client doesn't recognize yet.
|
||||
enum OutcomeReason {
|
||||
unknown,
|
||||
approved,
|
||||
belowScoreThreshold,
|
||||
earningsLockout,
|
||||
simulationVeto,
|
||||
aiRejected,
|
||||
noTechnicalSetups,
|
||||
duplicateActiveProposal,
|
||||
dividendGate;
|
||||
|
||||
static OutcomeReason fromJson(String? raw) {
|
||||
switch (raw) {
|
||||
case 'Approved':
|
||||
return OutcomeReason.approved;
|
||||
case 'BelowScoreThreshold':
|
||||
return OutcomeReason.belowScoreThreshold;
|
||||
case 'EarningsLockout':
|
||||
return OutcomeReason.earningsLockout;
|
||||
case 'SimulationVeto':
|
||||
return OutcomeReason.simulationVeto;
|
||||
case 'AiRejected':
|
||||
return OutcomeReason.aiRejected;
|
||||
case 'NoTechnicalSetups':
|
||||
return OutcomeReason.noTechnicalSetups;
|
||||
case 'DuplicateActiveProposal':
|
||||
return OutcomeReason.duplicateActiveProposal;
|
||||
case 'DividendGate':
|
||||
return OutcomeReason.dividendGate;
|
||||
case 'Unknown':
|
||||
default:
|
||||
return OutcomeReason.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact server-side enum member name. `[FromQuery] OutcomeReason?` on
|
||||
/// `AdminEvaluationHistoryController` model-binds a bare enum member name from
|
||||
/// the query string (ASP.NET Core's default `Enum.TryParse`-based binder), not a
|
||||
/// JSON string — so this is what must be sent back as the `outcome` filter value.
|
||||
String toApiValue() {
|
||||
switch (this) {
|
||||
case OutcomeReason.approved:
|
||||
return 'Approved';
|
||||
case OutcomeReason.belowScoreThreshold:
|
||||
return 'BelowScoreThreshold';
|
||||
case OutcomeReason.earningsLockout:
|
||||
return 'EarningsLockout';
|
||||
case OutcomeReason.simulationVeto:
|
||||
return 'SimulationVeto';
|
||||
case OutcomeReason.aiRejected:
|
||||
return 'AiRejected';
|
||||
case OutcomeReason.noTechnicalSetups:
|
||||
return 'NoTechnicalSetups';
|
||||
case OutcomeReason.duplicateActiveProposal:
|
||||
return 'DuplicateActiveProposal';
|
||||
case OutcomeReason.dividendGate:
|
||||
return 'DividendGate';
|
||||
case OutcomeReason.unknown:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case OutcomeReason.approved:
|
||||
return 'Freigegeben';
|
||||
case OutcomeReason.belowScoreThreshold:
|
||||
return 'Score zu niedrig';
|
||||
case OutcomeReason.earningsLockout:
|
||||
return 'Earnings-Sperre';
|
||||
case OutcomeReason.simulationVeto:
|
||||
return 'Simulation-Veto';
|
||||
case OutcomeReason.aiRejected:
|
||||
return 'KI abgelehnt';
|
||||
case OutcomeReason.noTechnicalSetups:
|
||||
return 'Kein Setup';
|
||||
case OutcomeReason.duplicateActiveProposal:
|
||||
return 'Bereits aktiver Vorschlag';
|
||||
case OutcomeReason.dividendGate:
|
||||
return 'Dividend-Sperre';
|
||||
case OutcomeReason.unknown:
|
||||
return 'Unbekannt';
|
||||
}
|
||||
}
|
||||
|
||||
/// Color-coding for the history-list badge, reusing only colors already
|
||||
/// established elsewhere in the app (`AppTheme.primaryEmerald`/`accentRed` plus
|
||||
/// the `Colors.amber`/`Colors.purpleAccent` already used by
|
||||
/// `EvaluationScoreBreakdownSheet`) rather than introducing a new palette.
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case OutcomeReason.approved:
|
||||
return AppTheme.primaryEmerald;
|
||||
case OutcomeReason.aiRejected:
|
||||
case OutcomeReason.simulationVeto:
|
||||
return AppTheme.accentRed;
|
||||
case OutcomeReason.belowScoreThreshold:
|
||||
case OutcomeReason.earningsLockout:
|
||||
case OutcomeReason.duplicateActiveProposal:
|
||||
case OutcomeReason.dividendGate:
|
||||
return Colors.amber;
|
||||
case OutcomeReason.noTechnicalSetups:
|
||||
case OutcomeReason.unknown:
|
||||
return AppTheme.textMuted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors `FinlyticCore.Dtos.Trading.TriggerSource`.
|
||||
enum TriggerSource {
|
||||
unknown,
|
||||
automatic,
|
||||
manual;
|
||||
|
||||
static TriggerSource fromJson(String? raw) {
|
||||
switch (raw) {
|
||||
case 'Automatic':
|
||||
return TriggerSource.automatic;
|
||||
case 'Manual':
|
||||
return TriggerSource.manual;
|
||||
case 'Unknown':
|
||||
default:
|
||||
return TriggerSource.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
String toApiValue() {
|
||||
switch (this) {
|
||||
case TriggerSource.automatic:
|
||||
return 'Automatic';
|
||||
case TriggerSource.manual:
|
||||
return 'Manual';
|
||||
case TriggerSource.unknown:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case TriggerSource.automatic:
|
||||
return 'Automatisch';
|
||||
case TriggerSource.manual:
|
||||
return 'Manuell';
|
||||
case TriggerSource.unknown:
|
||||
return 'Unbekannt';
|
||||
}
|
||||
}
|
||||
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case TriggerSource.automatic:
|
||||
return AppTheme.accentCyan;
|
||||
case TriggerSource.manual:
|
||||
return Colors.purpleAccent;
|
||||
case TriggerSource.unknown:
|
||||
return AppTheme.textMuted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors `FinlyticCore.Dtos.TechnicalAnalysis.UniverseSource` - which recurring
|
||||
/// FinlyticTechnicals selection mechanism added the ISIN to the continuously
|
||||
/// scanned universe before this evaluation ran. `null` on the Dart side (not
|
||||
/// modeled as its own enum value here) means the evaluation happened outside
|
||||
/// that universe entirely (e.g. a manual "Analyze now" call).
|
||||
enum UniverseSource {
|
||||
sentimentSpike,
|
||||
userFavorite,
|
||||
discovery;
|
||||
|
||||
static UniverseSource? fromJson(String? raw) {
|
||||
switch (raw) {
|
||||
case 'SentimentSpike':
|
||||
return UniverseSource.sentimentSpike;
|
||||
case 'UserFavorite':
|
||||
return UniverseSource.userFavorite;
|
||||
case 'Discovery':
|
||||
return UniverseSource.discovery;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case UniverseSource.sentimentSpike:
|
||||
return 'Sentiment-Spike';
|
||||
case UniverseSource.userFavorite:
|
||||
return 'Nutzer-Favorit';
|
||||
case UniverseSource.discovery:
|
||||
return 'Discovery-Liste';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'evaluation_history_entry_model.dart';
|
||||
import 'evaluation_history_summary_model.dart';
|
||||
|
||||
/// Typed mirror of `FinlyticCore.Dtos.Trading.GetEvaluationHistoryResponse` — the
|
||||
/// full response body of `GET /api/v1/admin/evaluations`.
|
||||
class EvaluationHistoryResponseModel extends Equatable {
|
||||
final int totalCount;
|
||||
final List<EvaluationHistoryEntryModel> entries;
|
||||
final EvaluationHistorySummaryModel summary;
|
||||
|
||||
const EvaluationHistoryResponseModel({
|
||||
required this.totalCount,
|
||||
required this.entries,
|
||||
required this.summary,
|
||||
});
|
||||
|
||||
factory EvaluationHistoryResponseModel.empty() => EvaluationHistoryResponseModel(
|
||||
totalCount: 0,
|
||||
entries: const [],
|
||||
summary: EvaluationHistorySummaryModel.empty(),
|
||||
);
|
||||
|
||||
factory EvaluationHistoryResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
final rawEntries = json['entries'];
|
||||
final entries = rawEntries is List
|
||||
? rawEntries.whereType<Map<String, dynamic>>().map(EvaluationHistoryEntryModel.fromJson).toList()
|
||||
: <EvaluationHistoryEntryModel>[];
|
||||
|
||||
final rawSummary = json['summary'];
|
||||
final summary = rawSummary is Map<String, dynamic>
|
||||
? EvaluationHistorySummaryModel.fromJson(rawSummary)
|
||||
: EvaluationHistorySummaryModel.empty();
|
||||
|
||||
return EvaluationHistoryResponseModel(
|
||||
totalCount: json['totalCount'] is num ? (json['totalCount'] as num).toInt() : 0,
|
||||
entries: entries,
|
||||
summary: summary,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [totalCount, entries, summary];
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'evaluation_history_enums.dart';
|
||||
|
||||
/// Typed mirror of `FinlyticCore.Dtos.Trading.OutcomeReasonCountDto`.
|
||||
class OutcomeReasonCountModel extends Equatable {
|
||||
final OutcomeReason outcomeReason;
|
||||
final int count;
|
||||
|
||||
const OutcomeReasonCountModel({required this.outcomeReason, required this.count});
|
||||
|
||||
factory OutcomeReasonCountModel.fromJson(Map<String, dynamic> json) {
|
||||
return OutcomeReasonCountModel(
|
||||
outcomeReason: OutcomeReason.fromJson(json['outcomeReason']?.toString()),
|
||||
count: json['count'] is num ? (json['count'] as num).toInt() : 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [outcomeReason, count];
|
||||
}
|
||||
|
||||
/// Typed mirror of `FinlyticCore.Dtos.Trading.EvaluationHistorySummaryDto` — the
|
||||
/// pre-aggregated headline numbers for the admin evaluation-history tab. Every
|
||||
/// field except [lastProposalCreatedAtUtc] is scoped to the same filters as the
|
||||
/// paginated entry list it accompanies; [lastProposalCreatedAtUtc] deliberately
|
||||
/// ignores the from/to filters (see the server-side DTO doc comment) so the admin
|
||||
/// always sees "how long since the last real proposal" regardless of which
|
||||
/// historical window is currently selected.
|
||||
class EvaluationHistorySummaryModel extends Equatable {
|
||||
final int totalEvaluations;
|
||||
final List<OutcomeReasonCountModel> countsByOutcome;
|
||||
final double averageCompositeScore;
|
||||
final int proposalsCreated;
|
||||
final DateTime? lastProposalCreatedAtUtc;
|
||||
|
||||
const EvaluationHistorySummaryModel({
|
||||
required this.totalEvaluations,
|
||||
required this.countsByOutcome,
|
||||
required this.averageCompositeScore,
|
||||
required this.proposalsCreated,
|
||||
this.lastProposalCreatedAtUtc,
|
||||
});
|
||||
|
||||
int countFor(OutcomeReason reason) {
|
||||
for (final c in countsByOutcome) {
|
||||
if (c.outcomeReason == reason) return c.count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
factory EvaluationHistorySummaryModel.empty() => const EvaluationHistorySummaryModel(
|
||||
totalEvaluations: 0,
|
||||
countsByOutcome: [],
|
||||
averageCompositeScore: 0,
|
||||
proposalsCreated: 0,
|
||||
lastProposalCreatedAtUtc: null,
|
||||
);
|
||||
|
||||
factory EvaluationHistorySummaryModel.fromJson(Map<String, dynamic> json) {
|
||||
double parseDbl(dynamic val) {
|
||||
if (val == null) return 0.0;
|
||||
if (val is num) return val.toDouble();
|
||||
return double.tryParse(val.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
final rawCounts = json['countsByOutcome'];
|
||||
final counts = rawCounts is List
|
||||
? rawCounts.whereType<Map<String, dynamic>>().map(OutcomeReasonCountModel.fromJson).toList()
|
||||
: <OutcomeReasonCountModel>[];
|
||||
|
||||
final rawLast = json['lastProposalCreatedAtUtc'];
|
||||
|
||||
return EvaluationHistorySummaryModel(
|
||||
totalEvaluations: json['totalEvaluations'] is num ? (json['totalEvaluations'] as num).toInt() : 0,
|
||||
countsByOutcome: counts,
|
||||
averageCompositeScore: parseDbl(json['averageCompositeScore']),
|
||||
proposalsCreated: json['proposalsCreated'] is num ? (json['proposalsCreated'] as num).toInt() : 0,
|
||||
lastProposalCreatedAtUtc: rawLast != null ? DateTime.tryParse(rawLast.toString())?.toUtc() : null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [totalEvaluations, countsByOutcome, averageCompositeScore, proposalsCreated, lastProposalCreatedAtUtc];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Typed mirror of the fields the admin UI needs from
|
||||
/// `FinlyticCore.Dtos.TechnicalAnalysis.StrategyResultDto`, as returned by
|
||||
/// `GET /api/v1/admin/evaluations/watchlist/{isin}/history` — the last N
|
||||
/// technical-analysis setups computed for one ISIN, most recent first, so the
|
||||
/// score trend (improving/worsening, and whether it ever cleared the engine's
|
||||
/// top-pick bar) is visible even for setups too weak to ever reach the engine.
|
||||
class RecentSetupModel extends Equatable {
|
||||
final String strategyName;
|
||||
final double qualityScore;
|
||||
final bool isTopPick;
|
||||
final String rating;
|
||||
final DateTime createdAt;
|
||||
|
||||
const RecentSetupModel({
|
||||
required this.strategyName,
|
||||
required this.qualityScore,
|
||||
required this.isTopPick,
|
||||
required this.rating,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory RecentSetupModel.fromJson(Map<String, dynamic> json) {
|
||||
double parseDbl(dynamic val) {
|
||||
if (val == null) return 0.0;
|
||||
if (val is num) return val.toDouble();
|
||||
return double.tryParse(val.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
return RecentSetupModel(
|
||||
strategyName: json['strategyName']?.toString() ?? '',
|
||||
qualityScore: parseDbl(json['qualityScore']),
|
||||
isTopPick: json['isTopPick'] == true,
|
||||
rating: json['rating']?.toString() ?? '',
|
||||
createdAt: DateTime.tryParse(json['createdAt']?.toString() ?? '')?.toUtc() ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [strategyName, qualityScore, isTopPick, rating, createdAt];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'evaluation_history_enums.dart';
|
||||
|
||||
/// Typed mirror of `FinlyticCore.Dtos.TechnicalAnalysis.WatchlistEntryDto` — one
|
||||
/// row of `GET /api/v1/admin/evaluations/watchlist`: an asset FinlyticTechnicals'
|
||||
/// background scanner is actually evaluating every cycle, independent of
|
||||
/// whether it has produced any evaluation the engine ever saw.
|
||||
class WatchlistEntryModel extends Equatable {
|
||||
final String isin;
|
||||
final String? symbol;
|
||||
final UniverseSource? source;
|
||||
final int priority;
|
||||
final DateTime addedAtUtc;
|
||||
final DateTime? expiresAtUtc;
|
||||
|
||||
const WatchlistEntryModel({
|
||||
required this.isin,
|
||||
this.symbol,
|
||||
this.source,
|
||||
required this.priority,
|
||||
required this.addedAtUtc,
|
||||
this.expiresAtUtc,
|
||||
});
|
||||
|
||||
factory WatchlistEntryModel.fromJson(Map<String, dynamic> json) {
|
||||
DateTime parseDate(dynamic val) {
|
||||
return DateTime.tryParse(val?.toString() ?? '')?.toUtc() ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
}
|
||||
|
||||
return WatchlistEntryModel(
|
||||
isin: json['isin']?.toString() ?? '',
|
||||
symbol: json['symbol']?.toString(),
|
||||
source: UniverseSource.fromJson(json['source']?.toString()),
|
||||
priority: json['priority'] is num ? (json['priority'] as num).toInt() : 0,
|
||||
addedAtUtc: parseDate(json['addedAtUtc']),
|
||||
expiresAtUtc: json['expiresAtUtc'] == null ? null : parseDate(json['expiresAtUtc']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [isin, symbol, source, priority, addedAtUtc, expiresAtUtc];
|
||||
}
|
||||
Reference in New Issue
Block a user