import 'dart:convert'; import 'package:http/http.dart' as http; import 'alpaca_env.dart'; import 'alpaca_models.dart'; /// REST client for Alpaca Trading API v2 (paper by default). /// /// Step 9 (`TRADING_TDD_PLAN.md`) — wraps `POST /v2/orders` and /// `GET /v2/orders:by_client_order_id` with idempotent semantics. class AlpacaTradingClient { AlpacaTradingClient({ required AlpacaEnv env, http.Client? httpClient, }) : _env = env, _client = httpClient ?? http.Client(); final AlpacaEnv _env; final http.Client _client; Map get _headers => { 'APCA-API-KEY-ID': _env.apiKeyId, 'APCA-API-SECRET-KEY': _env.apiSecretKey, 'Content-Type': 'application/json', 'Accept': 'application/json', }; /// `POST /v2/orders` — places a market order. /// /// Throws [AlpacaTradingDuplicateClientOrderIdException] when Alpaca rejects /// the request because [request.clientOrderId] was already used. Callers /// should resolve the existing order with [getOrderByClientOrderId]. Future submitOrder(AlpacaOrderRequest request) async { _env.requireCredentials(); _env.assertPaperOnly(); final Uri uri = Uri.parse('${_env.tradingBaseUrl}/v2/orders'); final http.Response response = await _client.post( uri, headers: _headers, body: jsonEncode(request.toJson()), ); if (response.statusCode == 200 || response.statusCode == 201) { return AlpacaOrderResponse.fromJson( jsonDecode(response.body) as Map, ); } if (response.statusCode == 422 && response.body.toLowerCase().contains('client_order_id')) { throw AlpacaTradingDuplicateClientOrderIdException( clientOrderId: request.clientOrderId, body: response.body, ); } throw AlpacaTradingException( 'submitOrder failed: ${response.statusCode} ${response.body}', ); } /// `GET /v2/orders:by_client_order_id?client_order_id=...` — returns null /// when Alpaca has no order under that id (404). Future getOrderByClientOrderId( String clientOrderId, ) async { _env.requireCredentials(); final Uri uri = Uri.parse('${_env.tradingBaseUrl}/v2/orders:by_client_order_id').replace( queryParameters: {'client_order_id': clientOrderId}, ); final http.Response response = await _client.get(uri, headers: _headers); if (response.statusCode == 200) { return AlpacaOrderResponse.fromJson( jsonDecode(response.body) as Map, ); } if (response.statusCode == 404) { return null; } throw AlpacaTradingException( 'getOrderByClientOrderId failed: ${response.statusCode} ${response.body}', ); } void close() => _client.close(); } class AlpacaTradingException implements Exception { AlpacaTradingException(this.message); final String message; @override String toString() => message; } class AlpacaTradingDuplicateClientOrderIdException extends AlpacaTradingException { AlpacaTradingDuplicateClientOrderIdException({ required this.clientOrderId, required this.body, }) : super('duplicate client_order_id=$clientOrderId: $body'); final String clientOrderId; final String body; }