From 3e596821c852dac85b994e996c1ca3ccd255a814 Mon Sep 17 00:00:00 2001 From: nickbair Date: Wed, 2 Oct 2024 20:14:13 -0600 Subject: [PATCH 1/6] WIP models --- config.yml | 54 +++---- lib/code_gen/code_generator.ex | 34 +++-- lib/code_gen/config.ex | 3 +- .../response/accounts/account_changes.ex | 136 ++++++++++++++++++ .../response/accounts/account_summary.ex | 86 +++++++++++ lib/models/response/accounts/find_account.ex | 90 ++++++++++++ .../accounts/list_account_instruments.ex | 89 ++++++++++++ lib/models/response/accounts/list_accounts.ex | 31 ++++ lib/models/response/accounts/order.ex | 36 +++++ lib/models/response/accounts/position.ex | 41 ++++++ lib/models/response/accounts/position_side.ex | 36 +++++ lib/models/response/accounts/trade_summary.ex | 65 +++++++++ lib/models/response/accounts/transaction.ex | 26 ++++ lib/models/response/atom.ex | 19 +++ lib/models/response/error.ex | 21 +++ .../response/instruments/list_candles.ex | 43 ++++++ lib/models/response/instruments/price.ex | 24 ++++ lib/models/response/map_or_list.ex | 18 +++ lib/models/response/response.ex | 28 ++++ lib/util/api.ex | 29 ++-- lib/util/http_status.ex | 34 +++++ lib/util/transform.ex | 52 +++++++ mix.exs | 1 + mix.lock | 1 + 24 files changed, 941 insertions(+), 56 deletions(-) create mode 100644 lib/models/response/accounts/account_changes.ex create mode 100644 lib/models/response/accounts/account_summary.ex create mode 100644 lib/models/response/accounts/find_account.ex create mode 100644 lib/models/response/accounts/list_account_instruments.ex create mode 100644 lib/models/response/accounts/list_accounts.ex create mode 100644 lib/models/response/accounts/order.ex create mode 100644 lib/models/response/accounts/position.ex create mode 100644 lib/models/response/accounts/position_side.ex create mode 100644 lib/models/response/accounts/trade_summary.ex create mode 100644 lib/models/response/accounts/transaction.ex create mode 100644 lib/models/response/atom.ex create mode 100644 lib/models/response/error.ex create mode 100644 lib/models/response/instruments/list_candles.ex create mode 100644 lib/models/response/instruments/price.ex create mode 100644 lib/models/response/map_or_list.ex create mode 100644 lib/models/response/response.ex create mode 100644 lib/util/http_status.ex create mode 100644 lib/util/transform.ex diff --git a/config.yml b/config.yml index 15c280c..08720ac 100644 --- a/config.yml +++ b/config.yml @@ -6,22 +6,26 @@ interfaces: description: "Get a list of accounts." http_method: "GET" path: "/accounts" + response_schema: "ListAccounts" - function_name: "find" description: "Fetch an account." http_method: "GET" path: "/accounts/:account_id" + response_schema: "FindAccount" arguments: - "account_id" - function_name: "summary" description: "Get a summary of an account." http_method: "GET" path: "/accounts/:account_id/summary" + response_schema: "AccountSummary" arguments: - "account_id" - function_name: "list_instruments" description: "Get a list of tradeable instruments for the given account." http_method: "GET" path: "/accounts/:account_id/instruments" + response_schema: "AccountInstruments" arguments: - "account_id" parameters: @@ -42,7 +46,7 @@ interfaces: arguments: - "account_id" parameters: - - name: "sinceTransactionID" + - name: "since_transaction_id" type: "string" doc: "ID of the Transaction to get Account changes since." - module_name: "Instruments" @@ -77,19 +81,19 @@ interfaces: type: "boolean" doc: "A flag that controls whether the candlestick is smoothed or not." default: false - - name: "includeFirst" + - name: "include_first" type: "boolean" doc: "A flag that controls whether the candlestick that is covered by the from time should be included in the results." default: true - - name: "dailyAlignment" + - name: "daily_alignment" type: "non_neg_integer" doc: "The hour of the day (in the specified timezone) to use for granularities that have daily alignments, minimum 0, maximum 23." default: 17 - - name: "alignmentTimezone" + - name: "alignment_timezone" type: "string" doc: "The timezone to use for the dailyAlignment parameter, note that the returned times will still be represented in UTC." default: "America/New_York" - - name: "weeklyAlignment" + - name: "weekly_alignment" type: "string" doc: "The day of the week used for granularities that have weekly alignment." default: "Friday" @@ -143,7 +147,7 @@ interfaces: type: "non_neg_integer" doc: "The maximum number of Orders to return." default: 50 - - name: "beforeID" + - name: "before_id" type: "string" doc: "The maximum Order ID to return. If not provided the most recent Orders in the Account are returned." - function_name: "list_pending" @@ -205,7 +209,7 @@ interfaces: type: "non_neg_integer" doc: "The maximum number of Trades to return." default: 50 - - name: "beforeID" + - name: "before_id" type: "string" doc: "The maximum Trade ID to return. If not provided the most recent Trades in the Account are returned." - function_name: "list_open" @@ -291,7 +295,7 @@ interfaces: - name: "to" type: "string" doc: "The ending time (inclusive) of the time range for the Transactions being queried. Defaults to current time." - - name: "pageSize" + - name: "page_size" type: "non_neg_integer" doc: "The number of Transactions to include in each page. [default=100, maximum=1000]." default: 100 @@ -342,7 +346,7 @@ interfaces: arguments: - "account_id" parameters: - - name: "candleSpecifications" + - name: "candle_specifications" type: "string" doc: "The specification of the candles to fetch." required: true @@ -354,15 +358,15 @@ interfaces: type: "boolean" doc: "A flag that controls whether the candlestick is smoothed or not." default: false - - name: "dailyAlignment" + - name: "daily_alignment" type: "non_neg_integer" doc: "The hour of the day (in the specified timezone) to use for granularities that have daily alignments, minimum 0, maximum 23." default: 17 - - name: "alignmentTimezone" + - name: "alignment_timezone" type: "string" doc: "The timezone to use for the dailyAlignment parameter, note that the returned times will still be represented in UTC." default: "America/New_York" - - name: "weeklyAlignment" + - name: "weekly_alignment" type: "string" doc: "The day of the week used for granularities that have weekly alignment." default: "Friday" @@ -380,11 +384,11 @@ interfaces: - name: "since" type: "string" doc: "The time of the snapshot to fetch using either RFC 3339 or Unix format. If not specified, then the most recent snapshot is fetched." - - name: "includeUnitsAvailable" + - name: "include_units_available" type: "boolean" doc: "Flag that enables the inclusion of the unitsAvailable field in the returned Price objects." default: false - - name: "includeHomeConversion" + - name: "include_home_conversion" type: "boolean" doc: "Flag that enables the inclusion of the homeConversions field in the returned response." default: false @@ -418,35 +422,23 @@ interfaces: type: "boolean" doc: "A flag that controls whether the candlestick is smoothed or not." default: false - - name: "includeFirst" + - name: "include_first" type: "boolean" doc: "A flag that controls whether the candlestick that is covered by the from time should be included in the results." default: true - - name: "dailyAlignment" + - name: "daily_alignment" type: "non_neg_integer" doc: "The hour of the day (in the specified timezone) to use for granularities that have daily alignments, minimum 0, maximum 23." default: 17 - - name: "alignmentTimezone" + - name: "alignment_timezone" type: "string" doc: "The timezone to use for the dailyAlignment parameter, note that the returned times will still be represented in UTC." default: "America/New_York" - - name: "weeklyAlignment" + - name: "weekly_alignment" type: "string" doc: "The day of the week used for granularities that have weekly alignment." default: "Friday" - name: "units" type: "string" doc: "The number of units used to calculate the volume-weighted average bid and ask prices." - default: 1 - - -# schemas: -# - module_name: "Account" -# description: "Schema for Oanda account" -# properties: -# - name: "id" -# type: "string" -# description: "The account id" -# - name: "name" -# type: "string" -# description: "The account name" \ No newline at end of file + default: 1 \ No newline at end of file diff --git a/lib/code_gen/code_generator.ex b/lib/code_gen/code_generator.ex index 6ec26fa..39ff774 100644 --- a/lib/code_gen/code_generator.ex +++ b/lib/code_gen/code_generator.ex @@ -99,10 +99,11 @@ defmodule ExOanda.CodeGenerator do end defp generate_function(config) do - %{function_name: name, description: desc, http_method: method, path: path, arguments: args, parameters: parameters} = config + %{function_name: name, description: desc, http_method: method, path: path, arguments: args, parameters: parameters, response_schema: response_schema} = config formatted_args = format_args(args) formatted_params = format_params(parameters) arg_types = generate_arg_types(args) + model = generate_module_name(response_schema) quote do @doc""" @@ -130,11 +131,11 @@ defmodule ExOanda.CodeGenerator do path_params: path_params, method: unquote(method), headers: API.base_headers(), - params: params + params: ExOanda.CodeGenerator.convert_params(params) ) |> API.maybe_attach_telemetry(conn) |> Req.request(conn.options) - |> API.handle_response() + |> API.handle_response(unquote(model)) {:error, reason} -> {:error, reason} @@ -169,6 +170,19 @@ defmodule ExOanda.CodeGenerator do |> String.to_atom() end + @doc false + def convert_params(params) do + params + |> Enum.into(%{}) + |> Recase.Enumerable.convert_keys(&Recase.to_camel/1) + |> Enum.map(fn {k, v} -> + case String.ends_with?(k, "Id") do + true -> {String.replace(k, "Id", "ID"), v} + false -> {k, v} + end + end) + end + defp generate_module_name(module_name), do: Module.concat([ExOanda, module_name]) defp format_args(args) do @@ -177,11 +191,15 @@ defmodule ExOanda.CodeGenerator do defp format_params(params) do Enum.reduce(params, [], fn %{name: name, type: type, required: required, default: default, doc: doc}, acc -> - acc - |> Keyword.put( - String.to_atom(name), - [type: String.to_atom(type), required: required, default: default, doc: doc] - ) + params_list = [ + type: String.to_atom(type), + required: required, + default: default, + doc: doc + ] + + filtered_params = Enum.reject(params_list, fn {_, value} -> is_nil(value) end) + Keyword.put(acc, String.to_atom(name), filtered_params) end) end diff --git a/lib/code_gen/config.ex b/lib/code_gen/config.ex index 205366b..0bdf47c 100644 --- a/lib/code_gen/config.ex +++ b/lib/code_gen/config.ex @@ -18,6 +18,7 @@ defmodule ExOanda.Config do field(:http_method, :string) field(:path, :string) field(:arguments, {:array, :string}, default: []) + field(:response_schema, :string) embeds_many :parameters, Parameters, primary_key: false do field(:name, :string) @@ -54,7 +55,7 @@ defmodule ExOanda.Config do defp functions_changeset(struct, params) do struct - |> cast(params, [:function_name, :description, :http_method, :path, :arguments]) + |> cast(params, [:function_name, :description, :http_method, :path, :arguments, :response_schema]) |> validate_required([:function_name, :description, :http_method, :path]) |> cast_embed(:parameters, with: &embedded_changeset/2) end diff --git a/lib/models/response/accounts/account_changes.ex b/lib/models/response/accounts/account_changes.ex new file mode 100644 index 0000000..583fbb0 --- /dev/null +++ b/lib/models/response/accounts/account_changes.ex @@ -0,0 +1,136 @@ +defmodule ExOanda.AccountChanges do + @moduledoc """ + Schema for Oanda account changes response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.{ + Order, + Position, + TradeSummary, + Transaction + } + + @primary_key false + + typed_embedded_schema do + embeds_many :changes, Change, primary_key: false do + embeds_many :orders_created, Order + embeds_many :orders_cancelled, Order + embeds_many :orders_filled, Order + embeds_many :orders_triggered, Order + + embeds_many :trades_opened, TradeSummary + embeds_many :trades_reduced, TradeSummary + embeds_many :trades_closed, TradeSummary + + embeds_many :positions, Position + + embeds_many :transactions, Transaction + end + + embeds_one :state, AccountState, primary_key: false do + field(:unrealized_pl, :float) + field(:nav, :float) + field(:margin_used, :float) + field(:margin_available, :float) + field(:position_value, :float) + field(:margin_closeout_unrealized_pl, :float) + field(:margin_closeout_nav, :float) + field(:margin_closeout_margin_used, :float) + field(:margin_closeout_percent, :float) + field(:margin_closeout_position_value, :float) + field(:withdrawal_limit, :float) + field(:margin_call_margin_used, :float) + field(:margin_call_percent, :float) + field(:balance, :float) + field(:pl, :float) + field(:resettable_pl, :float) + field(:financing, :float) + field(:commission, :float) + field(:dividend_adjustment, :float) + field(:guaranteed_execution_fees, :float) + field(:margin_call_enter_time, :utc_datetime_usec) + field(:margin_call_extension_count, :integer) + field(:last_margin_call_extension_time, :utc_datetime_usec) + field(:last_transaction_id, :string) + + embeds_many :orders, OrderState, primary_key: false do + field(:id, :string) + field(:trailing_stop_value, :float) + field(:trigger_distance, :float) + field(:is_trigger_distance_exact, :boolean) + end + + embeds_many :trades, TradeState, primary_key: false do + field(:id, :string) + field(:unrealized_pl, :float) + field(:margin_used, :float) + end + + embeds_many :positions, PositionState, primary_key: false do + field(:instrument, :string) + field(:net_unrealized_pl, :float) + field(:long_unrealized_pl, :float) + field(:short_unrealized_pl, :float) + field(:margin_used, :float) + end + end + + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:changes, with: &changes_changeset/2) + |> cast_embed(:state, with: &state_changeset/2) + end + + defp changes_changeset(struct, params) do + struct + |> cast(params, []) + |> cast_embed(:orders_created) + |> cast_embed(:orders_cancelled) + |> cast_embed(:orders_filled) + |> cast_embed(:orders_triggered) + |> cast_embed(:trades_opened) + |> cast_embed(:trades_reduced) + |> cast_embed(:trades_closed) + |> cast_embed(:positions) + |> cast_embed(:transactions) + end + + defp state_changeset(struct, params) do + struct + |> cast(params, [ + :unrealized_pl, :nav, :margin_used, :margin_available, :position_value, + :margin_closeout_unrealized_pl, :margin_closeout_nav, :margin_closeout_margin_used, + :margin_closeout_percent, :margin_closeout_position_value, :withdrawal_limit, + :margin_call_margin_used, :margin_call_percent, :balance, :pl, :resettable_pl, + :financing, :commission, :dividend_adjustment, :guaranteed_execution_fees, + :margin_call_enter_time, :margin_call_extension_count, :last_margin_call_extension_time, + :last_transaction_id + ]) + |> cast_embed(:orders, with: &order_state_changeset/2) + |> cast_embed(:trades, with: &trade_state_changeset/2) + |> cast_embed(:positions, with: &position_state_changeset/2) + end + + defp order_state_changeset(struct, params) do + struct + |> cast(params, [:id, :trailing_stop_value, :trigger_distance, :is_trigger_distance_exact]) + end + + defp trade_state_changeset(struct, params) do + struct + |> cast(params, [:id, :unrealized_pl, :margin_used]) + end + + defp position_state_changeset(struct, params) do + struct + |> cast(params, [:instrument, :net_unrealized_pl, :long_unrealized_pl, :short_unrealized_pl, :margin_used]) + end +end diff --git a/lib/models/response/accounts/account_summary.ex b/lib/models/response/accounts/account_summary.ex new file mode 100644 index 0000000..a8b02f1 --- /dev/null +++ b/lib/models/response/accounts/account_summary.ex @@ -0,0 +1,86 @@ +defmodule ExOanda.AccountSummary do + @moduledoc """ + Schmea for Oanda account summary response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + embeds_one :account, Account, primary_key: false do + field(:id, :string) + field(:alias, :string) + field(:currency, :string) + field(:created_by_user_id, :integer) + field(:created_time, :utc_datetime_usec) + field(:resettabled_pl_time, :utc_datetime_usec) + field(:margin_rate, :float) + field(:open_trade_count, :integer) + field(:open_position_count, :integer) + field(:pending_order_count, :integer) + field(:hedging_enabled, :boolean) + field(:unrealized_pl, :float) + field(:nav, :float) + field(:margin_used, :float) + field(:margin_available, :float) + field(:position_value, :float) + field(:margin_closeout_unrealized_pl, :float) + field(:margin_closeout_nav, :float) + field(:margin_closeout_margin_used, :float) + field(:margin_closeout_percent, :float) + field(:margin_closeout_position_value, :float) + field(:withdrawal_limit, :float) + field(:margin_call_margin_used, :float) + field(:margin_call_percent, :float) + field(:balance, :float) + field(:pl, :float) + field(:resettable_pl, :float) + field(:financing, :float) + field(:commission, :float) + field(:dividend_adjustment, :float) + field(:guaranteed_execution_fees, :float) + field(:margin_call_enter_time, :utc_datetime_usec) + field(:margin_call_extension_count, :integer) + field(:last_margin_call_extension_time, :utc_datetime_usec) + field(:last_transaction_id, :string) + field(:guaranteed_stop_loss_order_mode, Ecto.Enum, values: [:DISABLED, :ALLOWED, :REQUIRED]) + + embeds_one :guaranteed_stop_loss_order_parameters, GuaranteedStopLossOrderParameters, primary_key: false do + field(:mutability_market_open, Ecto.Enum, values: [:FIXED, :REPLACEABLE, :CANCELABLE, :PRICE_WIDEN_ONLY]) + field(:mutability_market_halted, Ecto.Enum, values: [:FIXED, :REPLACEABLE, :CANCELABLE, :PRICE_WIDEN_ONLY]) + end + end + + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:account, with: &account_changeset/2) + end + + defp account_changeset(struct, params) do + struct + |> cast(params, [ + :id, :alias, :currency, :created_by_user_id, :created_time, :resettabled_pl_time, :margin_rate, + :open_trade_count, :open_position_count, :pending_order_count, :hedging_enabled, :unrealized_pl, :nav, + :margin_used, :margin_available, :position_value, :margin_closeout_unrealized_pl, :margin_closeout_nav, + :margin_closeout_margin_used, :margin_closeout_percent, :margin_closeout_position_value, :withdrawal_limit, + :margin_call_margin_used, :margin_call_percent, :balance, :pl, :resettable_pl, :financing, :commission, + :dividend_adjustment, :guaranteed_execution_fees, :margin_call_enter_time, :margin_call_extension_count, + :last_margin_call_extension_time, :last_transaction_id, :guaranteed_stop_loss_order_mode + ]) + |> validate_required([:id]) + |> cast_embed(:guaranteed_stop_loss_order_parameters, with: &guaranteed_stop_loss_order_parameters_changeset/2) + end + + defp guaranteed_stop_loss_order_parameters_changeset(struct, params) do + struct + |> cast(params, [:mutability_market_open, :mutability_market_halted]) + |> validate_required([:mutability_market_open, :mutability_market_halted]) + end +end diff --git a/lib/models/response/accounts/find_account.ex b/lib/models/response/accounts/find_account.ex new file mode 100644 index 0000000..6e02595 --- /dev/null +++ b/lib/models/response/accounts/find_account.ex @@ -0,0 +1,90 @@ +defmodule ExOanda.FindAccount do + @moduledoc """ + Schema for Oanda find account response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + embeds_one :account, Account, primary_key: false do + field(:id, :string) + field(:alias, :string) + field(:currency, :string) + field(:created_by_user_id, :integer) + field(:created_time, :utc_datetime_usec) + field(:resettabled_pl_time, :utc_datetime_usec) + field(:margin_rate, :float) + field(:open_trade_count, :integer) + field(:open_position_count, :integer) + field(:pending_order_count, :integer) + field(:hedging_enabled, :boolean) + field(:unrealized_pl, :float) + field(:nav, :float) + field(:margin_used, :float) + field(:margin_available, :float) + field(:position_value, :float) + field(:margin_closeout_unrealized_pl, :float) + field(:margin_closeout_nav, :float) + field(:margin_closeout_margin_used, :float) + field(:margin_closeout_percent, :float) + field(:margin_closeout_position_value, :float) + field(:withdrawal_limit, :float) + field(:margin_call_margin_used, :float) + field(:margin_call_percent, :float) + field(:balance, :float) + field(:pl, :float) + field(:resettable_pl, :float) + field(:financing, :float) + field(:commission, :float) + field(:dividend_adjustment, :float) + field(:guaranteed_execution_fees, :float) + field(:margin_call_enter_time, :utc_datetime_usec) + field(:margin_call_extension_count, :integer) + field(:last_margin_call_extension_time, :utc_datetime_usec) + field(:last_transaction_id, :string) + field(:guaranteed_stop_loss_order_mode, Ecto.Enum, values: [:DISABLED, :ALLOWED, :REQUIRED]) + + embeds_one :guaranteed_stop_loss_order_parameters, GuaranteedStopLossOrderParameters, primary_key: false do + field(:mutability_market_open, Ecto.Enum, values: [:FIXED, :REPLACEABLE, :CANCELABLE, :PRICE_WIDEN_ONLY]) + field(:mutability_market_halted, Ecto.Enum, values: [:FIXED, :REPLACEABLE, :CANCELABLE, :PRICE_WIDEN_ONLY]) + end + + # TODO + field(:trades, {:array, :map}, primary_key: false) + field(:positions, {:array, :map}, primary_key: false) + field(:orders, {:array, :map}, primary_key: false) + end + + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:account, with: &account_changeset/2) + |> validate_required([:last_transaction_id]) + end + + defp account_changeset(struct, params) do + struct + |> cast(params, [ + :id, :alias, :currency, :created_by_user_id, :created_time, :resettabled_pl_time, :margin_rate, + :open_trade_count, :open_position_count, :pending_order_count, :hedging_enabled, :unrealized_pl, :nav, + :margin_used, :margin_available, :position_value, :margin_closeout_unrealized_pl, :margin_closeout_nav, + :margin_closeout_margin_used, :margin_closeout_percent, :margin_closeout_position_value, :withdrawal_limit, + :margin_call_margin_used, :margin_call_percent, :balance, :pl, :resettable_pl, :financing, :commission, + :dividend_adjustment, :guaranteed_execution_fees, :margin_call_enter_time, :margin_call_extension_count, + :last_margin_call_extension_time, :last_transaction_id + ]) + |> cast_embed(:guaranteed_stop_loss_order_parameters, with: &guaranteed_stop_loss_order_parameters_changeset/2) + end + + defp guaranteed_stop_loss_order_parameters_changeset(struct, params) do + struct + |> cast(params, [:mutability_market_open, :mutability_market_halted]) + end +end diff --git a/lib/models/response/accounts/list_account_instruments.ex b/lib/models/response/accounts/list_account_instruments.ex new file mode 100644 index 0000000..981f6f8 --- /dev/null +++ b/lib/models/response/accounts/list_account_instruments.ex @@ -0,0 +1,89 @@ +defmodule ExOanda.AccountInstruments do + @moduledoc """ + Schema for Oanda list account instruments response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + embeds_many :instruments, Instrument, primary_key: false do + field(:name, :string) + field(:type, Ecto.Enum, values: [:CURRENCY, :CFD, :METAL]) + field(:display_name, :string) + field(:pip_location, :integer) + field(:display_precision, :integer) + field(:trade_units_precision, :integer) + field(:minimum_trade_size, :float) + field(:maximum_trailing_stop_distance, :float) + field(:minimum_trailing_stop_distance, :float) + field(:maximum_position_size, :float) + field(:maximum_order_units, :float) + field(:margin_rate, :float) + field(:guaranteed_stop_loss_order_mode, Ecto.Enum, values: [:DISABLED, :ALLOWED, :REQUIRED]) + field(:commission, :float) + + embeds_many :tags, Tag, primary_key: false do + field(:type, :string) + field(:name, :string) + end + + embeds_one :financing, Financing, primary_key: false do + field(:long_rate, :float) + field(:short_rate, :float) + + embeds_many :financing_days_of_week, FinancingDay, primary_key: false do + field(:day_of_week, Ecto.Enum, values: [:SUNDAY, :MONDAY, :TUESDAY, :WEDNESDAY, :THURSDAY, :FRIDAY, :SATURDAY]) + field(:days_charged, :integer) + end + end + end + + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:instruments, with: &instrument_changeset/2) + end + + defp instrument_changeset(struct, params) do + struct + |> cast(params, [ + :name, :type, :display_name, :pip_location, :display_precision, :trade_units_precision, + :minimum_trade_size, :maximum_trailing_stop_distance, :minimum_trailing_stop_distance, + :maximum_position_size, :maximum_order_units, :margin_rate, :guaranteed_stop_loss_order_mode, + :commission + ]) + |> validate_required([ + :name, :type, :display_name, :pip_location, :display_precision, :trade_units_precision, + :minimum_trade_size, :maximum_trailing_stop_distance, :minimum_trailing_stop_distance, + :maximum_position_size, :maximum_order_units, :margin_rate, :guaranteed_stop_loss_order_mode + ]) + |> cast_embed(:tags, with: &tag_changeset/2) + |> cast_embed(:financing, with: &financing_changeset/2) + end + + defp tag_changeset(struct, params) do + struct + |> cast(params, [:type, :name]) + |> validate_required([:type, :name]) + end + + defp financing_changeset(struct, params) do + struct + |> cast(params, [:long_rate, :short_rate]) + |> validate_required([:long_rate, :short_rate]) + |> cast_embed(:financing_days_of_week, with: &financing_day_changeset/2) + end + + defp financing_day_changeset(struct, params) do + struct + |> cast(params, [:day_of_week, :days_charged]) + |> validate_required([:day_of_week, :days_charged]) + end +end diff --git a/lib/models/response/accounts/list_accounts.ex b/lib/models/response/accounts/list_accounts.ex new file mode 100644 index 0000000..ed7a465 --- /dev/null +++ b/lib/models/response/accounts/list_accounts.ex @@ -0,0 +1,31 @@ +defmodule ExOanda.ListAccounts do + @moduledoc """ + Schema for Oanda list accounts response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + embeds_many :accounts, AccountProperties, primary_key: false do + field(:id, :string) + field(:mt4_account_id, :integer) + field(:tags, {:array, :string}, default: []) + end + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, []) + |> cast_embed(:accounts, with: &account_properties_changeset/2) + end + + defp account_properties_changeset(struct, params) do + struct + |> cast(params, [:id, :mt4_account_id, :tags]) + |> validate_required([:id]) + end +end diff --git a/lib/models/response/accounts/order.ex b/lib/models/response/accounts/order.ex new file mode 100644 index 0000000..4d9a912 --- /dev/null +++ b/lib/models/response/accounts/order.ex @@ -0,0 +1,36 @@ +defmodule ExOanda.Order do + @moduledoc """ + Schema for Oanda order. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:id, :string) + field(:creat_time, :utc_datetime_usec) + field(:state, :string) + + embeds_many :client_extensions, ClientExtensions, primary_key: false do + field(:id, :string) + field(:tag, :string) + field(:comment, :string) + end + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:id, :creat_time, :state]) + |> cast_embed(:client_extensions, with: &client_extensions_changeset/2) + |> validate_required([:id, :creat_time, :state]) + end + + defp client_extensions_changeset(struct, params) do + struct + |> cast(params, [:id, :tag, :comment]) + |> validate_required([:id]) + end +end diff --git a/lib/models/response/accounts/position.ex b/lib/models/response/accounts/position.ex new file mode 100644 index 0000000..4fc69c8 --- /dev/null +++ b/lib/models/response/accounts/position.ex @@ -0,0 +1,41 @@ +defmodule ExOanda.Position do + @moduledoc """ + Schema for position + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.PositionSide + + @primary_key false + + typed_embedded_schema do + field(:instrument, :string) + field(:pl, :float) + field(:unrealized_pl, :float) + field(:margin_used, :float) + field(:resettable_pl, :float) + field(:financing, :float) + field(:commission, :float) + field(:dividend_adjustment, :float) + field(:guaranteed_execution_fees, :float) + + embeds_one :long, PositionSide + embeds_one :short, PositionSide + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [ + :instrument, :pl, :unrealized_pl, :margin_used, :resettable_pl, + :financing, :commission, :dividend_adjustment, :guaranteed_execution_fees + ]) + |> validate_required([ + :instrument, :pl, :unrealized_pl, :margin_used, :resettable_pl, + :financing, :commission, :dividend_adjustment, :guaranteed_execution_fees + ]) + |> cast_embed(:long) + |> cast_embed(:short) + end +end diff --git a/lib/models/response/accounts/position_side.ex b/lib/models/response/accounts/position_side.ex new file mode 100644 index 0000000..938de36 --- /dev/null +++ b/lib/models/response/accounts/position_side.ex @@ -0,0 +1,36 @@ +defmodule ExOanda.PositionSide do + @moduledoc """ + Schema for Oanda position side. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:units, :integer) + field(:average_price, :float) + field(:trade_ids, {:array, :string}, default: []) + field(:pl, :float) + field(:unrealized_pl, :float) + field(:resettable_pl, :float) + field(:financing, :float) + field(:commission, :float) + field(:dividend_adjustment, :float) + field(:guaranteed_execution_fees, :float) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [ + :units, :average_price, :trade_ids, :pl, :unrealized_pl, :resettable_pl, + :financing, :commission, :dividend_adjustment, :guaranteed_execution_fees + ]) + |> validate_required([ + :units, :average_price, :trade_ids, :pl, :unrealized_pl, :resettable_pl, + :financing, :commission, :dividend_adjustment, :guaranteed_execution_fees + ]) + end +end diff --git a/lib/models/response/accounts/trade_summary.ex b/lib/models/response/accounts/trade_summary.ex new file mode 100644 index 0000000..cc88697 --- /dev/null +++ b/lib/models/response/accounts/trade_summary.ex @@ -0,0 +1,65 @@ +defmodule ExOanda.TradeSummary do + @moduledoc """ + Schema and type definitions for trade summary. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:id, :string) + field(:instrument, :string) + field(:price, :float) + field(:open_time, :utc_datetime_usec) + field(:state, Ecto.Enum, values: [:OPEN, :CLOSED, :CLOSE_WHEN_TRADEABLE]) + field(:initial_units, :integer) + field(:initial_margin_required, :float) + field(:current_units, :integer) + field(:realized_pl, :float) + field(:unrealized_pl, :float) + field(:margin_used, :float) + field(:average_close_price, :float) + field(:closing_transaction_ids, {:array, :string}) + field(:financing, :float) + field(:dividend_adjustment, :float) + field(:close_time, :utc_datetime_usec) + field(:take_profit_order_id, :string) + field(:stop_loss_order_id, :string) + field(:guaranteed_stop_loss_order_id, :string) + field(:trailing_stop_loss_order_id, :string) + + embeds_many :client_extensions, ClientExtensions, primary_key: false do + field(:id, :string) + field(:tag, :string) + field(:comment, :string) + end + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [ + :id, :instrument, :price, :open_time, :state, :initial_units, + :initial_margin_required, :current_units, :realized_pl, :unrealized_pl, + :margin_used, :average_close_price, :closing_transaction_ids, :financing, + :dividend_adjustment, :close_time, :take_profit_order_id, :stop_loss_order_id, + :guaranteed_stop_loss_order_id, :trailing_stop_loss_order_id + ]) + |> cast_embed(:client_extensions, with: &client_extensions_changeset/2) + |> validate_required([ + :id, :instrument, :price, :open_time, :state, :initial_units, + :initial_margin_required, :current_units, :realized_pl, :unrealized_pl, + :margin_used, :average_close_price, :closing_transaction_ids, :financing, + :dividend_adjustment, :close_time, :take_profit_order_id, :stop_loss_order_id, + :guaranteed_stop_loss_order_id, :trailing_stop_loss_order_id + ]) + end + + defp client_extensions_changeset(struct, params) do + struct + |> cast(params, [:id, :tag, :comment]) + |> validate_required([:id]) + end +end diff --git a/lib/models/response/accounts/transaction.ex b/lib/models/response/accounts/transaction.ex new file mode 100644 index 0000000..bef63f6 --- /dev/null +++ b/lib/models/response/accounts/transaction.ex @@ -0,0 +1,26 @@ +defmodule ExOanda.Transaction do + @moduledoc """ + Schema for Oanda transaction. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:id, :string) + field(:time, :utc_datetime_usec) + field(:user_id, :integer) + field(:account_id, :string) + field(:batch_id, :string) + field(:request_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:id, :time, :user_id, :account_id, :batch_id, :request_id]) + |> validate_required([:id, :time, :user_id, :account_id, :batch_id, :request_id]) + end +end diff --git a/lib/models/response/atom.ex b/lib/models/response/atom.ex new file mode 100644 index 0000000..c66dff3 --- /dev/null +++ b/lib/models/response/atom.ex @@ -0,0 +1,19 @@ +defmodule ExOanda.Type.Atom do + @moduledoc """ + Custom Ecto type for atom. + """ + + use Ecto.Type + + @type t :: atom() + + def type, do: :string + + def cast(value) when is_atom(value), do: {:ok, value} + def cast(_), do: :error + + def load(value), do: {:ok, String.to_existing_atom(value)} + + def dump(value) when is_atom(value), do: {:ok, Atom.to_string(value)} + def dump(_), do: :error +end diff --git a/lib/models/response/error.ex b/lib/models/response/error.ex new file mode 100644 index 0000000..8b951f4 --- /dev/null +++ b/lib/models/response/error.ex @@ -0,0 +1,21 @@ +defmodule ExOanda.Error do + @moduledoc """ + Standard error wrapper + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:error_code, :string) + field(:error_message, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:error_code, :error_message]) + end +end diff --git a/lib/models/response/instruments/list_candles.ex b/lib/models/response/instruments/list_candles.ex new file mode 100644 index 0000000..293a67f --- /dev/null +++ b/lib/models/response/instruments/list_candles.ex @@ -0,0 +1,43 @@ +defmodule ExOanda.ListCandles do + @moduledoc """ + Schema for Oanda instruments response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.Price + + @primary_key false + + typed_embedded_schema do + field(:instrument, :string) + field(:granularity, :string) + + embeds_many :candles, Candlestick, primary_key: false do + field(:time, :utc_datetime_usec) + field(:volume, :integer) + field(:complete, :boolean) + + embeds_one :bid, Price + embeds_one :ask, Price + embeds_one :mid, Price + end + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:instrument, :granularity]) + |> cast_embed(:candles, with: &candlestick_changeset/2) + |> validate_required([:instrument, :granularity]) + end + + defp candlestick_changeset(struct, params) do + struct + |> cast(params, [:time, :volume, :complete]) + |> cast_embed(:bid) + |> cast_embed(:ask) + |> cast_embed(:mid) + |> validate_required([:time, :volume, :complete]) + end +end diff --git a/lib/models/response/instruments/price.ex b/lib/models/response/instruments/price.ex new file mode 100644 index 0000000..037dd18 --- /dev/null +++ b/lib/models/response/instruments/price.ex @@ -0,0 +1,24 @@ +defmodule ExOanda.Price do + @moduledoc """ + Schema for Oanda price. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:o, :float) + field(:h, :float) + field(:l, :float) + field(:c, :float) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:o, :h, :l, :c]) + |> validate_required([:o, :h, :l, :c]) + end +end diff --git a/lib/models/response/map_or_list.ex b/lib/models/response/map_or_list.ex new file mode 100644 index 0000000..8c69643 --- /dev/null +++ b/lib/models/response/map_or_list.ex @@ -0,0 +1,18 @@ +defmodule ExOanda.Type.MapOrList do + @moduledoc """ + Custom Ecto type for map or list. + """ + + use Ecto.Type + + @type t :: map() | list() + + def type, do: :any + + def cast(data) when is_map(data) or is_list(data), do: {:ok, data} + def cast(_), do: :error + + def load(data) when is_map(data) or is_list(data), do: {:ok, data} + + def dump(data) when is_map(data) or is_list(data), do: {:ok, data} +end diff --git a/lib/models/response/response.ex b/lib/models/response/response.ex new file mode 100644 index 0000000..9999dd8 --- /dev/null +++ b/lib/models/response/response.ex @@ -0,0 +1,28 @@ +defmodule ExOanda.Response do + @moduledoc """ + Common response schema for Oanda API. + """ + + use TypedEctoSchema + import Ecto.Changeset + + alias ExOanda.{ + Type.Atom, + Type.MapOrList + } + + @primary_key false + + typed_embedded_schema do + field(:data, MapOrList) + field(:request_id, :string) + field(:status, Atom) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:data, :request_id, :status]) + |> validate_required([:data, :request_id, :status]) + end +end diff --git a/lib/util/api.ex b/lib/util/api.ex index d6a5e81..bba1d19 100644 --- a/lib/util/api.ex +++ b/lib/util/api.ex @@ -2,6 +2,7 @@ defmodule ExOanda.API do @moduledoc false alias ExOanda.Connection, as: Conn + alias ExOanda.Transform, as: TF # Requests ################################################################### @@ -21,27 +22,23 @@ defmodule ExOanda.API do def base_headers(opts \\ []), do: Keyword.merge(@base_headers, opts) # Responses #################################################################### - @spec handle_response({atom(), Req.Response.t() | map()}) :: {:ok, any()} | {:error, any()} - def handle_response(res) do - case format_response(res) do - {:ok, body} -> {:ok, body} - {:error, body} -> {:error, body} - _ -> res - end - end - defp format_response({:ok, %{status: status, body: body}}) when status in @success_codes do - {:ok, body, status} - end + @spec handle_response({atom(), Req.Response.t() | map()}, atom() | nil) :: {:ok, any()} | {:error, any()} + def handle_response(res, transform_to \\ nil) do + case format_response(res) do + {:ok, r} -> {:ok, TF.transform(r, transform_to)} + {:error, r} -> {:error, TF.transform(r, transform_to)} - defp format_response({:ok, %{status: status, body: body}}) do - {:error, body, status} - end + # TODO: handle this one + # {:error, reason} -> {:error, reason} - defp format_response({:error, %{reason: reason}}) do - {:error, reason} + _ -> res + end end + defp format_response({:ok, %{status: status} = res}) when status in @success_codes, do: {:ok, res} + defp format_response({:ok, res}), do: {:error, res} + defp format_response({:error, %{reason: reason}}), do: {:error, reason} defp format_response(res), do: res # Telemetry ############################################################## diff --git a/lib/util/http_status.ex b/lib/util/http_status.ex new file mode 100644 index 0000000..fe97736 --- /dev/null +++ b/lib/util/http_status.ex @@ -0,0 +1,34 @@ +defmodule ExOanda.HttpStatus do + @moduledoc false + + @status_codes %{ + 200 => :ok, + 201 => :created, + 202 => :accepted, + 204 => :no_content, + 400 => :bad_request, + 401 => :unauthorized, + 403 => :forbidden, + 404 => :not_found, + 405 => :method_not_allowed, + 406 => :not_acceptable, + 409 => :conflict, + 410 => :gone, + 411 => :length_required, + 412 => :precondition_failed, + 413 => :payload_too_large, + 414 => :uri_too_long, + 415 => :unsupported_media_type, + 418 => :im_a_teapot, + 429 => :too_many_requests, + 500 => :internal_server_error, + 501 => :not_implemented, + 502 => :bad_gateway, + 503 => :service_unavailable, + 504 => :gateway_timeout + } + + @doc false + @spec status_to_atom(integer) :: atom() + def status_to_atom(status), do: Map.get(@status_codes, status, :unknown) +end diff --git a/lib/util/transform.ex b/lib/util/transform.ex new file mode 100644 index 0000000..978215a --- /dev/null +++ b/lib/util/transform.ex @@ -0,0 +1,52 @@ +defmodule ExOanda.Transform do + @moduledoc false + + import Ecto.Changeset + require Logger + alias ExOanda.{ + CodeGenerator, + HttpStatus, + Response + } + + @spec transform(map(), atom()) :: Response.t() + def transform(response, model) do + %Response{} + |> Response.changeset(preprocess_body(model, response)) + |> apply_changes() + end + + defp preprocess_body(model, response) do + %{ + "data" => preprocess_data(model, response.body), + "status" => HttpStatus.status_to_atom(response.status), + "request_id" => Map.get(response.headers, "requestid", []) |> List.first() + } + end + + @spec preprocess_data(nil | atom(), map()) :: [Ecto.Schema.t()] | [map()] | Ecto.Schema.t() | map() + def preprocess_data(nil, data), do: data + + def preprocess_data(model, data) when is_map(data) do + model.__struct__() + |> model.changeset(Recase.Enumerable.convert_keys(data, &Recase.to_snake/1)) + |> log_validations(model) + |> apply_changes() + end + + def preprocess_data(model, data) when is_list(data) do + Enum.map(data, &preprocess_data(model, &1)) + end + + def preprocess_data(_model, data), do: data + + defp log_validations(%{valid?: true} = changeset, _model), do: changeset + + defp log_validations(changeset, model) do + traverse_errors(changeset, fn _changeset, field, {msg, opts} -> + Logger.warning("Validation error while transforming #{CodeGenerator.format_module_name(model)}: #{field} #{msg} #{inspect(opts)}") + end) + + changeset + end +end diff --git a/mix.exs b/mix.exs index da76b25..722fa16 100644 --- a/mix.exs +++ b/mix.exs @@ -34,6 +34,7 @@ defmodule ExOanda.MixProject do {:excoveralls, "~> 0.18.1", only: :test}, {:ex_doc, "~> 0.31", only: :dev, runtime: false}, {:nimble_options, "~> 1.1"}, + {:recase, "~> 0.8.1"}, {:req, "~> 0.5.2"}, {:req_telemetry, "~> 0.1.1"}, {:typed_ecto_schema, "~> 0.4.1", runtime: false}, diff --git a/mix.lock b/mix.lock index 4035d6f..59899c5 100644 --- a/mix.lock +++ b/mix.lock @@ -29,6 +29,7 @@ "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "recase": {:hex, :recase, "0.8.1", "ab98cd35857a86fa5ca99036f575241d71d77d9c2ab0c39aacf1c9b61f6f7d1d", [:mix], [], "hexpm", "9fd8d63e7e43bd9ea385b12364e305778b2bbd92537e95c4b2e26fc507d5e4c2"}, "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, "req_telemetry": {:hex, :req_telemetry, "0.1.1", "208663ff2402bc9278225db2e79ba467a78c39cf6b00814e056eb6312e9f4e95", [:mix], [{:req, "~> 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a3f7427e44a6e8a48d387eff50ee504c662576592c2c2024e66adfec1c0c614e"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, From 0ab260349ce91b77617f8e10101e5e54b4f4ba30 Mon Sep 17 00:00:00 2001 From: nickbair Date: Wed, 2 Oct 2024 20:52:32 -0600 Subject: [PATCH 2/6] Add order models --- config.yml | 17 +++++--------- lib/models/response/{accounts => }/order.ex | 0 lib/models/response/orders/find_order.ex | 22 ++++++++++++++++++ lib/models/response/orders/list_orders.ex | 23 +++++++++++++++++++ .../response/orders/list_pending_orders.ex | 23 +++++++++++++++++++ 5 files changed, 74 insertions(+), 11 deletions(-) rename lib/models/response/{accounts => }/order.ex (100%) create mode 100644 lib/models/response/orders/find_order.ex create mode 100644 lib/models/response/orders/list_orders.ex create mode 100644 lib/models/response/orders/list_pending_orders.ex diff --git a/config.yml b/config.yml index 08720ac..54da755 100644 --- a/config.yml +++ b/config.yml @@ -43,6 +43,7 @@ interfaces: description: "Get a list of changes to an account." http_method: "GET" path: "/accounts/:account_id/changes" + response_schema: "AccountChanges" arguments: - "account_id" parameters: @@ -56,6 +57,7 @@ interfaces: description: "Get candlestick data for an instrument." http_method: "GET" path: "/instruments/:instrument/candles" + response_schema: "ListCandles" arguments: - "instrument" parameters: @@ -97,17 +99,7 @@ interfaces: type: "string" doc: "The day of the week used for granularities that have weekly alignment." default: "Friday" - - function_name: "list_order_book" - description: "Get order book data for an instrument." - http_method: "GET" - path: "/instruments/:instrument/orderBook" - arguments: - - "instrument" - parameters: - - name: "time" - type: "string" - doc: "The time of the snapshot to fetch using either RFC 3339 or Unix format. If not specified, then the most recent snapshot is fetched." - - function_name: "list_position_book" + description: "Get position book data for an instrument." http_method: "GET" path: "/instruments/:instrument/positionBook" @@ -131,6 +123,7 @@ interfaces: description: "Get a list of orders for an account." http_method: "GET" path: "/accounts/:account_id/orders" + response_schema: "ListOrders" arguments: - "account_id" parameters: @@ -154,12 +147,14 @@ interfaces: description: "Get a list of pending orders for an account." http_method: "GET" path: "/accounts/:account_id/pendingOrders" + response_schema: "ListPendingOrders" arguments: - "account_id" - function_name: "find" description: "Get details for a single order in an account." http_method: "GET" path: "/accounts/:account_id/orders/:order_id" + response_schema: "FindOrder" arguments: - "account_id" - "order_id" diff --git a/lib/models/response/accounts/order.ex b/lib/models/response/order.ex similarity index 100% rename from lib/models/response/accounts/order.ex rename to lib/models/response/order.ex diff --git a/lib/models/response/orders/find_order.ex b/lib/models/response/orders/find_order.ex new file mode 100644 index 0000000..ea94798 --- /dev/null +++ b/lib/models/response/orders/find_order.ex @@ -0,0 +1,22 @@ +defmodule ExOanda.FindOrder do + @moduledoc """ + Schema for Oanda find order response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + embeds_one :order, Order + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:order) + end +end diff --git a/lib/models/response/orders/list_orders.ex b/lib/models/response/orders/list_orders.ex new file mode 100644 index 0000000..f03b098 --- /dev/null +++ b/lib/models/response/orders/list_orders.ex @@ -0,0 +1,23 @@ +defmodule ExOanda.ListOrders do + @moduledoc """ + Schema for Oanda list orders response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.Order + + @primary_key false + + typed_embedded_schema do + embeds_many :orders, Order + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:orders) + end +end diff --git a/lib/models/response/orders/list_pending_orders.ex b/lib/models/response/orders/list_pending_orders.ex new file mode 100644 index 0000000..32f0906 --- /dev/null +++ b/lib/models/response/orders/list_pending_orders.ex @@ -0,0 +1,23 @@ +defmodule ExOanda.ListPendingOrders do + @moduledoc """ + Schema for Oanda list pending orders response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.Order + + @primary_key false + + typed_embedded_schema do + embeds_many :orders, Order + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:orders) + end +end From a7aac88eb4d8e132d858a8918dc5bafbc55e84c0 Mon Sep 17 00:00:00 2001 From: nickbair Date: Wed, 2 Oct 2024 21:18:37 -0600 Subject: [PATCH 3/6] Fix transform and tests --- lib/models/response/orders/find_order.ex | 1 + lib/util/api.ex | 12 +++++++----- lib/util/transform.ex | 22 +++++++++++++++++----- test/api_test.exs | 2 +- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/models/response/orders/find_order.ex b/lib/models/response/orders/find_order.ex index ea94798..e4d9637 100644 --- a/lib/models/response/orders/find_order.ex +++ b/lib/models/response/orders/find_order.ex @@ -5,6 +5,7 @@ defmodule ExOanda.FindOrder do use TypedEctoSchema import Ecto.Changeset + alias ExOanda.Order @primary_key false diff --git a/lib/util/api.ex b/lib/util/api.ex index bba1d19..482cc96 100644 --- a/lib/util/api.ex +++ b/lib/util/api.ex @@ -26,11 +26,14 @@ defmodule ExOanda.API do @spec handle_response({atom(), Req.Response.t() | map()}, atom() | nil) :: {:ok, any()} | {:error, any()} def handle_response(res, transform_to \\ nil) do case format_response(res) do - {:ok, r} -> {:ok, TF.transform(r, transform_to)} - {:error, r} -> {:error, TF.transform(r, transform_to)} + # 2xx status codes + {:ok, fr} -> {:ok, TF.transform(fr, transform_to)} - # TODO: handle this one - # {:error, reason} -> {:error, reason} + # HTTP error, e.g. transport error + {:error, %{reason: reason}} -> {:error, reason} + + # Non-2xx status codes + {:error, fr} -> {:error, TF.transform(fr, transform_to)} _ -> res end @@ -38,7 +41,6 @@ defmodule ExOanda.API do defp format_response({:ok, %{status: status} = res}) when status in @success_codes, do: {:ok, res} defp format_response({:ok, res}), do: {:error, res} - defp format_response({:error, %{reason: reason}}), do: {:error, reason} defp format_response(res), do: res # Telemetry ############################################################## diff --git a/lib/util/transform.ex b/lib/util/transform.ex index 978215a..1e02133 100644 --- a/lib/util/transform.ex +++ b/lib/util/transform.ex @@ -17,11 +17,23 @@ defmodule ExOanda.Transform do end defp preprocess_body(model, response) do - %{ - "data" => preprocess_data(model, response.body), - "status" => HttpStatus.status_to_atom(response.status), - "request_id" => Map.get(response.headers, "requestid", []) |> List.first() - } + data = + response + |> Map.get(:body) + |> then(&preprocess_data(model, &1)) + + status = + response + |> Map.get(:status) + |> HttpStatus.status_to_atom() + + request_id = + response + |> Map.get(:headers, %{}) + |> Map.get("requestid", []) + |> List.first() + + %{"data" => data, "status" => status, "request_id" => request_id} end @spec preprocess_data(nil | atom(), map()) :: [Ecto.Schema.t()] | [map()] | Ecto.Schema.t() | map() diff --git a/test/api_test.exs b/test/api_test.exs index 2cd282f..90b8e8b 100644 --- a/test/api_test.exs +++ b/test/api_test.exs @@ -44,7 +44,7 @@ defmodule ExOandaTest.API do end test "returns {:error, reason} for HTTP issues, e.g. timeout" do - assert API.handle_response({:error, "timeout"}) == {:error, "timeout"} + assert API.handle_response({:error, %Req.TransportError{reason: :nxdomain}}) == {:error, :nxdomain} end end From 5649f52eb7dc7d5e1e9f0aae132495759b46be2a Mon Sep 17 00:00:00 2001 From: nickbair Date: Thu, 3 Oct 2024 20:15:59 -0600 Subject: [PATCH 4/6] Add more models --- config.yml | 13 ++++ .../response/positions/list_positions.ex | 24 +++++++ .../{accounts => positions}/position.ex | 0 lib/models/response/pricing/latest_candles.ex | 21 ++++++ lib/models/response/pricing/list_pricing.ex | 59 ++++++++++++++++ lib/models/response/pricing/price_bucket.ex | 22 ++++++ lib/models/response/trades/find_trade.ex | 24 +++++++ lib/models/response/trades/list_trades.ex | 26 +++++++ lib/models/response/trades/trade.ex | 67 +++++++++++++++++++ lib/models/response/trades/trade_order.ex | 64 ++++++++++++++++++ .../response/transactions/find_transaction.ex | 24 +++++++ .../transactions/list_transactions.ex | 27 ++++++++ .../list_transactions_id_range.ex | 24 +++++++ .../{accounts => transactions}/transaction.ex | 0 14 files changed, 395 insertions(+) create mode 100644 lib/models/response/positions/list_positions.ex rename lib/models/response/{accounts => positions}/position.ex (100%) create mode 100644 lib/models/response/pricing/latest_candles.ex create mode 100644 lib/models/response/pricing/list_pricing.ex create mode 100644 lib/models/response/pricing/price_bucket.ex create mode 100644 lib/models/response/trades/find_trade.ex create mode 100644 lib/models/response/trades/list_trades.ex create mode 100644 lib/models/response/trades/trade.ex create mode 100644 lib/models/response/trades/trade_order.ex create mode 100644 lib/models/response/transactions/find_transaction.ex create mode 100644 lib/models/response/transactions/list_transactions.ex create mode 100644 lib/models/response/transactions/list_transactions_id_range.ex rename lib/models/response/{accounts => transactions}/transaction.ex (100%) diff --git a/config.yml b/config.yml index 54da755..4731757 100644 --- a/config.yml +++ b/config.yml @@ -188,6 +188,7 @@ interfaces: description: "Get a list of trades for an account." http_method: "GET" path: "/accounts/:account_id/trades" + response_schema: "ListTrades" arguments: - "account_id" parameters: @@ -211,12 +212,14 @@ interfaces: description: "Get a list of open trades for an account." http_method: "GET" path: "/accounts/:account_id/openTrades" + response_schema: "ListTrades" arguments: - "account_id" - function_name: "find" description: "Get details for a single trade in an account." http_method: "GET" path: "/accounts/:account_id/trades/:trade_id" + response_schema: "FindTrade" arguments: - "account_id" - "trade_id" @@ -251,18 +254,21 @@ interfaces: description: "Get a list of positions for an account." http_method: "GET" path: "/accounts/:account_id/positions" + response_schema: "ListPositions" arguments: - "account_id" - function_name: "list_open" description: "Get a list of open positions for an account." http_method: "GET" path: "/accounts/:account_id/openPositions" + response_schema: "ListPositions" arguments: - "account_id" - function_name: "find" description: "Get the details of a single instrument's position in an account." http_method: "GET" path: "/accounts/:account_id/positions/:instrument" + response_schema: "ListPositions" arguments: - "account_id" - "instrument" @@ -281,6 +287,7 @@ interfaces: description: "Get a list of transactions for an account." http_method: "GET" path: "/accounts/:account_id/transactions" + response_schema: "ListTransactions" arguments: - "account_id" parameters: @@ -301,6 +308,7 @@ interfaces: description: "Get the details of a single transaction in an account." http_method: "GET" path: "/accounts/:account_id/transactions/:transaction_id" + response_schema: "FindTransaction" arguments: - "account_id" - "transaction_id" @@ -308,6 +316,7 @@ interfaces: description: "Get a list of transactions for an account within a specific TransactionID range." http_method: "GET" path: "/accounts/:account_id/transactions/idrange" + response_schema: "ListTransactionsIdRange" arguments: - "account_id" parameters: @@ -324,6 +333,7 @@ interfaces: description: "Get a list of transactions for an account since a specific TransactionID." http_method: "GET" path: "/accounts/:account_id/transactions/sinceid" + response_schema: "ListTransactionsIdRange" arguments: - "account_id" - "transaction_id" @@ -338,6 +348,7 @@ interfaces: description: "Get most recently completed candles within an account for specified combinations of instrument, granularity and price component." http_method: "GET" path: "/accounts/:account_id/candles/latest" + response_schema: "LatestCandles" arguments: - "account_id" parameters: @@ -369,6 +380,7 @@ interfaces: description: "Get pricing information for a list of instruments." http_method: "GET" path: "/accounts/:account_id/pricing" + response_schema: "ListPricing" arguments: - "account_id" parameters: @@ -391,6 +403,7 @@ interfaces: description: "Fetch candlestick data for an instrument." http_method: "GET" path: "/accounts/:account_id/instruments/:instrument/candles" + response_schema: "ListCandles" arguments: - "account_id" - "instrument" diff --git a/lib/models/response/positions/list_positions.ex b/lib/models/response/positions/list_positions.ex new file mode 100644 index 0000000..ca22c39 --- /dev/null +++ b/lib/models/response/positions/list_positions.ex @@ -0,0 +1,24 @@ +defmodule ExOanda.ListPositions do + @moduledoc """ + Schema for Oanda list positions response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.Position + + @primary_key false + + typed_embedded_schema do + embeds_many :positions, Position + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:positions) + |> validate_required([:last_transaction_id]) + end +end diff --git a/lib/models/response/accounts/position.ex b/lib/models/response/positions/position.ex similarity index 100% rename from lib/models/response/accounts/position.ex rename to lib/models/response/positions/position.ex diff --git a/lib/models/response/pricing/latest_candles.ex b/lib/models/response/pricing/latest_candles.ex new file mode 100644 index 0000000..3d45805 --- /dev/null +++ b/lib/models/response/pricing/latest_candles.ex @@ -0,0 +1,21 @@ +defmodule ExOanda.LatestCandles do + @moduledoc """ + Schema for Oanda list candles response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + embeds_one :latest_candles, ExOanda.ListCandles + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, []) + |> cast_embed(:latest_candles) + end +end diff --git a/lib/models/response/pricing/list_pricing.ex b/lib/models/response/pricing/list_pricing.ex new file mode 100644 index 0000000..8539059 --- /dev/null +++ b/lib/models/response/pricing/list_pricing.ex @@ -0,0 +1,59 @@ +defmodule ExOanda.ListPricing do + @moduledoc """ + Schema for Oanda list pricing response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.PriceBucket + + @primary_key false + + typed_embedded_schema do + embeds_many :prices, ClientPrice, primary_key: false do + field(:type, :string) + field(:instrument, :string) + field(:time, :utc_datetime_usec) + field(:tradeable, :boolean) + field(:closeout_bid, :float) + field(:closeout_ask, :float) + + embeds_many :bids, PriceBucket + embeds_many :asks, PriceBucket + end + + embeds_many :home_conversions, HomeConversion, primary_key: false do + field(:currency, :string) + field(:account_gain, :float) + field(:account_loss, :float) + field(:position_value, :float) + end + + field(:time, :utc_datetime_usec) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:time]) + |> cast_embed(:prices, with: &client_price_changeset/2) + |> cast_embed(:home_conversions, with: &home_conversion_changeset/2) + |> validate_required([:time]) + end + + defp client_price_changeset(struct, params) do + struct + |> cast(params, [:type, :instrument, :time, :tradeable, :closeout_bid, :closeout_ask]) + |> cast_embed(:bids) + |> cast_embed(:asks) + |> validate_required([:type, :instrument, :time, :tradeable, :closeout_bid, :closeout_ask]) + end + + defp home_conversion_changeset(struct, params) do + struct + |> cast(params, [:currency, :account_gain, :account_loss, :position_value]) + |> validate_required([:currency, :account_gain, :account_loss, :position_value]) + end + + +end diff --git a/lib/models/response/pricing/price_bucket.ex b/lib/models/response/pricing/price_bucket.ex new file mode 100644 index 0000000..66ee757 --- /dev/null +++ b/lib/models/response/pricing/price_bucket.ex @@ -0,0 +1,22 @@ +defmodule ExOanda.PriceBucket do + @moduledoc """ + Schema for Oanda price bucket. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:price, :float) + field(:liquidity, :integer) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:price, :liquidity]) + |> validate_required([:price, :liquidity]) + end +end diff --git a/lib/models/response/trades/find_trade.ex b/lib/models/response/trades/find_trade.ex new file mode 100644 index 0000000..8c648b6 --- /dev/null +++ b/lib/models/response/trades/find_trade.ex @@ -0,0 +1,24 @@ +defmodule ExOanda.FindTrade do + @moduledoc """ + Schema for Oanda find trade response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.Trade + + @primary_key false + + typed_embedded_schema do + embeds_one :trade, Trade + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:trade) + |> validate_required([:last_transaction_id]) + end +end diff --git a/lib/models/response/trades/list_trades.ex b/lib/models/response/trades/list_trades.ex new file mode 100644 index 0000000..7910b6d --- /dev/null +++ b/lib/models/response/trades/list_trades.ex @@ -0,0 +1,26 @@ +defmodule ExOanda.ListTrades do + @moduledoc """ + Schema for Oanda list trades response. + """ + + use TypedEctoSchema + import Ecto.Changeset + # alias ExOanda.TradeOrder + alias ExOanda.Trade + + @primary_key false + + typed_embedded_schema do + embeds_many :trades, Trade + + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:trades) + |> validate_required([:last_transaction_id]) + end +end diff --git a/lib/models/response/trades/trade.ex b/lib/models/response/trades/trade.ex new file mode 100644 index 0000000..5f9b12c --- /dev/null +++ b/lib/models/response/trades/trade.ex @@ -0,0 +1,67 @@ +defmodule ExOanda.Trade do + @moduledoc """ + Schema for Oanda trade. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.TradeOrder + + @primary_key false + + typed_embedded_schema do + field(:id, :string) + field(:instrument, :string) + field(:price, :float) + field(:open_time, :utc_datetime_usec) + field(:initial_units, :integer) + field(:initial_margin_required, :float) + field(:state, Ecto.Enum, values: [:OPEN, :CLOSED, :CLOSE_WHEN_TRADEABLE]) + field(:current_units, :integer) + field(:realized_pl, :float) + field(:unrealized_pl, :float) + field(:margin_used, :float) + field(:average_close_price, :float) + field(:closing_transaction_ids, {:array, :string}, default: []) + field(:financing, :float) + field(:dividend_adjustment, :float) + field(:close_time, :utc_datetime_usec) + + embeds_one :take_profit_order, TradeOrder + embeds_one :stop_loss_order, TradeOrder + embeds_one :trailing_stop_loss_order, TradeOrder + + embeds_many :client_extensions, ClientExtensions, primary_key: false do + field(:id, :string) + field(:tag, :string) + field(:comment, :string) + end + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [ + :id, :instrument, :price, :open_time, :initial_units, + :initial_margin_required, :state, :current_units, :realized_pl, + :unrealized_pl, :margin_used, :average_close_price, :closing_transaction_ids, + :financing, :dividend_adjustment, :close_time + ]) + |> cast_embed(:take_profit_order) + |> cast_embed(:stop_loss_order) + |> cast_embed(:trailing_stop_loss_order) + |> cast_embed(:client_extensions, with: &client_extensions_changeset/2) + |> validate_required([ + :id, :instrument, :price, :open_time, :initial_units, + :initial_margin_required, :state, :current_units, :realized_pl, + :unrealized_pl, :margin_used, :average_close_price, :closing_transaction_ids, + :financing, :dividend_adjustment, :close_time + ]) + end + + defp client_extensions_changeset(struct, params) do + struct + |> cast(params, [:id, :tag, :comment]) + |> validate_required([:id]) + end +end diff --git a/lib/models/response/trades/trade_order.ex b/lib/models/response/trades/trade_order.ex new file mode 100644 index 0000000..6d7bffc --- /dev/null +++ b/lib/models/response/trades/trade_order.ex @@ -0,0 +1,64 @@ +defmodule ExOanda.TradeOrder do + @moduledoc """ + Schema for Oanda trade order response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:id, :string) + field(:create_time, :utc_datetime_usec) + field(:state, Ecto.Enum, values: [:PENDING, :FILLED, :TRIGGERED, :CANCELLED]) + field(:type, Ecto.Enum, values: [:LIMIT, :STOP, :MARKET_IF_TOUCHED, :TAKE_PROFIT]) + field(:trade_id, :string) + field(:client_trade_id, :string) + field(:price, :float) + field(:time_in_force, Ecto.Enum, values: [:GTC, :GTD, :GFD, :FOK, :IOC], default: :GTC) + field(:gtd_time, :utc_datetime_usec) + field(:trigger_condition, Ecto.Enum, values: [:DEFAULT, :INVERSE, :BID, :ASK, :MID], default: :DEFAULT) + field(:filling_transaction_id, :string) + field(:filled_time, :utc_datetime_usec) + field(:trade_opened_id, :string) + field(:trade_reduced_id, :string) + field(:trade_closed_ids, {:array, :string}, default: []) + field(:cancelling_transaction_id, :string) + field(:cancelled_time, :utc_datetime_usec) + field(:replaces_order_id, :string) + field(:replaced_by_order_id, :string) + + embeds_many :client_extensions, ClientExtensions, primary_key: false do + field(:id, :string) + field(:tag, :string) + field(:comment, :string) + end + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [ + :id, :create_time, :state, :type, :trade_id, :client_trade_id, :price, + :time_in_force, :gtd_time, :trigger_condition, :filling_transaction_id, + :filled_time, :trade_opened_id, :trade_reduced_id, :trade_closed_ids, + :cancelling_transaction_id, :cancelled_time, :replaces_order_id, + :replaced_by_order_id + ]) + |> cast_embed(:client_extensions, with: &client_extensions_changeset/2) + |> validate_required([ + :id, :create_time, :state, :type, :trade_id, :client_trade_id, :price, + :time_in_force, :gtd_time, :trigger_condition, :filling_transaction_id, + :filled_time, :trade_opened_id, :trade_reduced_id, :trade_closed_ids, + :cancelling_transaction_id, :cancelled_time, :replaces_order_id, + :replaced_by_order_id + ]) + end + + defp client_extensions_changeset(struct, params) do + struct + |> cast(params, [:id, :tag, :comment]) + |> validate_required([:id]) + end +end diff --git a/lib/models/response/transactions/find_transaction.ex b/lib/models/response/transactions/find_transaction.ex new file mode 100644 index 0000000..415f1c0 --- /dev/null +++ b/lib/models/response/transactions/find_transaction.ex @@ -0,0 +1,24 @@ +defmodule ExOanda.FindTransaction do + @moduledoc """ + Schema for Oanda find transaction response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.Transaction + + @primary_key false + + typed_embedded_schema do + embeds_one :transaction, Transaction + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:transaction) + |> validate_required([:last_transaction_id]) + end +end diff --git a/lib/models/response/transactions/list_transactions.ex b/lib/models/response/transactions/list_transactions.ex new file mode 100644 index 0000000..6365fe8 --- /dev/null +++ b/lib/models/response/transactions/list_transactions.ex @@ -0,0 +1,27 @@ +defmodule ExOanda.ListTransactions do + @moduledoc """ + Schema for Oanda list transactions response. + """ + + use TypedEctoSchema + import Ecto.Changeset + + @primary_key false + + typed_embedded_schema do + field(:from, :utc_datetime_usec) + field(:to, :utc_datetime_usec) + field(:page_size, :integer) + field(:type, {:array, :string}) + field(:count, :integer) + field(:pages, {:array, :string}) + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:from, :to, :page_size, :type, :count, :pages, :last_transaction_id]) + |> validate_required([:from, :to, :page_size, :type, :count, :pages, :last_transaction_id]) + end +end diff --git a/lib/models/response/transactions/list_transactions_id_range.ex b/lib/models/response/transactions/list_transactions_id_range.ex new file mode 100644 index 0000000..8d0610c --- /dev/null +++ b/lib/models/response/transactions/list_transactions_id_range.ex @@ -0,0 +1,24 @@ +defmodule ExOanda.ListTransactionsIdRange do + @moduledoc """ + Schema for Oanda list transactions id range response. + """ + + use TypedEctoSchema + import Ecto.Changeset + alias ExOanda.Transaction + + @primary_key false + + typed_embedded_schema do + embeds_many :transactions, Transaction + field(:last_transaction_id, :string) + end + + @doc false + def changeset(struct, params) do + struct + |> cast(params, [:last_transaction_id]) + |> cast_embed(:transactions) + |> validate_required([:last_transaction_id]) + end +end diff --git a/lib/models/response/accounts/transaction.ex b/lib/models/response/transactions/transaction.ex similarity index 100% rename from lib/models/response/accounts/transaction.ex rename to lib/models/response/transactions/transaction.ex From 26a5b6c939459e46cb53b1d00158fb7ebe19d3e4 Mon Sep 17 00:00:00 2001 From: nickbair Date: Thu, 3 Oct 2024 20:39:09 -0600 Subject: [PATCH 5/6] Model fixes --- lib/models/response/accounts/find_account.ex | 15 +++++++++++---- lib/models/response/order.ex | 2 +- lib/models/response/pricing/list_pricing.ex | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/models/response/accounts/find_account.ex b/lib/models/response/accounts/find_account.ex index 6e02595..127d0c0 100644 --- a/lib/models/response/accounts/find_account.ex +++ b/lib/models/response/accounts/find_account.ex @@ -5,6 +5,11 @@ defmodule ExOanda.FindAccount do use TypedEctoSchema import Ecto.Changeset + alias ExOanda.{ + Order, + Position, + TradeSummary + } @primary_key false @@ -52,10 +57,9 @@ defmodule ExOanda.FindAccount do field(:mutability_market_halted, Ecto.Enum, values: [:FIXED, :REPLACEABLE, :CANCELABLE, :PRICE_WIDEN_ONLY]) end - # TODO - field(:trades, {:array, :map}, primary_key: false) - field(:positions, {:array, :map}, primary_key: false) - field(:orders, {:array, :map}, primary_key: false) + embeds_many :trades, TradeSummary + embeds_many :positions, Position + embeds_many :orders, Order end field(:last_transaction_id, :string) @@ -81,6 +85,9 @@ defmodule ExOanda.FindAccount do :last_margin_call_extension_time, :last_transaction_id ]) |> cast_embed(:guaranteed_stop_loss_order_parameters, with: &guaranteed_stop_loss_order_parameters_changeset/2) + |> cast_embed(:trades) + |> cast_embed(:positions) + |> cast_embed(:orders) end defp guaranteed_stop_loss_order_parameters_changeset(struct, params) do diff --git a/lib/models/response/order.ex b/lib/models/response/order.ex index 4d9a912..f89679b 100644 --- a/lib/models/response/order.ex +++ b/lib/models/response/order.ex @@ -11,7 +11,7 @@ defmodule ExOanda.Order do typed_embedded_schema do field(:id, :string) field(:creat_time, :utc_datetime_usec) - field(:state, :string) + field(:state, Ecto.Enum, values: [:PENDING, :FILLED, :TRIGGERED, :CANCELLED]) embeds_many :client_extensions, ClientExtensions, primary_key: false do field(:id, :string) diff --git a/lib/models/response/pricing/list_pricing.ex b/lib/models/response/pricing/list_pricing.ex index 8539059..4872e6d 100644 --- a/lib/models/response/pricing/list_pricing.ex +++ b/lib/models/response/pricing/list_pricing.ex @@ -11,7 +11,7 @@ defmodule ExOanda.ListPricing do typed_embedded_schema do embeds_many :prices, ClientPrice, primary_key: false do - field(:type, :string) + field(:type, :string, default: "PRICE") field(:instrument, :string) field(:time, :utc_datetime_usec) field(:tradeable, :boolean) From d4df878e073aa34937fa1ce63d9534fb8e74ed95 Mon Sep 17 00:00:00 2001 From: nickbair Date: Fri, 4 Oct 2024 15:52:36 -0600 Subject: [PATCH 6/6] Fix convert_params/1 --- lib/code_gen/code_generator.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/code_gen/code_generator.ex b/lib/code_gen/code_generator.ex index 39ff774..0fd7c0f 100644 --- a/lib/code_gen/code_generator.ex +++ b/lib/code_gen/code_generator.ex @@ -176,6 +176,8 @@ defmodule ExOanda.CodeGenerator do |> Enum.into(%{}) |> Recase.Enumerable.convert_keys(&Recase.to_camel/1) |> Enum.map(fn {k, v} -> + k = maybe_convert_to_string(k) + case String.ends_with?(k, "Id") do true -> {String.replace(k, "Id", "ID"), v} false -> {k, v} @@ -183,6 +185,9 @@ defmodule ExOanda.CodeGenerator do end) end + def maybe_convert_to_string(val) when is_atom(val), do: Atom.to_string(val) + def maybe_convert_to_string(val), do: val + defp generate_module_name(module_name), do: Module.concat([ExOanda, module_name]) defp format_args(args) do