cyberhybridhub/server/lib/alpaca/alpaca_models.dart

221 lines
5.9 KiB
Dart

/// Alpaca v2 latest-trade response: `{ "symbol", "trade" }`.
class AlpacaLatestTradeResponse {
AlpacaLatestTradeResponse({required this.symbol, required this.trade});
final String symbol;
final AlpacaTrade trade;
factory AlpacaLatestTradeResponse.fromJson(Map<String, dynamic> json) {
return AlpacaLatestTradeResponse(
symbol: json['symbol']! as String,
trade: AlpacaTrade.fromJson(
Map<String, dynamic>.from(json['trade'] as Map),
),
);
}
}
/// Single trade tick from market data API.
class AlpacaTrade {
AlpacaTrade({
required this.timestamp,
required this.price,
required this.size,
this.exchange,
this.tape,
});
final DateTime timestamp;
final num price;
final num size;
final String? exchange;
final String? tape;
factory AlpacaTrade.fromJson(Map<String, dynamic> json) {
return AlpacaTrade(
timestamp: DateTime.parse(json['t']! as String).toUtc(),
price: json['p'] as num,
size: json['s'] as num,
exchange: json['x'] as String?,
tape: json['z'] as String?,
);
}
}
/// Daily (or intraday) OHLCV bar.
class AlpacaBar {
AlpacaBar({
required this.timestamp,
required this.open,
required this.high,
required this.low,
required this.close,
required this.volume,
});
final DateTime timestamp;
final num open;
final num high;
final num low;
final num close;
final num volume;
factory AlpacaBar.fromJson(Map<String, dynamic> json) {
return AlpacaBar(
timestamp: DateTime.parse(json['t']! as String).toUtc(),
open: json['o'] as num,
high: json['h'] as num,
low: json['l'] as num,
close: json['c'] as num,
volume: json['v'] as num,
);
}
}
/// Multi-symbol bars response: `{ "bars": { "SPY": [ ... ] } }`.
class AlpacaBarsResponse {
AlpacaBarsResponse({required this.barsBySymbol});
final Map<String, List<AlpacaBar>> barsBySymbol;
factory AlpacaBarsResponse.fromJson(Map<String, dynamic> json) {
final Map<String, dynamic> rawBars =
Map<String, dynamic>.from(json['bars'] as Map? ?? <String, dynamic>{});
final Map<String, List<AlpacaBar>> parsed = <String, List<AlpacaBar>>{};
for (final MapEntry<String, dynamic> entry in rawBars.entries) {
final List<dynamic> list = entry.value as List<dynamic>? ?? <dynamic>[];
parsed[entry.key] = list
.whereType<Map>()
.map((Map<dynamic, dynamic> m) =>
AlpacaBar.fromJson(Map<String, dynamic>.from(m)))
.toList();
}
return AlpacaBarsResponse(barsBySymbol: parsed);
}
AlpacaBar? latestBar(String symbol) {
final List<AlpacaBar>? bars = barsBySymbol[symbol];
if (bars == null || bars.isEmpty) {
return null;
}
return bars.last;
}
/// Prior daily bar when [limit] ≥ 2 (used for `prev_close` metric).
AlpacaBar? previousDailyBar(String symbol) {
final List<AlpacaBar>? bars = barsBySymbol[symbol];
if (bars == null || bars.length < 2) {
return null;
}
return bars[bars.length - 2];
}
}
/// Parsed `POST /v2/orders` (or `GET /v2/orders/by_client_order_id`) response.
///
/// Captures the fields the trade actuator uses to persist a `trade_orders`
/// row: Alpaca order id, client order id, status, symbol, side, type,
/// notional/qty, fill price, and timestamps.
class AlpacaOrderResponse {
AlpacaOrderResponse({
required this.id,
required this.clientOrderId,
required this.symbol,
required this.side,
required this.type,
required this.status,
this.notional,
this.qty,
this.filledQty,
this.filledAvgPrice,
this.submittedAt,
this.filledAt,
this.raw,
});
final String id;
final String clientOrderId;
final String symbol;
final String side;
final String type;
final String status;
final num? notional;
final num? qty;
final num? filledQty;
final num? filledAvgPrice;
final DateTime? submittedAt;
final DateTime? filledAt;
final Map<String, dynamic>? raw;
factory AlpacaOrderResponse.fromJson(Map<String, dynamic> json) {
return AlpacaOrderResponse(
id: json['id']! as String,
clientOrderId: json['client_order_id']! as String,
symbol: json['symbol']! as String,
side: json['side']! as String,
type: json['type']! as String,
status: json['status']! as String,
notional: _readOptionalNum(json['notional']),
qty: _readOptionalNum(json['qty']),
filledQty: _readOptionalNum(json['filled_qty']),
filledAvgPrice: _readOptionalNum(json['filled_avg_price']),
submittedAt: _readOptionalDateTime(json['submitted_at']),
filledAt: _readOptionalDateTime(json['filled_at']),
raw: json,
);
}
static num? _readOptionalNum(Object? value) {
if (value == null) return null;
if (value is num) return value;
if (value is String) {
if (value.isEmpty) return null;
return num.tryParse(value);
}
return null;
}
static DateTime? _readOptionalDateTime(Object? value) {
if (value == null) return null;
if (value is DateTime) return value.toUtc();
if (value is String) {
if (value.isEmpty) return null;
return DateTime.tryParse(value)?.toUtc();
}
return null;
}
}
/// Body for `POST /v2/orders` (market by notional).
class AlpacaOrderRequest {
AlpacaOrderRequest({
required this.symbol,
required this.side,
required this.type,
required this.timeInForce,
required this.clientOrderId,
this.notional,
this.qty,
});
final String symbol;
final String side;
final String type;
final String timeInForce;
final String clientOrderId;
final num? notional;
final num? qty;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'symbol': symbol,
'side': side,
'type': type,
'time_in_force': timeInForce,
'client_order_id': clientOrderId,
if (notional != null) 'notional': notional,
if (qty != null) 'qty': qty,
};
}
}