221 lines
5.9 KiB
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,
|
|
};
|
|
}
|
|
}
|