feat(trades): add live execution cockpit, closing cockpit, calculation cards and precision trade settings

This commit is contained in:
2026-08-15 19:30:25 +02:00
parent 882d24a316
commit 34fa774cbf
31 changed files with 4235 additions and 630 deletions
@@ -1,12 +1,24 @@
/// Typed DTO for requesting a trade exit/close.
class CloseTradeRequestDto {
final double userExitPrice;
final DateTime? userExitTimestamp;
final double exitFee;
final String closeReason;
const CloseTradeRequestDto({required this.userExitPrice});
const CloseTradeRequestDto({
required this.userExitPrice,
this.userExitTimestamp,
this.exitFee = 1.0,
this.closeReason = 'ManualClosure',
});
Map<String, dynamic> toJson() {
return {
'userExitPrice': userExitPrice,
if (userExitTimestamp != null) 'userExitTimestamp': userExitTimestamp!.toUtc().toIso8601String(),
'exitFee': exitFee,
'closeReason': closeReason,
};
}
}
@@ -1,5 +1,59 @@
import 'package:equatable/equatable.dart';
enum DriftStatus {
onTrack,
trailingActive,
driftWarning,
exitAlert,
}
class TradeHourlyUpdateModel extends Equatable {
final String recommendation;
final double currentPrice;
final double? suggestedStopLoss;
final double? suggestedTakeProfit;
final double vixValue;
final String reasoning;
final DateTime timestamp;
const TradeHourlyUpdateModel({
required this.recommendation,
required this.currentPrice,
this.suggestedStopLoss,
this.suggestedTakeProfit,
this.vixValue = 0.0,
required this.reasoning,
required this.timestamp,
});
factory TradeHourlyUpdateModel.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 ts = DateTime.now();
final tsStr = (json['timestamp'] ?? json['Timestamp'])?.toString();
if (tsStr != null && tsStr.isNotEmpty) {
ts = DateTime.tryParse(tsStr) ?? DateTime.now();
}
return TradeHourlyUpdateModel(
recommendation: (json['recommendation'] ?? json['Recommendation'])?.toString() ?? 'Hold',
currentPrice: parseDbl(json['currentPrice'] ?? json['CurrentPrice']),
suggestedStopLoss: json['suggestedStopLoss'] != null ? parseDbl(json['suggestedStopLoss'] ?? json['SuggestedStopLoss']) : null,
suggestedTakeProfit: json['suggestedTakeProfit'] != null ? parseDbl(json['suggestedTakeProfit'] ?? json['SuggestedTakeProfit']) : null,
vixValue: parseDbl(json['vixValue'] ?? json['VixValue']),
reasoning: (json['reasoning'] ?? json['Reasoning'])?.toString() ?? '',
timestamp: ts,
);
}
@override
List<Object?> get props => [recommendation, currentPrice, suggestedStopLoss, suggestedTakeProfit, reasoning, timestamp];
}
class TradeModel extends Equatable {
final String id;
final String analysisId;
@@ -27,6 +81,9 @@ class TradeModel extends Equatable {
final double winRate;
final String timeframe;
final String instrumentType;
final String assetType;
final bool hasCfd;
final List<String> derivativeProductCategories;
final String derivativeIsin;
final DateTime? createdAt;
@@ -41,6 +98,12 @@ class TradeModel extends Equatable {
final double exitFee;
final double quantity;
final String closeReason;
final DateTime? userExitTimestamp;
final bool hasPendingExitAlert;
final String pendingExitReason;
final List<TradeHourlyUpdateModel> hourlyUpdates;
const TradeModel({
required this.id,
this.analysisId = '',
@@ -68,6 +131,9 @@ class TradeModel extends Equatable {
this.winRate = 50.0,
this.timeframe = '1D',
this.instrumentType = 'Stock',
this.assetType = 'stock',
this.hasCfd = false,
this.derivativeProductCategories = const [],
this.derivativeIsin = '',
this.createdAt,
this.riskTolerance = 'Moderate',
@@ -80,6 +146,11 @@ class TradeModel extends Equatable {
this.entryFee = 0.0,
this.exitFee = 0.0,
this.quantity = 0.0,
this.closeReason = '',
this.userExitTimestamp,
this.hasPendingExitAlert = false,
this.pendingExitReason = '',
this.hourlyUpdates = const [],
});
bool get isActive => status.toLowerCase() == 'active';
@@ -87,6 +158,15 @@ class TradeModel extends Equatable {
bool get isRejected => status.toLowerCase() == 'rejected';
bool get isProposed => (status.toLowerCase() == 'proposed' || isGlobalProposal) && !isRejected && !isActive && !isClosed;
DriftStatus get driftStatus {
if (hasPendingExitAlert) return DriftStatus.exitAlert;
if (hourlyUpdates.any((u) => u.recommendation.toLowerCase().contains('adjustsl') || u.recommendation.toLowerCase().contains('trailing'))) {
return DriftStatus.trailingActive;
}
if (calculatedPnlPct < -3.5) return DriftStatus.driftWarning;
return DriftStatus.onTrack;
}
double get effectiveCurrentPrice {
if (currentPrice > 0) return currentPrice;
if (actualEntryPrice > 0) return actualEntryPrice;
@@ -162,6 +242,18 @@ class TradeModel extends Equatable {
dt = DateTime.tryParse(createdStr);
}
DateTime? exitDt;
final exitStr = (json['userExitTimestamp'] ?? json['UserExitTimestamp'])?.toString();
if (exitStr != null && exitStr.isNotEmpty) {
exitDt = DateTime.tryParse(exitStr);
}
List<TradeHourlyUpdateModel> updates = [];
final rawUpdates = json['hourlyUpdates'] ?? json['HourlyUpdates'];
if (rawUpdates is List) {
updates = rawUpdates.map((u) => TradeHourlyUpdateModel.fromJson(Map<String, dynamic>.from(u))).toList();
}
return TradeModel(
id: idVal,
analysisId: (json['analysisId'] ?? json['AnalysisId'])?.toString() ?? '',
@@ -189,6 +281,11 @@ class TradeModel extends Equatable {
winRate: parseDbl(json['winRate'] ?? json['WinRate']),
timeframe: (json['timeframe'] ?? json['Timeframe'])?.toString() ?? '1D',
instrumentType: (json['instrumentType'] ?? json['InstrumentType'])?.toString() ?? 'Stock',
assetType: (json['assetType'] ?? json['AssetType'])?.toString() ?? 'stock',
hasCfd: json['hasCfd'] == true || json['HasCfd'] == true,
derivativeProductCategories: (json['derivativeProductCategories'] ?? json['DerivativeProductCategories']) is List
? ((json['derivativeProductCategories'] ?? json['DerivativeProductCategories']) as List).map((e) => e.toString()).toList()
: const [],
derivativeIsin: (json['derivativeIsin'] ?? json['DerivativeIsin'] ?? json['knockoutIsin'] ?? json['KnockoutIsin'])?.toString() ?? '',
createdAt: dt,
riskTolerance: (json['riskTolerance'] ?? json['RiskTolerance'])?.toString() ?? 'Moderate',
@@ -203,6 +300,11 @@ class TradeModel extends Equatable {
entryFee: parseDbl(json['entryFee'] ?? json['EntryFee']),
exitFee: parseDbl(json['exitFee'] ?? json['ExitFee']),
quantity: parseDbl(json['quantity'] ?? json['Quantity']),
closeReason: (json['closeReason'] ?? json['CloseReason'])?.toString() ?? '',
userExitTimestamp: exitDt,
hasPendingExitAlert: json['hasPendingExitAlert'] == true || json['HasPendingExitAlert'] == true,
pendingExitReason: (json['pendingExitReason'] ?? json['PendingExitReason'])?.toString() ?? '',
hourlyUpdates: updates,
);
}
@@ -234,6 +336,9 @@ class TradeModel extends Equatable {
'winRate': winRate,
'timeframe': timeframe,
'instrumentType': instrumentType,
'assetType': assetType,
'hasCfd': hasCfd,
'derivativeProductCategories': derivativeProductCategories,
'derivativeIsin': derivativeIsin,
'createdAt': createdAt?.toIso8601String(),
'riskTolerance': riskTolerance,
@@ -246,6 +351,10 @@ class TradeModel extends Equatable {
'entryFee': entryFee,
'exitFee': exitFee,
'quantity': quantity,
'closeReason': closeReason,
'userExitTimestamp': userExitTimestamp?.toIso8601String(),
'hasPendingExitAlert': hasPendingExitAlert,
'pendingExitReason': pendingExitReason,
};
}
@@ -263,5 +372,8 @@ class TradeModel extends Equatable {
currentPrice,
pnlAbsolute,
pnlPercent,
hasPendingExitAlert,
hourlyUpdates,
];
}