feat(app): add evaluation history, bot control panel, simulation visualizer, and vendored signalr_core
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
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
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user