From 7f858ca908eb7bbf6cf6255ce616a50766671ee7 Mon Sep 17 00:00:00 2001 From: jrusso1020 Date: Tue, 2 Jun 2020 16:59:45 -0700 Subject: [PATCH] Add position endpoints Add the position endpoints to list all positions, get a position by symbol, delete all positions, or delete a position by symbol. Also refactor out the module structs for plain json for now. This will speed up development maybe add structs and marshalling in and out of them at a later time --- .../position/delete_all_success.json | 31 ++++++++ .../position/delete_not_found.json | 31 ++++++++ .../position/delete_success.json | 31 ++++++++ .../vcr_cassettes/position/get_not_found.json | 31 ++++++++ .../vcr_cassettes/position/get_success.json | 31 ++++++++ .../vcr_cassettes/position/list_success.json | 31 ++++++++ lib/alpaca/models/account.ex | 21 ++++++ lib/alpaca/{resources => models}/order.ex | 48 ++----------- lib/alpaca/models/position.ex | 24 +++++++ lib/alpaca/resource.ex | 32 +++------ lib/alpaca/resources/account.ex | 53 -------------- mix.exs | 4 +- test/alpaca/resources/account_test.exs | 2 +- test/alpaca/resources/order_test.exs | 10 +-- test/alpaca/resources/position_test.exs | 72 +++++++++++++++++++ test/test_helper.exs | 1 - 16 files changed, 325 insertions(+), 128 deletions(-) create mode 100644 fixture/vcr_cassettes/position/delete_all_success.json create mode 100644 fixture/vcr_cassettes/position/delete_not_found.json create mode 100644 fixture/vcr_cassettes/position/delete_success.json create mode 100644 fixture/vcr_cassettes/position/get_not_found.json create mode 100644 fixture/vcr_cassettes/position/get_success.json create mode 100644 fixture/vcr_cassettes/position/list_success.json create mode 100644 lib/alpaca/models/account.ex rename lib/alpaca/{resources => models}/order.ex (53%) create mode 100644 lib/alpaca/models/position.ex delete mode 100644 lib/alpaca/resources/account.ex create mode 100644 test/alpaca/resources/position_test.exs diff --git a/fixture/vcr_cassettes/position/delete_all_success.json b/fixture/vcr_cassettes/position/delete_all_success.json new file mode 100644 index 0000000..d062f06 --- /dev/null +++ b/fixture/vcr_cassettes/position/delete_all_success.json @@ -0,0 +1,31 @@ +[ + { + "request": { + "body": "", + "headers": { + "APCA-API-KEY-ID": "***", + "APCA-API-SECRET-KEY": "***" + }, + "method": "delete", + "options": { + "recv_timeout": 30000 + }, + "request_body": "", + "url": "https://paper-api.alpaca.markets/v2/positions" + }, + "response": { + "binary": false, + "body": "[{\"symbol\":\"TSLA\",\"status\":200,\"body\":{\"id\":\"c47ff82f-fa83-4083-9121-3623f4b3cb8d\",\"client_order_id\":\"dec09b9d-64ef-4ec1-b5a1-a644280d2535\",\"created_at\":\"2020-06-02T23:54:00.592748Z\",\"updated_at\":\"2020-06-02T23:54:00.592748Z\",\"submitted_at\":\"2020-06-02T23:54:00.589548Z\",\"filled_at\":null,\"expired_at\":null,\"canceled_at\":null,\"failed_at\":null,\"replaced_at\":null,\"replaced_by\":null,\"replaces\":null,\"asset_id\":\"8ccae427-5dd0-45b3-b5fe-7ba5e422c766\",\"symbol\":\"TSLA\",\"asset_class\":\"us_equity\",\"qty\":\"5\",\"filled_qty\":\"0\",\"filled_avg_price\":null,\"order_class\":\"\",\"order_type\":\"market\",\"type\":\"market\",\"side\":\"sell\",\"time_in_force\":\"day\",\"limit_price\":null,\"stop_price\":null,\"status\":\"accepted\",\"extended_hours\":false,\"legs\":null}},{\"symbol\":\"GOOG\",\"status\":200,\"body\":{\"id\":\"3dd828ae-7f70-449f-a54b-28758db2323d\",\"client_order_id\":\"43cd5d6c-7cbd-4e6b-968b-aa08ea19e4c1\",\"created_at\":\"2020-06-02T23:54:00.610001Z\",\"updated_at\":\"2020-06-02T23:54:00.610001Z\",\"submitted_at\":\"2020-06-02T23:54:00.604825Z\",\"filled_at\":null,\"expired_at\":null,\"canceled_at\":null,\"failed_at\":null,\"replaced_at\":null,\"replaced_by\":null,\"replaces\":null,\"asset_id\":\"f30d734c-2806-4d0d-b145-f9fade61432b\",\"symbol\":\"GOOG\",\"asset_class\":\"us_equity\",\"qty\":\"5\",\"filled_qty\":\"0\",\"filled_avg_price\":null,\"order_class\":\"\",\"order_type\":\"market\",\"type\":\"market\",\"side\":\"sell\",\"time_in_force\":\"day\",\"limit_price\":null,\"stop_price\":null,\"status\":\"accepted\",\"extended_hours\":false,\"legs\":null}},{\"symbol\":\"AAPL\",\"status\":403,\"body\":{\"available\":\"0\",\"code\":40310000,\"existing_qty\":\"2\",\"held_for_orders\":\"2\",\"message\":\"insufficient qty available for order (requested: 2, available: 0)\",\"related_orders\":[\"888e4b05-b624-48b9-a6cd-a1edf680acbf\"],\"symbol\":\"AAPL\"}}]", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Tue, 02 Jun 2020 23:54:00 GMT", + "Content-Type": "application/json; charset=UTF-8", + "Content-Length": "1712", + "Connection": "keep-alive", + "Vary": "Origin" + }, + "status_code": 207, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/fixture/vcr_cassettes/position/delete_not_found.json b/fixture/vcr_cassettes/position/delete_not_found.json new file mode 100644 index 0000000..8c714c2 --- /dev/null +++ b/fixture/vcr_cassettes/position/delete_not_found.json @@ -0,0 +1,31 @@ +[ + { + "request": { + "body": "", + "headers": { + "APCA-API-KEY-ID": "***", + "APCA-API-SECRET-KEY": "***" + }, + "method": "delete", + "options": { + "recv_timeout": 30000 + }, + "request_body": "", + "url": "https://paper-api.alpaca.markets/v2/positions/F" + }, + "response": { + "binary": false, + "body": "{\"code\":40410000,\"message\":\"position does not exist\"}", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Tue, 02 Jun 2020 23:51:59 GMT", + "Content-Type": "application/json; charset=UTF-8", + "Content-Length": "53", + "Connection": "keep-alive", + "Vary": "Origin" + }, + "status_code": 404, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/fixture/vcr_cassettes/position/delete_success.json b/fixture/vcr_cassettes/position/delete_success.json new file mode 100644 index 0000000..7697a66 --- /dev/null +++ b/fixture/vcr_cassettes/position/delete_success.json @@ -0,0 +1,31 @@ +[ + { + "request": { + "body": "", + "headers": { + "APCA-API-KEY-ID": "***", + "APCA-API-SECRET-KEY": "***" + }, + "method": "delete", + "options": { + "recv_timeout": 30000 + }, + "request_body": "", + "url": "https://paper-api.alpaca.markets/v2/positions/AAPL" + }, + "response": { + "binary": false, + "body": "{\"id\":\"888e4b05-b624-48b9-a6cd-a1edf680acbf\",\"client_order_id\":\"b126e527-3166-401f-8d82-6eb33a030ca5\",\"created_at\":\"2020-06-02T23:51:52.52428Z\",\"updated_at\":\"2020-06-02T23:51:52.52428Z\",\"submitted_at\":\"2020-06-02T23:51:52.521322Z\",\"filled_at\":null,\"expired_at\":null,\"canceled_at\":null,\"failed_at\":null,\"replaced_at\":null,\"replaced_by\":null,\"replaces\":null,\"asset_id\":\"b0b6dd9d-8b9b-48a9-ba46-b9d54906e415\",\"symbol\":\"AAPL\",\"asset_class\":\"us_equity\",\"qty\":\"2\",\"filled_qty\":\"0\",\"filled_avg_price\":null,\"order_class\":\"\",\"order_type\":\"market\",\"type\":\"market\",\"side\":\"sell\",\"time_in_force\":\"day\",\"limit_price\":null,\"stop_price\":null,\"status\":\"accepted\",\"extended_hours\":false,\"legs\":null}", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Tue, 02 Jun 2020 23:51:52 GMT", + "Content-Type": "application/json; charset=UTF-8", + "Content-Length": "682", + "Connection": "keep-alive", + "Vary": "Origin" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/fixture/vcr_cassettes/position/get_not_found.json b/fixture/vcr_cassettes/position/get_not_found.json new file mode 100644 index 0000000..5b03ac6 --- /dev/null +++ b/fixture/vcr_cassettes/position/get_not_found.json @@ -0,0 +1,31 @@ +[ + { + "request": { + "body": "", + "headers": { + "APCA-API-KEY-ID": "***", + "APCA-API-SECRET-KEY": "***" + }, + "method": "get", + "options": { + "recv_timeout": 30000 + }, + "request_body": "", + "url": "https://paper-api.alpaca.markets/v2/positions/F" + }, + "response": { + "binary": false, + "body": "{\"code\":40410000,\"message\":\"position does not exist\"}", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Tue, 02 Jun 2020 23:49:51 GMT", + "Content-Type": "application/json; charset=UTF-8", + "Content-Length": "53", + "Connection": "keep-alive", + "Vary": "Origin" + }, + "status_code": 404, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/fixture/vcr_cassettes/position/get_success.json b/fixture/vcr_cassettes/position/get_success.json new file mode 100644 index 0000000..336292f --- /dev/null +++ b/fixture/vcr_cassettes/position/get_success.json @@ -0,0 +1,31 @@ +[ + { + "request": { + "body": "", + "headers": { + "APCA-API-KEY-ID": "***", + "APCA-API-SECRET-KEY": "***" + }, + "method": "get", + "options": { + "recv_timeout": 30000 + }, + "request_body": "", + "url": "https://paper-api.alpaca.markets/v2/positions/AAPL" + }, + "response": { + "binary": false, + "body": "{\"asset_id\":\"b0b6dd9d-8b9b-48a9-ba46-b9d54906e415\",\"symbol\":\"AAPL\",\"exchange\":\"NASDAQ\",\"asset_class\":\"us_equity\",\"qty\":\"2\",\"avg_entry_price\":\"321.33\",\"side\":\"long\",\"market_value\":\"646.7\",\"cost_basis\":\"642.66\",\"unrealized_pl\":\"4.04\",\"unrealized_plpc\":\"0.0062863722652725\",\"unrealized_intraday_pl\":\"4.04\",\"unrealized_intraday_plpc\":\"0.0062863722652725\",\"current_price\":\"323.35\",\"lastday_price\":\"321.85\",\"change_today\":\"0.0046605561597017\"}", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Tue, 02 Jun 2020 23:47:49 GMT", + "Content-Type": "application/json; charset=UTF-8", + "Content-Length": "437", + "Connection": "keep-alive", + "Vary": "Origin" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/fixture/vcr_cassettes/position/list_success.json b/fixture/vcr_cassettes/position/list_success.json new file mode 100644 index 0000000..38eff33 --- /dev/null +++ b/fixture/vcr_cassettes/position/list_success.json @@ -0,0 +1,31 @@ +[ + { + "request": { + "body": "", + "headers": { + "APCA-API-KEY-ID": "***", + "APCA-API-SECRET-KEY": "***" + }, + "method": "get", + "options": { + "recv_timeout": 30000 + }, + "request_body": "", + "url": "https://paper-api.alpaca.markets/v2/positions" + }, + "response": { + "binary": false, + "body": "[{\"asset_id\":\"8ccae427-5dd0-45b3-b5fe-7ba5e422c766\",\"symbol\":\"TSLA\",\"exchange\":\"NASDAQ\",\"asset_class\":\"us_equity\",\"qty\":\"5\",\"avg_entry_price\":\"892.736\",\"side\":\"long\",\"market_value\":\"4411.95\",\"cost_basis\":\"4463.68\",\"unrealized_pl\":\"-51.73\",\"unrealized_plpc\":\"-0.0115890924080579\",\"unrealized_intraday_pl\":\"-51.73\",\"unrealized_intraday_plpc\":\"-0.0115890924080579\",\"current_price\":\"882.39\",\"lastday_price\":\"898.1\",\"change_today\":\"-0.01749248413317\"},{\"asset_id\":\"f30d734c-2806-4d0d-b145-f9fade61432b\",\"symbol\":\"GOOG\",\"exchange\":\"NASDAQ\",\"asset_class\":\"us_equity\",\"qty\":\"5\",\"avg_entry_price\":\"1431.01\",\"side\":\"long\",\"market_value\":\"7157.5\",\"cost_basis\":\"7155.05\",\"unrealized_pl\":\"2.45\",\"unrealized_plpc\":\"0.0003424154967471\",\"unrealized_intraday_pl\":\"2.45\",\"unrealized_intraday_plpc\":\"0.0003424154967471\",\"current_price\":\"1431.5\",\"lastday_price\":\"1431.82\",\"change_today\":\"-0.000223491779693\"},{\"asset_id\":\"b0b6dd9d-8b9b-48a9-ba46-b9d54906e415\",\"symbol\":\"AAPL\",\"exchange\":\"NASDAQ\",\"asset_class\":\"us_equity\",\"qty\":\"2\",\"avg_entry_price\":\"321.33\",\"side\":\"long\",\"market_value\":\"646.4\",\"cost_basis\":\"642.66\",\"unrealized_pl\":\"3.74\",\"unrealized_plpc\":\"0.0058195624435938\",\"unrealized_intraday_pl\":\"3.74\",\"unrealized_intraday_plpc\":\"0.0058195624435938\",\"current_price\":\"323.2\",\"lastday_price\":\"321.85\",\"change_today\":\"0.0041945005437316\"}]", + "headers": { + "Server": "nginx/1.16.1", + "Date": "Tue, 02 Jun 2020 23:45:50 GMT", + "Content-Type": "application/json; charset=UTF-8", + "Content-Length": "1326", + "Connection": "keep-alive", + "Vary": "Accept-Encoding" + }, + "status_code": 200, + "type": "ok" + } + } +] \ No newline at end of file diff --git a/lib/alpaca/models/account.ex b/lib/alpaca/models/account.ex new file mode 100644 index 0000000..1a4ffc8 --- /dev/null +++ b/lib/alpaca/models/account.ex @@ -0,0 +1,21 @@ +defmodule Alpaca.Account do + @moduledoc """ + A resource that allows us to query an Account from Alpaca + """ + + alias Alpaca.Client + + @doc """ + Retrieve your Alpaca trading account info + + ## Example + iex> {:ok, %{} = account} = Account.get() + + Allows us to retrieve our own account information as a result tuple {:ok, %{}} + if successful. If not success we will get back a result tuple {:error, {status: http_status_code, body: http_response_body}} + """ + @spec get() :: {:ok, map()} | {:error, map()} + def get() do + Client.get("/v2/account") + end +end diff --git a/lib/alpaca/resources/order.ex b/lib/alpaca/models/order.ex similarity index 53% rename from lib/alpaca/resources/order.ex rename to lib/alpaca/models/order.ex index c3c434a..c588d56 100644 --- a/lib/alpaca/resources/order.ex +++ b/lib/alpaca/models/order.ex @@ -30,64 +30,26 @@ defmodule Alpaca.Order do The `delete_all/0` method allows us to delete all open orders by calling `Alpaca.Order.delete_all()`. The `delete/1` method allows us to delete a specific order by calling `Alpaca.Order.delete(id)`. - Where id is the id of the order we want to delete. + Where `id` is the id of the order we want to delete. The `get_by_client_order_id/1` method allows us to get a specific order by the client order id by calling `Alpaca.Order.get_by_client_order_id(client_order_id)`. Where client_order_id is the client order id on the order. """ use Alpaca.Resource, endpoint: "orders" - use TypedStruct - - @typedoc "An Order" - typedstruct do - field(:id, String.t()) - field(:client_order_id, String.t()) - field(:created_at, String.t()) - field(:updated_at, String.t()) - field(:submitted_at, String.t()) - field(:filled_at, String.t()) - field(:expired_at, String.t()) - field(:canceled_at, String.t()) - field(:failed_at, String.t()) - field(:replaced_at, String.t()) - field(:replaced_by, String.t()) - field(:replaces, String.t()) - field(:asset_id, String.t()) - field(:symbol, String.t()) - field(:asset_class, String.t()) - field(:qty, String.t()) - field(:filled_qty, String.t()) - field(:type, String.t()) - field(:side, String.t()) - field(:time_in_force, String.t()) - field(:limit_price, String.t()) - field(:stop_price, String.t()) - field(:filled_avg_price, String.t()) - field(:status, String.t()) - field(:extended_hours, boolean()) - # TODO: investigate best way for nested objects - # field(:legs, [Order.t()]) - end @doc """ Retrieve an order by client order id ### Example ``` - iex> {:ok, %Order{} = order} = Order.get_by_client_order_id(client_order_id) + iex> {:ok, %{} = order} = Order.get_by_client_order_id(client_order_id) ``` - Allows us to retrieve our an order as a result tuple `{:ok, %Alpaca.Order{}}` + Allows us to retrieve our an order as a result tuple `{:ok, %{}}` if successful. If not success we will get back a result tuple `{:error, {status: http_status_code, body: http_response_body}}` """ - @spec get_by_client_order_id(String.t()) :: {:ok, __MODULE__.t()} | {:error, map()} + @spec get_by_client_order_id(String.t()) :: {:ok, map()} | {:error, map()} def get_by_client_order_id(client_order_id) do - case Client.get("/v2/orders:by_client_order_id", %{client_order_id: client_order_id}) do - {:ok, order} -> - {:ok, struct(__MODULE__, order)} - - {:error, error} -> - {:error, error} - end + Client.get("/v2/orders:by_client_order_id", %{client_order_id: client_order_id}) end end diff --git a/lib/alpaca/models/position.ex b/lib/alpaca/models/position.ex new file mode 100644 index 0000000..0b345bc --- /dev/null +++ b/lib/alpaca/models/position.ex @@ -0,0 +1,24 @@ +defmodule Alpaca.Position do + @moduledoc """ + A resource that allows us to perform operations on an Position + + An position has the following methods we can call on it + ``` + get/1 + list/1 + delete_all/0 + delete/1 + ``` + + The `get/1` method allows us to get a singular position by calling `Alpaca.Position.get(symbol)`. + Where `symbol` is the symbol of the position to get. + + The `list/1` method allows us to list all positions by calling `Alpaca.Position.list()`. + + The `delete_all/0` method allows us to close all open positions by calling `Alpaca.Position.delete_all()`. + + The `delete/1` method allows us to close a specific position by calling `Alpaca.Position.delete(symbol)`. + Where `symbol` is the symbol of the position we want to close. + """ + use Alpaca.Resource, endpoint: "positions", exclude: [:create, :edit] +end diff --git a/lib/alpaca/resource.ex b/lib/alpaca/resource.ex index 2d2aebb..c134f25 100644 --- a/lib/alpaca/resource.ex +++ b/lib/alpaca/resource.ex @@ -42,11 +42,9 @@ defmodule Alpaca.Resource do @doc """ A function to list all resources from the Alpaca API """ - @spec list(map()) :: {:ok, [__MODULE__.t()]} | {:error, map()} + @spec list(map()) :: {:ok, [map()]} | {:error, map()} def list(params \\ %{}) do - with {:ok, resources} <- Client.get(base_url(), params) do - {:ok, Enum.map(resources, fn resource -> struct(__MODULE__, resource) end)} - end + Client.get(base_url(), params) end end @@ -54,11 +52,9 @@ defmodule Alpaca.Resource do @doc """ A function to get a singlular resource from the Alpaca API """ - @spec get(String.t(), map()) :: {:ok, __MODULE__.t()} | {:error, map()} + @spec get(String.t(), map()) :: {:ok, map()} | {:error, map()} def get(id, params \\ %{}) do - with {:ok, resource} <- Client.get(resource_url(id), params) do - {:ok, struct(__MODULE__, resource)} - end + Client.get(resource_url(id), params) end end @@ -66,11 +62,9 @@ defmodule Alpaca.Resource do @doc """ A function to create a new resource from the Alpaca API """ - @spec create(map()) :: {:ok, __MODULE__.t()} | {:error, map()} + @spec create(map()) :: {:ok, map()} | {:error, map()} def create(params) do - with {:ok, resource} <- Client.post(base_url(), params) do - {:ok, struct(__MODULE__, resource)} - end + Client.post(base_url(), params) end end @@ -78,11 +72,9 @@ defmodule Alpaca.Resource do @doc """ A function to edit an existing resource using the Alpaca API """ - @spec edit(String.t(), map()) :: {:ok, __MODULE__.t()} | {:error, map()} + @spec edit(String.t(), map()) :: {:ok, map()} | {:error, map()} def edit(id, params) do - with {:ok, resource} <- Client.patch(resource_url(id), params) do - {:ok, struct(__MODULE__, resource)} - end + Client.patch(resource_url(id), params) end end @@ -97,8 +89,8 @@ defmodule Alpaca.Resource do Enum.map(response_body, fn item -> %{ status: item.status, - id: item.id, - resource: delete_all_resource_body(item.body) + id: item[:id] || item[:symbol], + resource: item.body } end)} end @@ -117,10 +109,6 @@ defmodule Alpaca.Resource do end end - defp delete_all_resource_body(nil), do: nil - - defp delete_all_resource_body(resource), do: struct(__MODULE__, resource) - @spec base_url :: String.t() defp base_url, do: "/v2/#{unquote(endpoint)}" diff --git a/lib/alpaca/resources/account.ex b/lib/alpaca/resources/account.ex deleted file mode 100644 index 63ec10f..0000000 --- a/lib/alpaca/resources/account.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Alpaca.Account do - @moduledoc """ - A resource that allows us to query an Account from Alpaca - """ - - use TypedStruct - - alias Alpaca.Client - - @typedoc "An Account" - typedstruct enforce: true do - field(:id, String.t()) - field(:account_number, String.t()) - field(:status, String.t()) - field(:currency, String.t()) - field(:cash, String.t()) - field(:portfolio_value, String.t()) - field(:pattern_day_trader, boolean()) - field(:trade_suspended_by_user, boolean()) - field(:transfers_blocked, boolean()) - field(:account_blocked, boolean()) - field(:created_at, String.t()) - field(:shorting_enabled, boolean()) - field(:long_market_value, String.t()) - field(:equity, String.t()) - field(:last_equity, String.t()) - field(:multiplier, String.t()) - field(:buying_power, String.t()) - field(:initial_margin, String.t()) - field(:maintenance_margin, String.t()) - field(:sma, String.t()) - field(:daytrade_count, String.t()) - field(:last_maintenance_margin, String.t()) - field(:daytrading_buying_power, String.t()) - field(:regt_buying_power, String.t()) - end - - @doc """ - Retrieve your Alpaca trading account info - - ## Example - iex> {:ok, %Account{} = account} = Account.get() - - Allows us to retrieve our own account information as a result tuple {:ok, %Account{}} - if successful. If not success we will get back a result tuple {:error, {status: http_status_code, body: http_response_body}} - """ - @spec get() :: {:ok, __MODULE__.t()} | {:error, map()} - def get() do - with {:ok, account} <- Client.get("/v2/account") do - {:ok, struct(__MODULE__, account)} - end - end -end diff --git a/mix.exs b/mix.exs index ce78a9c..1a32de9 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,6 @@ defmodule AlpacaElixir.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:bypass, "~> 1.0", only: :test}, {:confex, "~> 3.4.0"}, {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:excoveralls, "~> 0.10", only: :test}, @@ -42,8 +41,7 @@ defmodule AlpacaElixir.MixProject do {:hackney, "~> 1.15.2"}, {:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:jason, "~> 1.2"}, - {:tesla, "~> 1.3.0"}, - {:typed_struct, "~> 0.1.4"} + {:tesla, "~> 1.3.0"} ] end diff --git a/test/alpaca/resources/account_test.exs b/test/alpaca/resources/account_test.exs index b2eb26f..0a032d7 100644 --- a/test/alpaca/resources/account_test.exs +++ b/test/alpaca/resources/account_test.exs @@ -7,7 +7,7 @@ defmodule Alpaca.AccountTest do describe "get/0" do test "gets the account successfully" do use_cassette "account/get_success" do - assert {:ok, %Account{} = account} = Account.get() + assert {:ok, account} = Account.get() assert not is_nil(account.id) end diff --git a/test/alpaca/resources/order_test.exs b/test/alpaca/resources/order_test.exs index e81ba4e..5444b29 100644 --- a/test/alpaca/resources/order_test.exs +++ b/test/alpaca/resources/order_test.exs @@ -9,7 +9,7 @@ defmodule Alpaca.OrderTest do use_cassette "order/list_success" do params = %{status: "all"} - assert {:ok, [%Order{} = order] = orders} = Order.list(params) + assert {:ok, [order] = orders} = Order.list(params) assert not is_nil(order.id) end end @@ -20,7 +20,7 @@ defmodule Alpaca.OrderTest do use_cassette "order/get_success" do id = "ebfc1c74-fd4a-46e1-b4e7-7f6f8ab1ef7a" - assert {:ok, %Order{id: ^id} = order} = Order.get(id) + assert {:ok, %{id: ^id} = order} = Order.get(id) end end @@ -55,7 +55,7 @@ defmodule Alpaca.OrderTest do time_in_force: "day" } - assert {:ok, %Order{} = order} = Order.create(params) + assert {:ok, order} = Order.create(params) assert not is_nil(order.id) end end @@ -100,7 +100,7 @@ defmodule Alpaca.OrderTest do use_cassette "order/get_by_client_order_id_success" do client_order_id = "e08ad98f-07a4-40a8-9b6e-7186b0b7dd8d" - assert {:ok, %Order{client_order_id: ^client_order_id}} = + assert {:ok, %{client_order_id: ^client_order_id}} = Order.get_by_client_order_id(client_order_id) end end @@ -141,7 +141,7 @@ defmodule Alpaca.OrderTest do Enum.map(response, fn item -> assert item.status == 200 assert not is_nil(item.id) - assert %Order{} = item.resource + assert not is_nil(item.resource) end) end end diff --git a/test/alpaca/resources/position_test.exs b/test/alpaca/resources/position_test.exs new file mode 100644 index 0000000..8ba72d8 --- /dev/null +++ b/test/alpaca/resources/position_test.exs @@ -0,0 +1,72 @@ +defmodule Alpaca.PositionTest do + use ExUnit.Case, async: true + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + + alias Alpaca.Position + + describe "list/0" do + test "gets the list of positions successfully" do + use_cassette "position/list_success" do + assert {:ok, positions} = Position.list() + assert length(positions) == 3 + end + end + end + + describe "get/1" do + test "gets the position successfully" do + use_cassette "position/get_success" do + assert {:ok, %{symbol: "AAPL", qty: "2"}} = Position.get("AAPL") + end + end + + test "cannot find the position" do + use_cassette "position/get_not_found" do + symbol = "F" + message = "position does not exist" + + assert {:error, %{status: 404, body: %{code: 40_410_000, message: ^message}}} = + Position.get(symbol) + end + end + end + + describe "delete_all/0" do + test "deletes the position successfully" do + use_cassette "position/delete_all_success" do + assert {:ok, response} = Position.delete_all() + + Enum.map(response, fn item -> + if item.id in ["TSLA", "GOOG"] do + assert item.status == 200 + assert not is_nil(item.id) + assert not is_nil(item.resource) + else + # AAPL has already been liquidated + assert item.status == 403 + assert not is_nil(item.id) + assert not is_nil(item.resource) + end + end) + end + end + end + + describe "delete/1" do + test "deletes the position successfully" do + use_cassette "position/delete_success" do + assert {:ok, %{symbol: "AAPL", qty: "2"}} = Position.delete("AAPL") + end + end + + test "cannot find the position" do + use_cassette "position/delete_not_found" do + symbol = "F" + message = "position does not exist" + + assert {:error, %{status: 404, body: %{code: 40_410_000, message: ^message}}} = + Position.delete(symbol) + end + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 071f140..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1 @@ ExUnit.start() -Application.ensure_all_started(:bypass)