Files

221 lines
7.9 KiB
Dart

import 'package:equatable/equatable.dart';
enum BotExecutionVenue {
alpacaPaperTrading,
syntheticPaperBroker,
}
enum BotPositionStatus {
pending,
active,
breakEvenTriggered,
tp1Hit,
tp2Hit,
closed,
stoppedOut,
knockedOut,
canceled,
}
class BotTradeOrderModel extends Equatable {
final String orderId;
final String proposalId;
final String isin;
final String symbol;
final String venue;
final String direction;
final double requestedQuantity;
final double filledQuantity;
final double entryPrice;
final double averageBuyIn;
final double initialStopLoss;
final double currentStopLoss;
final double takeProfit1;
final double takeProfit2;
final double currentPrice;
final double unrealizedPnlEur;
final double realizedPnlEur;
final String status;
final DateTime createdAt;
final DateTime? filledAt;
final DateTime? closedAt;
const BotTradeOrderModel({
required this.orderId,
required this.proposalId,
required this.isin,
required this.symbol,
required this.venue,
required this.direction,
required this.requestedQuantity,
required this.filledQuantity,
required this.entryPrice,
required this.averageBuyIn,
required this.initialStopLoss,
required this.currentStopLoss,
required this.takeProfit1,
required this.takeProfit2,
required this.currentPrice,
required this.unrealizedPnlEur,
required this.realizedPnlEur,
required this.status,
required this.createdAt,
this.filledAt,
this.closedAt,
});
bool get isLong => direction.toUpperCase() == 'BUY' || direction.toUpperCase() == 'LONG';
bool get isActive => status.toLowerCase() == 'active' || status.toLowerCase() == 'breakeventriggered' || status.toLowerCase() == 'tp1hit';
bool get isBreakEven => status.toLowerCase() == 'breakeventriggered';
bool get isTp1Hit => status.toLowerCase() == 'tp1hit';
bool get isClosed => status.toLowerCase() == 'closed' || status.toLowerCase() == 'stoppedout' || status.toLowerCase() == 'knockedout';
double get pnlPercent {
if (averageBuyIn <= 0) return 0.0;
return isLong
? ((currentPrice - averageBuyIn) / averageBuyIn) * 100.0
: ((averageBuyIn - currentPrice) / averageBuyIn) * 100.0;
}
double get rMultiple {
final risk = (entryPrice - initialStopLoss).abs();
if (risk <= 0) return 0.0;
final reward = isLong ? (currentPrice - entryPrice) : (entryPrice - currentPrice);
return reward / risk;
}
factory BotTradeOrderModel.fromJson(Map<String, dynamic> json) {
return BotTradeOrderModel(
orderId: json['orderId']?.toString() ?? '',
proposalId: json['proposalId']?.toString() ?? '',
isin: json['isin']?.toString() ?? '',
symbol: json['symbol']?.toString() ?? '',
venue: json['venue']?.toString() ?? 'SyntheticPaperBroker',
direction: json['direction']?.toString() ?? 'BUY',
requestedQuantity: (json['requestedQuantity'] as num?)?.toDouble() ?? 0.0,
filledQuantity: (json['filledQuantity'] as num?)?.toDouble() ?? 0.0,
entryPrice: (json['entryPrice'] as num?)?.toDouble() ?? 0.0,
averageBuyIn: (json['averageBuyIn'] as num?)?.toDouble() ?? 0.0,
initialStopLoss: (json['initialStopLoss'] as num?)?.toDouble() ?? 0.0,
currentStopLoss: (json['currentStopLoss'] as num?)?.toDouble() ?? 0.0,
takeProfit1: (json['takeProfit1'] as num?)?.toDouble() ?? 0.0,
takeProfit2: (json['takeProfit2'] as num?)?.toDouble() ?? 0.0,
currentPrice: (json['currentPrice'] as num?)?.toDouble() ?? 0.0,
unrealizedPnlEur: (json['unrealizedPnlEur'] as num?)?.toDouble() ?? 0.0,
realizedPnlEur: (json['realizedPnlEur'] as num?)?.toDouble() ?? 0.0,
status: json['status']?.toString() ?? 'Active',
createdAt: json['createdAtUtc'] != null ? DateTime.tryParse(json['createdAtUtc'].toString()) ?? DateTime.now() : DateTime.now(),
filledAt: json['filledAtUtc'] != null ? DateTime.tryParse(json['filledAtUtc'].toString()) : null,
closedAt: json['closedAtUtc'] != null ? DateTime.tryParse(json['closedAtUtc'].toString()) : null,
);
}
@override
List<Object?> get props => [
orderId, proposalId, isin, symbol, venue, direction,
requestedQuantity, filledQuantity, entryPrice, averageBuyIn,
currentPrice, unrealizedPnlEur, realizedPnlEur, status, currentStopLoss
];
}
class AccountSummaryModel extends Equatable {
/// Nullable: a `null` value means the server did not report this field
/// (e.g. broker/account service unavailable). The UI MUST show an explicit
/// "not available" state in that case rather than a fabricated number
/// (Rules.md §4).
final double? equity;
final double? cash;
final double? buyingPower;
final String currency;
final String status;
const AccountSummaryModel({
required this.equity,
required this.cash,
required this.buyingPower,
required this.currency,
required this.status,
});
bool get hasAccountData => equity != null && buyingPower != null;
factory AccountSummaryModel.fromJson(Map<String, dynamic> json) {
return AccountSummaryModel(
equity: (json['equity'] as num?)?.toDouble(),
cash: (json['cash'] as num?)?.toDouble(),
buyingPower: (json['buyingPower'] as num?)?.toDouble(),
currency: json['currency']?.toString() ?? 'EUR',
status: json['status']?.toString() ?? 'Active',
);
}
@override
List<Object?> get props => [equity, cash, buyingPower, currency, status];
}
/// Result of an emergency "panic close" (`POST /api/v1/bot/orders/panic-close`). [skippedCount] is non-zero
/// whenever an Alpaca position could not be confirmed as liquidated by the broker (not configured, or the
/// broker call failed) - the UI MUST surface that count rather than only celebrating [closedCount] as if the
/// whole operation fully succeeded (Rules.md §4: no fabricated full success on a partial result).
class PanicCloseResultModel extends Equatable {
final int closedCount;
final int skippedCount;
final List<BotTradeOrderModel> closedOrders;
const PanicCloseResultModel({
required this.closedCount,
required this.skippedCount,
required this.closedOrders,
});
factory PanicCloseResultModel.fromJson(Map<String, dynamic> json) {
final List<dynamic> orders = json['closedOrders'] as List<dynamic>? ?? const [];
return PanicCloseResultModel(
closedCount: (json['closedCount'] as num?)?.toInt() ?? 0,
skippedCount: (json['skippedCount'] as num?)?.toInt() ?? 0,
closedOrders: orders.map((o) => BotTradeOrderModel.fromJson(o as Map<String, dynamic>)).toList(),
);
}
@override
List<Object?> get props => [closedCount, skippedCount, closedOrders];
}
class BotStatusModel extends Equatable {
final bool isRunning;
final bool autoExecutionEnabled;
final int activePositionsCount;
final int maxPositions;
final double riskPerTradePercent;
final int minCompositeScore;
final String venuesActive;
const BotStatusModel({
required this.isRunning,
required this.autoExecutionEnabled,
required this.activePositionsCount,
required this.maxPositions,
required this.riskPerTradePercent,
required this.minCompositeScore,
required this.venuesActive,
});
factory BotStatusModel.fromJson(Map<String, dynamic> json) {
return BotStatusModel(
isRunning: json['isRunning'] == true,
autoExecutionEnabled: json['autoExecutionEnabled'] == true,
activePositionsCount: (json['activePositionsCount'] as num?)?.toInt() ?? 0,
maxPositions: (json['maxPositions'] as num?)?.toInt() ?? 5,
riskPerTradePercent: (json['riskPerTradePercent'] as num?)?.toDouble() ?? 1.0,
minCompositeScore: (json['minCompositeScore'] as num?)?.toInt() ?? 75,
venuesActive: json['venuesActive']?.toString() ?? 'AlpacaPaper/Synthetic',
);
}
@override
List<Object?> get props => [
isRunning, autoExecutionEnabled, activePositionsCount,
maxPositions, riskPerTradePercent, minCompositeScore, venuesActive
];
}