Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 158 additions & 112 deletions docusaurus/docs/Getting Started/orders.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions docusaurus/docs/Getting Started/strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,28 @@ order = self.create_limit_order(
sync=True
)

# Create a market order (fills at best available price)
order = self.create_market_order(
target_symbol="BTC",
order_side=OrderSide.BUY,
amount=0.01, # Amount in target symbol
# OR
amount_trading_symbol=500, # Amount in trading symbol (EUR)
# OR
percentage_of_portfolio=10, # 10% of portfolio
)

# Convenience methods for market orders
self.create_market_buy_order(
target_symbol="BTC",
percentage_of_portfolio=10, # Buy 10% of portfolio
)

self.create_market_sell_order(
target_symbol="BTC",
percentage_of_position=50, # Sell 50% of position
)

# Close a position entirely
self.close_position(symbol="BTC")
```
Expand Down
219 changes: 219 additions & 0 deletions investing_algorithm_framework/app/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,225 @@ def create_limit_order(
order_data, execute=execute, validate=validate, sync=sync
)

def create_market_order(
self,
target_symbol,
order_side,
amount=None,
amount_trading_symbol=None,
percentage=None,
percentage_of_portfolio=None,
percentage_of_position=None,
precision=None,
market=None,
execute=True,
validate=True,
sync=True,
metadata=None
) -> Order:
"""
Function to create a market order. Market orders execute at
the best available price. In backtesting, this means the
open price of the next candle (+ slippage).

An estimated price (current latest price) is used for amount
calculation and cash reservation. The actual fill price is
determined at fill time and the portfolio is reconciled.

Args:
target_symbol: The symbol of the asset to trade
order_side: The side of the order (BUY or SELL)
amount (optional): The amount of the asset to trade
amount_trading_symbol (optional): The amount of the
trading symbol to trade
percentage (optional): The percentage of the portfolio
to allocate to the order
percentage_of_portfolio (optional): The percentage
of the portfolio to allocate to the order
percentage_of_position (optional): The percentage
of the position to allocate to the
order. (Only supported for SELL orders)
precision (optional): The precision of the amount
market (optional): The market to trade the asset
execute (optional): Default True. If set to True,
the order will be executed
validate (optional): Default True. If set to
True, the order will be validated
sync (optional): Default True. If set to True,
the created order will be synced with the
portfolio of the algorithm
metadata (optional): Additional metadata for the order

Returns:
Order: Instance of the order created
"""
portfolio = self.portfolio_service.find({"market": market})
full_symbol = (f"{target_symbol}/{portfolio.trading_symbol}")
estimated_price = self.get_latest_price(full_symbol, market=market)

if estimated_price is None:
raise OperationalException(
f"Cannot create market order for {target_symbol}: "
f"no price data available to estimate order size."
)

if percentage_of_portfolio is not None:
if not OrderSide.BUY.equals(order_side):
raise OperationalException(
"Percentage of portfolio is only supported for BUY orders."
)

net_size = portfolio.get_net_size()
size = net_size * (percentage_of_portfolio / 100)
amount = size / estimated_price

elif percentage_of_position is not None:

if not OrderSide.SELL.equals(order_side):
raise OperationalException(
"Percentage of position is only supported for SELL orders."
)

position = self.position_service.find(
{
"symbol": target_symbol,
"portfolio": portfolio.id
}
)
amount = position.get_amount() * (percentage_of_position / 100)

elif percentage is not None:
net_size = portfolio.get_net_size()
size = net_size * (percentage / 100)
amount = size / estimated_price

if precision is not None:
amount = RoundingService.round_down(amount, precision)

if amount_trading_symbol is not None:
amount = amount_trading_symbol / estimated_price

if amount is None:
raise OperationalException(
"The amount parameter is required to create a market order. "
"Either the amount, amount_trading_symbol, percentage, "
"percentage_of_portfolio or percentage_of_position "
"parameter must be specified."
)

logger.info(
f"Creating market order: {target_symbol} "
f"{order_side} {amount} @ estimated {estimated_price}"
)

order_metadata = metadata if metadata is not None else {}
order_metadata["estimated_price"] = estimated_price

order_data = {
"target_symbol": target_symbol,
"price": estimated_price,
"amount": amount,
"order_type": OrderType.MARKET.value,
"order_side": OrderSide.from_value(order_side).value,
"portfolio_id": portfolio.id,
"status": OrderStatus.CREATED.value,
"trading_symbol": portfolio.trading_symbol,
"metadata": order_metadata,
}

if BACKTESTING_FLAG in self.configuration_service.config \
and self.configuration_service.config[BACKTESTING_FLAG]:
order_data["created_at"] = \
self.configuration_service.config[INDEX_DATETIME]

return self.order_service.create(
order_data, execute=execute, validate=validate, sync=sync
)

def create_market_buy_order(
self,
target_symbol,
amount=None,
percentage_of_portfolio=None,
market=None,
portfolio_id=None,
metadata=None
) -> Order:
"""
Function to create a market buy order.

Args:
target_symbol (str): The symbol of the asset to buy
amount (float, optional): The amount of the asset to buy
percentage_of_portfolio (float, optional): The percentage of the
portfolio to buy.
market (str, optional): the portfolio corresponding to the market
to buy the asset
portfolio_id (str, optional): The ID of the portfolio to buy
the asset from.
metadata (dict, optional): Additional metadata for the order

Returns:
Order: The order created
"""

if amount is None and percentage_of_portfolio is None:
raise OperationalException(
"Either amount or percentage_of_portfolio must be specified "
"to create a market buy order."
)

return self.create_market_order(
target_symbol=target_symbol,
order_side=OrderSide.BUY,
amount=amount,
percentage_of_portfolio=percentage_of_portfolio,
market=market,
metadata=metadata
)

def create_market_sell_order(
self,
target_symbol,
amount=None,
percentage_of_position=None,
market=None,
portfolio_id=None,
metadata=None
) -> Order:
"""
Function to create a market sell order.

Args:
target_symbol (str): The symbol of the asset to sell
amount (float, optional): The amount of the asset to sell
percentage_of_position (float, optional): The percentage of the
position to sell.
market (str, optional): the portfolio corresponding to the market
to sell the asset
portfolio_id (str, optional): The ID of the portfolio to sell
the asset from.
metadata (dict, optional): Additional metadata for the order

Returns:
Order: The order created
"""

if amount is None and percentage_of_position is None:
raise OperationalException(
"Either amount or percentage_of_position must be specified "
"to create a market sell order."
)

return self.create_market_order(
target_symbol=target_symbol,
order_side=OrderSide.SELL,
amount=amount,
percentage_of_position=percentage_of_position,
market=market,
metadata=metadata
)

def create_limit_sell_order(
self,
target_symbol,
Expand Down
63 changes: 63 additions & 0 deletions investing_algorithm_framework/app/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,69 @@ def create_limit_order(
metadata=metadata
)

def create_market_order(
self,
target_symbol,
order_side,
amount=None,
amount_trading_symbol=None,
percentage=None,
percentage_of_portfolio=None,
percentage_of_position=None,
precision=None,
market=None,
execute=True,
validate=True,
sync=True,
metadata=None
) -> Order:
"""
Function to create a market order. Market orders execute at
the best available price. In backtesting, this means the
open price of the next candle (+ slippage).

Args:
target_symbol: The symbol of the asset to trade
order_side: The side of the order (BUY or SELL)
amount (optional): The amount of the asset to trade
amount_trading_symbol (optional): The amount of the trading
symbol to trade
percentage (optional): The percentage of the portfolio to
allocate to the order
percentage_of_portfolio (optional): The percentage of
the portfolio to allocate to the order
percentage_of_position (optional): The percentage of
the position to allocate to the order.
(Only supported for SELL orders)
precision (optional): The precision of the amount
market (optional): The market to trade the asset
execute (optional): Default True. If set to True, the order
will be executed
validate (optional): Default True. If set to True, the order
will be validated
sync (optional): Default True. If set to True, the created
order will be synced with the portfolio of the context
metadata (optional): Additional metadata for the order

Returns:
Order: Instance of the order created
"""
return self.context.create_market_order(
target_symbol=target_symbol,
order_side=order_side,
amount=amount,
amount_trading_symbol=amount_trading_symbol,
percentage=percentage,
percentage_of_portfolio=percentage_of_portfolio,
percentage_of_position=percentage_of_position,
precision=precision,
market=market,
execute=execute,
validate=validate,
sync=sync,
metadata=metadata
)

def close_position(
self, symbol, market=None, identifier=None, precision=None
) -> Order:
Expand Down
22 changes: 19 additions & 3 deletions investing_algorithm_framework/domain/models/order/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,28 @@ def __repr__(self):
updated_at=self.get_updated_at(),
)

@property
def estimated_price(self):
"""Get the estimated price stored in metadata (used for market
orders to track the price estimate at creation time)."""
return self.metadata.get("estimated_price")

@estimated_price.setter
def estimated_price(self, value):
self.metadata["estimated_price"] = value

def get_size(self):
"""
Get the size of the order
Get the size of the order. For market orders with an estimated
price, uses the estimated price for size calculation.

Returns:
float: The size of the order
"""
return self.get_amount() * self.get_price() \
if self.get_price() is not None else 0
price = self.get_price()

if price is None or price == 0:
# Fall back to estimated_price for market orders
price = self.estimated_price

return self.get_amount() * price if price is not None else 0
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

class OrderType(Enum):
LIMIT = 'LIMIT'
MARKET = 'MARKET'

@staticmethod
def from_string(value: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ def execute_order(self, portfolio, order, market_credential) -> Order:
external_order = exchange.createLimitSellOrder(
symbol, amount, price,
)
elif OrderType.MARKET.equals(order_type):
if OrderSide.BUY.equals(order_side):

if not hasattr(exchange, "createMarketBuyOrder"):
raise OperationalException(
f"Exchange {market} does not support "
f"functionality createMarketBuyOrder"
)

external_order = exchange.createMarketBuyOrder(
symbol, amount,
)
else:

if not hasattr(exchange, "createMarketSellOrder"):
raise OperationalException(
f"Exchange {market} does not support "
f"functionality createMarketSellOrder"
)

external_order = exchange.createMarketSellOrder(
symbol, amount,
)
else:
raise OperationalException(
f"Order type {order_type} not supported "
Expand Down
Loading
Loading