221 lines
7.9 KiB
Dart
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
|
|
];
|
|
}
|