Skip to content

Commit c142cc3

Browse files
authored
Add backtest csv report (#251)
* Add backtest csv report * Fix flake8 warnings
1 parent 6af2e23 commit c142cc3

File tree

25 files changed

+801
-327
lines changed

25 files changed

+801
-327
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,5 @@ lib/
145145
bumpversion.egg-info/
146146
*.sqlite3
147147

148-
*/backtest_data/
148+
*/backtest_data/
149+
*/backtest_reports/

investing_algorithm_framework/app/algorithm.py

Lines changed: 58 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import logging
21
import decimal
2+
import logging
33
from typing import List
44

55
from investing_algorithm_framework.domain import OrderStatus, OrderFee, \
66
Position, Order, Portfolio, OrderType, OrderSide, ApiException, \
7-
BACKTESTING_FLAG, BACKTESTING_INDEX_DATETIME, Trade, PeekableQueue, \
8-
MarketService
7+
BACKTESTING_FLAG, BACKTESTING_INDEX_DATETIME, MarketService
98
from investing_algorithm_framework.services import MarketCredentialService, \
10-
MarketDataSourceService
9+
MarketDataSourceService, PortfolioService, PositionService, TradeService, \
10+
OrderService, ConfigurationService, StrategyOrchestratorService, \
11+
PortfolioConfigurationService
1112

1213
logger = logging.getLogger("investing_algorithm_framework")
1314

@@ -24,21 +25,26 @@ def __init__(
2425
market_service,
2526
strategy_orchestrator_service,
2627
market_credential_service,
27-
market_data_source_service
28+
market_data_source_service,
29+
trade_service
2830
):
29-
self.portfolio_service = portfolio_service
30-
self.position_service = position_service
31-
self.order_service = order_service
32-
self._market_service: MarketService = market_service
33-
self.configuration_service = configuration_service
34-
self.portfolio_configuration_service = portfolio_configuration_service
35-
self.strategy_orchestrator_service = strategy_orchestrator_service
31+
self.portfolio_service: PortfolioService = portfolio_service
32+
self.position_service: PositionService = position_service
33+
self.order_service: OrderService = order_service
34+
self.market_service: MarketService = market_service
35+
self.configuration_service: ConfigurationService \
36+
= configuration_service
37+
self.portfolio_configuration_service: PortfolioConfigurationService \
38+
= portfolio_configuration_service
39+
self.strategy_orchestrator_service: StrategyOrchestratorService \
40+
= strategy_orchestrator_service
3641
self._market_data_sources = {}
3742
self._strategies = []
3843
self._market_credential_service: MarketCredentialService \
3944
= market_credential_service
4045
self._market_data_source_service: MarketDataSourceService \
4146
= market_data_source_service
47+
self.trade_service: TradeService = trade_service
4248

4349
def start(self, number_of_iterations=None, stateless=False):
4450

@@ -158,14 +164,14 @@ def create_limit_order(
158164
)
159165

160166
def create_market_order(
161-
self,
162-
target_symbol,
163-
order_side,
164-
amount,
165-
market=None,
166-
execute=False,
167-
validate=False,
168-
sync=True
167+
self,
168+
target_symbol,
169+
order_side,
170+
amount,
171+
market=None,
172+
execute=False,
173+
validate=False,
174+
sync=True
169175
):
170176

171177
if market is None:
@@ -221,13 +227,13 @@ def reset(self):
221227
self._running_workers = []
222228

223229
def get_order(
224-
self,
225-
reference_id=None,
226-
market=None,
227-
target_symbol=None,
228-
trading_symbol=None,
229-
order_side=None,
230-
order_type=None
230+
self,
231+
reference_id=None,
232+
market=None,
233+
target_symbol=None,
234+
trading_symbol=None,
235+
order_side=None,
236+
order_type=None
231237
) -> Order:
232238
query_params = {}
233239

@@ -256,12 +262,12 @@ def get_order(
256262
return self.order_service.find(query_params)
257263

258264
def get_orders(
259-
self,
260-
target_symbol=None,
261-
status=None,
262-
order_type=None,
263-
order_side=None,
264-
market=None
265+
self,
266+
target_symbol=None,
267+
status=None,
268+
order_type=None,
269+
order_side=None,
270+
market=None
265271
) -> List[Order]:
266272

267273
if market is None:
@@ -284,13 +290,13 @@ def get_order_fee(self, order_id) -> OrderFee:
284290
return self.order_service.get_order_fee(order_id)
285291

286292
def get_positions(
287-
self,
288-
market=None,
289-
identifier=None,
290-
amount_gt=None,
291-
amount_gte=None,
292-
amount_lt=None,
293-
amount_lte=None
293+
self,
294+
market=None,
295+
identifier=None,
296+
amount_gt=None,
297+
amount_gte=None,
298+
amount_lt=None,
299+
amount_lte=None
294300
) -> List[Position]:
295301
query_params = {}
296302

@@ -346,14 +352,14 @@ def get_position(self, symbol, market=None, identifier=None) -> Position:
346352
return None
347353

348354
def has_position(
349-
self,
350-
symbol,
351-
market=None,
352-
identifier=None,
353-
amount_gt=0,
354-
amount_gte=None,
355-
amount_lt=None,
356-
amount_lte=None
355+
self,
356+
symbol,
357+
market=None,
358+
identifier=None,
359+
amount_gt=0,
360+
amount_gte=None,
361+
amount_lt=None,
362+
amount_lte=None
357363
):
358364
return self.position_exists(
359365
symbol,
@@ -675,55 +681,10 @@ def check_pending_orders(self):
675681
self.order_service.check_pending_orders()
676682

677683
def get_trades(self, market=None):
678-
portfolios = self.portfolio_service.get_all()
679-
trades = []
680-
681-
for portfolio in portfolios:
682-
buy_orders = self.order_service.get_all({
683-
"status": OrderStatus.CLOSED.value,
684-
"order_side": OrderSide.BUY.value,
685-
"portfolio_id": portfolio.id
686-
})
687-
688-
for buy_order in buy_orders:
689-
symbol = buy_order.get_symbol()
690-
ticker = self._market_data_source_service.get_ticker(
691-
symbol=symbol, market=market
692-
)
693-
trades.append(
694-
Trade(
695-
buy_order_id=buy_order.id,
696-
target_symbol=buy_order.get_target_symbol(),
697-
trading_symbol=buy_order.get_trading_symbol(),
698-
amount=buy_order.get_amount(),
699-
open_price=buy_order.get_price(),
700-
closed_price=buy_order.get_trade_closed_price(),
701-
closed_at=buy_order.get_trade_closed_at(),
702-
opened_at=buy_order.get_created_at(),
703-
current_price=ticker["bid"]
704-
)
705-
)
706-
707-
return trades
684+
return self.trade_service.get_trades(market)
708685

709686
def get_closed_trades(self):
710-
buy_orders = self.order_service.get_all({
711-
"status": OrderStatus.CLOSED.value,
712-
"order_side": OrderSide.BUY.value
713-
})
714-
return [
715-
Trade(
716-
buy_order_id=order.id,
717-
target_symbol=order.get_target_symbol(),
718-
trading_symbol=order.get_trading_symbol(),
719-
amount=order.get_amount(),
720-
open_price=order.get_price(),
721-
closed_price=order.get_trade_closed_price(),
722-
closed_at=order.get_trade_closed_at(),
723-
opened_at=order.get_created_at()
724-
) for order in buy_orders
725-
if order.get_trade_closed_at() is not None
726-
]
687+
return self.trade_service.get_closed_trades()
727688

728689
def round_down(self, value, amount_of_decimals):
729690

@@ -743,140 +704,10 @@ def count_decimals(self, number):
743704
return 0
744705

745706
def get_open_trades(self, target_symbol=None, market=None):
746-
portfolios = self.portfolio_service.get_all()
747-
trades = []
748-
749-
for portfolio in portfolios:
750-
751-
if target_symbol is not None:
752-
buy_orders = self.order_service.get_all({
753-
"status": OrderStatus.CLOSED.value,
754-
"order_side": OrderSide.BUY.value,
755-
"portfolio_id": portfolio.id,
756-
"target_symbol": target_symbol
757-
})
758-
sell_orders = self.order_service.get_all({
759-
"status": OrderStatus.OPEN.value,
760-
"order_side": OrderSide.SELL.value,
761-
"portfolio_id": portfolio.id,
762-
"target_symbol": target_symbol
763-
})
764-
else:
765-
buy_orders = self.order_service.get_all({
766-
"status": OrderStatus.CLOSED.value,
767-
"order_side": OrderSide.BUY.value,
768-
"portfolio_id": portfolio.id
769-
})
770-
sell_orders = self.order_service.get_all({
771-
"status": OrderStatus.OPEN.value,
772-
"order_side": OrderSide.SELL.value,
773-
"portfolio_id": portfolio.id
774-
})
775-
776-
buy_orders = [
777-
buy_order for buy_order in buy_orders
778-
if buy_order.get_trade_closed_at() is None
779-
]
780-
sell_amount = sum([order.amount for order in sell_orders])
781-
782-
# Subtract the amount of the open sell orders
783-
# from the amount of the buy orders
784-
buy_orders_queue = PeekableQueue()
785-
786-
for buy_order in buy_orders:
787-
buy_orders_queue.enqueue(buy_order)
788-
789-
while sell_amount > 0 and not buy_orders_queue.is_empty():
790-
first_buy_order = buy_orders_queue.peek()
791-
available = first_buy_order.get_filled() \
792-
- first_buy_order.get_trade_closed_amount()
793-
794-
if available > sell_amount:
795-
remaining = available - sell_amount
796-
sell_amount = 0
797-
first_buy_order.set_filled(remaining)
798-
else:
799-
sell_amount = sell_amount - available
800-
buy_orders_queue.dequeue()
801-
802-
for buy_order in buy_orders_queue:
803-
symbol = buy_order.get_symbol()
804-
805-
try:
806-
ticker = self._market_data_source_service.get_ticker(
807-
symbol=symbol, market=market
808-
)
809-
except Exception as e:
810-
logger.error(e)
811-
raise ApiException(
812-
f"Error getting ticker data for "
813-
f"trade {buy_order.get_target_symbol()}"
814-
f"-{buy_order.get_trading_symbol()}. Make sure you "
815-
f"have registered a ticker market data source for "
816-
f"{buy_order.get_target_symbol()}"
817-
f"-{buy_order.get_trading_symbol()} "
818-
f"for market {portfolio.market}"
819-
)
820-
821-
amount = buy_order.get_filled()
822-
closed_amount = buy_order.get_trade_closed_amount()
823-
824-
if closed_amount is not None:
825-
amount = amount - closed_amount
826-
827-
trades.append(
828-
Trade(
829-
buy_order_id=buy_order.id,
830-
target_symbol=buy_order.get_target_symbol(),
831-
trading_symbol=buy_order.get_trading_symbol(),
832-
amount=amount,
833-
open_price=buy_order.get_price(),
834-
opened_at=buy_order.get_created_at(),
835-
current_price=ticker["bid"]
836-
)
837-
)
838-
839-
return trades
707+
return self.trade_service.get_open_trades(target_symbol, market)
840708

841709
def close_trade(self, trade, market=None):
842-
843-
if trade.closed_at is not None:
844-
raise ApiException("Trade already closed.")
845-
846-
order = self.order_service.get(trade.buy_order_id)
847-
848-
if order.get_filled() <= 0:
849-
raise ApiException(
850-
"Buy order belonging to the trade has no amount."
851-
)
852-
853-
portfolio = self.portfolio_service\
854-
.find({"position": order.position_id})
855-
position = self.position_service.find(
856-
{"portfolio": portfolio.id, "symbol": order.get_target_symbol()}
857-
)
858-
amount = order.get_amount()
859-
860-
if position.get_amount() < amount:
861-
logger.warning(
862-
f"Order amount {amount} is larger then amount "
863-
f"of available {position.symbol} "
864-
f"position: {position.get_amount()}, "
865-
f"changing order amount to size of position"
866-
)
867-
amount = position.get_amount()
868-
869-
symbol = f"{order.get_target_symbol().upper()}" \
870-
f"/{order.get_trading_symbol().upper()}"
871-
ticker = self._market_data_source_service.get_ticker(
872-
symbol=symbol, market=market
873-
)
874-
self.create_limit_order(
875-
target_symbol=order.target_symbol,
876-
amount=amount,
877-
order_side=OrderSide.SELL.value,
878-
price=ticker["bid"],
879-
)
710+
self.trade_service.close_trade(trade, market)
880711

881712
def get_number_of_positions(self):
882713
"""

0 commit comments

Comments
 (0)