Current section
Files
Jump to
Current section
Files
lib/ccxt/order/builder.ex
defmodule CCXT.Order.Builder do
@moduledoc """
Fluent API for building and submitting orders.
Provides a chainable interface for constructing orders with optional
stop-loss, take-profit, and other parameters.
## Example
alias CCXT.Order.Builder
# Build and submit a limit order with stop-loss and take-profit
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.limit(50_000.0)
|> Builder.stop_loss(48_000.0)
|> Builder.take_profit(55_000.0)
|> Builder.client_id("my-order-123")
|> Builder.submit(CCXT.Bybit, credentials)
# Simple market order
Builder.new("ETH/USDT", :sell, 1.0)
|> Builder.submit(CCXT.Binance, credentials)
## How It Works
The builder accumulates configuration in a struct, then calls the
exchange module's `create_order/7` function when `submit/3` is called.
Stop-loss and take-profit are passed through the `params` option,
letting the exchange's param_mappings handle exchange-specific naming.
"""
@type side :: :buy | :sell
@type order_type :: :market | :limit
@type t :: %__MODULE__{
symbol: String.t(),
side: side(),
amount: number(),
order_type: order_type(),
price: number() | nil,
stop_loss: number() | nil,
take_profit: number() | nil,
client_order_id: String.t() | nil,
params: keyword()
}
defstruct [
:symbol,
:side,
:amount,
:price,
:stop_loss,
:take_profit,
:client_order_id,
order_type: :market,
params: []
]
@doc """
Creates a new order builder with the required fields.
Defaults to a market order. Use `limit/2` to change to a limit order.
## Parameters
- `symbol` - Trading pair in unified format (e.g., "BTC/USDT")
- `side` - `:buy` or `:sell`
- `amount` - Order quantity
## Example
Builder.new("BTC/USDT", :buy, 0.1)
"""
@spec new(String.t(), side(), number()) :: t()
def new(symbol, side, amount) when side in [:buy, :sell] and is_number(amount) do
%__MODULE__{
symbol: symbol,
side: side,
amount: amount
}
end
@doc """
Converts the order to a limit order with the specified price.
## Example
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.limit(50_000.0)
"""
@spec limit(t(), number()) :: t()
def limit(%__MODULE__{} = builder, price) when is_number(price) and price > 0 do
%{builder | order_type: :limit, price: price}
end
@doc """
Sets a stop-loss price for the order.
The stop-loss is passed through the `params` option. Exchange-specific
parameter naming is handled by the exchange's param_mappings.
## Example
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.stop_loss(48_000.0)
"""
@spec stop_loss(t(), number()) :: t()
def stop_loss(%__MODULE__{} = builder, price) when is_number(price) and price > 0 do
%{builder | stop_loss: price}
end
@doc """
Sets a take-profit price for the order.
The take-profit is passed through the `params` option. Exchange-specific
parameter naming is handled by the exchange's param_mappings.
## Example
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.take_profit(55_000.0)
"""
@spec take_profit(t(), number()) :: t()
def take_profit(%__MODULE__{} = builder, price) when is_number(price) and price > 0 do
%{builder | take_profit: price}
end
@doc """
Sets a client order ID for the order.
Client order IDs allow you to track orders with your own identifiers.
## Example
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.client_id("my-order-123")
"""
@spec client_id(t(), String.t()) :: t()
def client_id(%__MODULE__{} = builder, id) when is_binary(id) do
%{builder | client_order_id: id}
end
@doc """
Merges additional parameters into the order.
Use this for exchange-specific parameters not covered by the builder.
## Example
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.params(timeInForce: "GTC", reduceOnly: true)
"""
@spec params(t(), keyword()) :: t()
def params(%__MODULE__{} = builder, extra_params) when is_list(extra_params) do
merged = Keyword.merge(builder.params, extra_params)
%{builder | params: merged}
end
@doc """
Submits the order to the exchange.
Calls the exchange module's `create_order` function with the accumulated
configuration.
## Parameters
- `builder` - The order builder struct
- `exchange_module` - The exchange module (e.g., `CCXT.Bybit`)
- `credentials` - `CCXT.Credentials` struct for authentication
## Returns
- `{:ok, order}` - The created order
- `{:error, error}` - An error occurred
## Example
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.limit(50_000.0)
|> Builder.submit(CCXT.Bybit, credentials)
"""
@spec submit(t(), module(), CCXT.Credentials.t()) :: {:ok, map()} | {:error, CCXT.Error.t()}
def submit(%__MODULE__{} = builder, exchange_module, credentials) do
# Build the params keyword list with SL/TP and extra params
opts = build_opts(builder)
# Call create_order with the accumulated configuration
# Signature: create_order(credentials, symbol, type, side, amount, price \\ nil, opts \\ [])
exchange_module.create_order(
credentials,
builder.symbol,
Atom.to_string(builder.order_type),
Atom.to_string(builder.side),
builder.amount,
builder.price,
opts
)
end
@doc """
Returns the order configuration as a map (for inspection/debugging).
## Example
Builder.new("BTC/USDT", :buy, 0.1)
|> Builder.limit(50_000.0)
|> Builder.to_map()
# => %{symbol: "BTC/USDT", side: :buy, amount: 0.1, ...}
"""
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = builder) do
Map.from_struct(builder)
end
# Build the opts keyword list for create_order
@doc false
defp build_opts(%__MODULE__{} = builder) do
# Collect SL/TP/clientOrderId, filter nils, merge with user params
# Keys use camelCase to match exchange API naming conventions (stopLoss, takeProfit, etc.)
params =
[
{:stopLoss, builder.stop_loss},
{:takeProfit, builder.take_profit},
{:clientOrderId, builder.client_order_id}
]
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Keyword.merge(builder.params)
case params do
[] -> []
params -> [params: params]
end
end
end