Skip to content

Commit 3a3e5c9

Browse files
authored
Merge pull request #468 from coding-kitties/dev
Release: merge dev into main
2 parents 188ec26 + 7950c99 commit 3a3e5c9

18 files changed

Lines changed: 1930 additions & 127 deletions

File tree

docusaurus/docs/Getting Started/orders.md

Lines changed: 158 additions & 112 deletions
Large diffs are not rendered by default.

docusaurus/docs/Getting Started/strategies.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,28 @@ order = self.create_limit_order(
357357
sync=True
358358
)
359359

360+
# Create a market order (fills at best available price)
361+
order = self.create_market_order(
362+
target_symbol="BTC",
363+
order_side=OrderSide.BUY,
364+
amount=0.01, # Amount in target symbol
365+
# OR
366+
amount_trading_symbol=500, # Amount in trading symbol (EUR)
367+
# OR
368+
percentage_of_portfolio=10, # 10% of portfolio
369+
)
370+
371+
# Convenience methods for market orders
372+
self.create_market_buy_order(
373+
target_symbol="BTC",
374+
percentage_of_portfolio=10, # Buy 10% of portfolio
375+
)
376+
377+
self.create_market_sell_order(
378+
target_symbol="BTC",
379+
percentage_of_position=50, # Sell 50% of position
380+
)
381+
360382
# Close a position entirely
361383
self.close_position(symbol="BTC")
362384
```

investing_algorithm_framework/app/context.py

Lines changed: 284 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,57 @@ def __init__(
5151
self.trade_take_profit_service: TradeTakeProfitService = \
5252
trade_take_profit_service
5353

54+
def _validate_target_symbol(self, target_symbol, market=None):
55+
"""
56+
Validate the target_symbol for order creation:
57+
1. Prevents orders where target_symbol equals trading_symbol
58+
(e.g. EUR/EUR).
59+
2. Checks that a data source exists for the
60+
target_symbol/trading_symbol combination (e.g. BTC/EUR).
61+
62+
Args:
63+
target_symbol: The symbol of the asset to trade
64+
market: The market to check against
65+
66+
Raises:
67+
OperationalException: If validation fails
68+
"""
69+
portfolio = self.portfolio_service.find({"market": market})
70+
trading_symbol = portfolio.trading_symbol
71+
72+
# Check target_symbol != trading_symbol
73+
if target_symbol.upper() == trading_symbol.upper():
74+
raise OperationalException(
75+
f"target_symbol '{target_symbol}' is the same as "
76+
f"the trading_symbol '{trading_symbol}'. "
77+
f"This would result in a "
78+
f"'{trading_symbol}/{trading_symbol}' "
79+
f"order which is not valid. "
80+
f"To skip this check, set validate_symbol=False "
81+
f"or omit the parameter."
82+
)
83+
84+
# Check that a data source is registered for this pair
85+
expected_symbol = f"{target_symbol}/{trading_symbol}".upper()
86+
known_symbols = set()
87+
88+
if self.data_provider_service.data_provider_index is not None:
89+
for data_source, _ in \
90+
self.data_provider_service \
91+
.data_provider_index.get_all():
92+
if data_source.symbol is not None:
93+
known_symbols.add(data_source.symbol.upper())
94+
95+
if expected_symbol not in known_symbols:
96+
sorted_symbols = sorted(known_symbols)
97+
raise OperationalException(
98+
f"No data source registered for '{expected_symbol}'. "
99+
f"A data source is required to track price history. "
100+
f"Registered data source symbols: {sorted_symbols}. "
101+
f"To skip this check, set validate_symbol=False "
102+
f"or omit the parameter."
103+
)
104+
54105
@property
55106
def config(self):
56107
"""
@@ -78,7 +129,8 @@ def create_order(
78129
market=None,
79130
execute=True,
80131
validate=True,
81-
sync=True
132+
sync=True,
133+
validate_symbol=False
82134
) -> Order:
83135
"""
84136
Function to create an order. This function will create an order
@@ -96,10 +148,15 @@ def create_order(
96148
validate: If set to True, the order will be validated
97149
sync: If set to True, the created order will be synced
98150
with the portfolio of the algorithm.
151+
validate_symbol: Default False. If set to True,
152+
validates that target_symbol is not the trading_symbol.
99153
100154
Returns:
101155
The order created
102156
"""
157+
if validate_symbol:
158+
self._validate_target_symbol(target_symbol, market=market)
159+
103160
portfolio = self.portfolio_service.find({"market": market})
104161
order_data = {
105162
"target_symbol": target_symbol,
@@ -162,7 +219,8 @@ def create_limit_order(
162219
execute=True,
163220
validate=True,
164221
sync=True,
165-
metadata=None
222+
metadata=None,
223+
validate_symbol=False
166224
) -> Order:
167225
"""
168226
Function to create a limit order. This function will create a limit
@@ -193,10 +251,15 @@ def create_limit_order(
193251
sync (optional): Default True. If set to True,
194252
the created order will be synced with the
195253
portfolio of the algorithm
254+
validate_symbol (optional): Default False. If set to True,
255+
validates that target_symbol is not the trading_symbol.
196256
197257
Returns:
198258
Order: Instance of the order created
199259
"""
260+
if validate_symbol:
261+
self._validate_target_symbol(target_symbol, market=market)
262+
200263
portfolio = self.portfolio_service.find({"market": market})
201264

202265
if percentage_of_portfolio is not None:
@@ -270,6 +333,225 @@ def create_limit_order(
270333
order_data, execute=execute, validate=validate, sync=sync
271334
)
272335

336+
def create_market_order(
337+
self,
338+
target_symbol,
339+
order_side,
340+
amount=None,
341+
amount_trading_symbol=None,
342+
percentage=None,
343+
percentage_of_portfolio=None,
344+
percentage_of_position=None,
345+
precision=None,
346+
market=None,
347+
execute=True,
348+
validate=True,
349+
sync=True,
350+
metadata=None
351+
) -> Order:
352+
"""
353+
Function to create a market order. Market orders execute at
354+
the best available price. In backtesting, this means the
355+
open price of the next candle (+ slippage).
356+
357+
An estimated price (current latest price) is used for amount
358+
calculation and cash reservation. The actual fill price is
359+
determined at fill time and the portfolio is reconciled.
360+
361+
Args:
362+
target_symbol: The symbol of the asset to trade
363+
order_side: The side of the order (BUY or SELL)
364+
amount (optional): The amount of the asset to trade
365+
amount_trading_symbol (optional): The amount of the
366+
trading symbol to trade
367+
percentage (optional): The percentage of the portfolio
368+
to allocate to the order
369+
percentage_of_portfolio (optional): The percentage
370+
of the portfolio to allocate to the order
371+
percentage_of_position (optional): The percentage
372+
of the position to allocate to the
373+
order. (Only supported for SELL orders)
374+
precision (optional): The precision of the amount
375+
market (optional): The market to trade the asset
376+
execute (optional): Default True. If set to True,
377+
the order will be executed
378+
validate (optional): Default True. If set to
379+
True, the order will be validated
380+
sync (optional): Default True. If set to True,
381+
the created order will be synced with the
382+
portfolio of the algorithm
383+
metadata (optional): Additional metadata for the order
384+
385+
Returns:
386+
Order: Instance of the order created
387+
"""
388+
portfolio = self.portfolio_service.find({"market": market})
389+
full_symbol = (f"{target_symbol}/{portfolio.trading_symbol}")
390+
estimated_price = self.get_latest_price(full_symbol, market=market)
391+
392+
if estimated_price is None:
393+
raise OperationalException(
394+
f"Cannot create market order for {target_symbol}: "
395+
f"no price data available to estimate order size."
396+
)
397+
398+
if percentage_of_portfolio is not None:
399+
if not OrderSide.BUY.equals(order_side):
400+
raise OperationalException(
401+
"Percentage of portfolio is only supported for BUY orders."
402+
)
403+
404+
net_size = portfolio.get_net_size()
405+
size = net_size * (percentage_of_portfolio / 100)
406+
amount = size / estimated_price
407+
408+
elif percentage_of_position is not None:
409+
410+
if not OrderSide.SELL.equals(order_side):
411+
raise OperationalException(
412+
"Percentage of position is only supported for SELL orders."
413+
)
414+
415+
position = self.position_service.find(
416+
{
417+
"symbol": target_symbol,
418+
"portfolio": portfolio.id
419+
}
420+
)
421+
amount = position.get_amount() * (percentage_of_position / 100)
422+
423+
elif percentage is not None:
424+
net_size = portfolio.get_net_size()
425+
size = net_size * (percentage / 100)
426+
amount = size / estimated_price
427+
428+
if precision is not None:
429+
amount = RoundingService.round_down(amount, precision)
430+
431+
if amount_trading_symbol is not None:
432+
amount = amount_trading_symbol / estimated_price
433+
434+
if amount is None:
435+
raise OperationalException(
436+
"The amount parameter is required to create a market order. "
437+
"Either the amount, amount_trading_symbol, percentage, "
438+
"percentage_of_portfolio or percentage_of_position "
439+
"parameter must be specified."
440+
)
441+
442+
logger.info(
443+
f"Creating market order: {target_symbol} "
444+
f"{order_side} {amount} @ estimated {estimated_price}"
445+
)
446+
447+
order_metadata = metadata if metadata is not None else {}
448+
order_metadata["estimated_price"] = estimated_price
449+
450+
order_data = {
451+
"target_symbol": target_symbol,
452+
"price": estimated_price,
453+
"amount": amount,
454+
"order_type": OrderType.MARKET.value,
455+
"order_side": OrderSide.from_value(order_side).value,
456+
"portfolio_id": portfolio.id,
457+
"status": OrderStatus.CREATED.value,
458+
"trading_symbol": portfolio.trading_symbol,
459+
"metadata": order_metadata,
460+
}
461+
462+
if BACKTESTING_FLAG in self.configuration_service.config \
463+
and self.configuration_service.config[BACKTESTING_FLAG]:
464+
order_data["created_at"] = \
465+
self.configuration_service.config[INDEX_DATETIME]
466+
467+
return self.order_service.create(
468+
order_data, execute=execute, validate=validate, sync=sync
469+
)
470+
471+
def create_market_buy_order(
472+
self,
473+
target_symbol,
474+
amount=None,
475+
percentage_of_portfolio=None,
476+
market=None,
477+
portfolio_id=None,
478+
metadata=None
479+
) -> Order:
480+
"""
481+
Function to create a market buy order.
482+
483+
Args:
484+
target_symbol (str): The symbol of the asset to buy
485+
amount (float, optional): The amount of the asset to buy
486+
percentage_of_portfolio (float, optional): The percentage of the
487+
portfolio to buy.
488+
market (str, optional): the portfolio corresponding to the market
489+
to buy the asset
490+
portfolio_id (str, optional): The ID of the portfolio to buy
491+
the asset from.
492+
metadata (dict, optional): Additional metadata for the order
493+
494+
Returns:
495+
Order: The order created
496+
"""
497+
498+
if amount is None and percentage_of_portfolio is None:
499+
raise OperationalException(
500+
"Either amount or percentage_of_portfolio must be specified "
501+
"to create a market buy order."
502+
)
503+
504+
return self.create_market_order(
505+
target_symbol=target_symbol,
506+
order_side=OrderSide.BUY,
507+
amount=amount,
508+
percentage_of_portfolio=percentage_of_portfolio,
509+
market=market,
510+
metadata=metadata
511+
)
512+
513+
def create_market_sell_order(
514+
self,
515+
target_symbol,
516+
amount=None,
517+
percentage_of_position=None,
518+
market=None,
519+
portfolio_id=None,
520+
metadata=None
521+
) -> Order:
522+
"""
523+
Function to create a market sell order.
524+
525+
Args:
526+
target_symbol (str): The symbol of the asset to sell
527+
amount (float, optional): The amount of the asset to sell
528+
percentage_of_position (float, optional): The percentage of the
529+
position to sell.
530+
market (str, optional): the portfolio corresponding to the market
531+
to sell the asset
532+
portfolio_id (str, optional): The ID of the portfolio to sell
533+
the asset from.
534+
metadata (dict, optional): Additional metadata for the order
535+
536+
Returns:
537+
Order: The order created
538+
"""
539+
540+
if amount is None and percentage_of_position is None:
541+
raise OperationalException(
542+
"Either amount or percentage_of_position must be specified "
543+
"to create a market sell order."
544+
)
545+
546+
return self.create_market_order(
547+
target_symbol=target_symbol,
548+
order_side=OrderSide.SELL,
549+
amount=amount,
550+
percentage_of_position=percentage_of_position,
551+
market=market,
552+
metadata=metadata
553+
)
554+
273555
def create_limit_sell_order(
274556
self,
275557
target_symbol,

investing_algorithm_framework/app/reporting/backtest_report.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535

3636
def _read_template(filename):
37-
with open(os.path.join(_TEMPLATE_DIR, filename), 'r') as f:
37+
with open(os.path.join(_TEMPLATE_DIR, filename), 'r', encoding='utf-8') as f:
3838
return f.read()
3939

4040

@@ -186,7 +186,7 @@ def show(self, backtest_date_range=None, browser=False):
186186
self.html_report = self._build_html()
187187

188188
path = "/tmp/backtest_report.html"
189-
with open(path, "w") as f:
189+
with open(path, "w", encoding="utf-8") as f:
190190
f.write(self.html_report)
191191

192192
if browser:
@@ -208,7 +208,7 @@ def show(self, backtest_date_range=None, browser=False):
208208
def save(self, path):
209209
if not self.html_report:
210210
self.html_report = self._build_html()
211-
with open(path, "w") as f:
211+
with open(path, "w", encoding="utf-8") as f:
212212
f.write(self.html_report)
213213

214214
@staticmethod

0 commit comments

Comments
 (0)