From 2ccdcde0c19d6d2c722b5e5b36511f58d37ddabb Mon Sep 17 00:00:00 2001 From: Marc van Duyn Date: Mon, 9 Oct 2023 19:14:03 +0200 Subject: [PATCH 1/3] Fix/position costs (#245) * Refactor position costs * Bump version --- examples/app.py | 11 +- .../app/algorithm.py | 8 +- .../dependency_container.py | 13 +- .../domain/__init__.py | 3 +- .../domain/models/__init__.py | 3 +- .../domain/models/order/order.py | 33 +++- .../domain/models/position/__init__.py | 3 +- .../domain/models/position/position.py | 8 + .../infrastructure/__init__.py | 7 +- .../infrastructure/models/__init__.py | 4 +- .../infrastructure/models/order/order.py | 6 + .../models/position/__init__.py | 3 +- .../models/position/position.py | 11 +- .../models/position/position_cost.py | 41 ---- .../infrastructure/repositories/__init__.py | 2 - .../repositories/position_cost_repository.py | 16 -- .../services/__init__.py | 2 - .../services/order_service.py | 177 ++++++------------ .../test_create_market_sell_order.py | 2 +- .../algorithm/test_has_open_sell_orders.py | 4 +- tests/services/test_order_service.py | 116 ++++++++++++ version.py | 2 +- 22 files changed, 242 insertions(+), 233 deletions(-) delete mode 100644 investing_algorithm_framework/infrastructure/models/position/position_cost.py delete mode 100644 investing_algorithm_framework/infrastructure/repositories/position_cost_repository.py diff --git a/examples/app.py b/examples/app.py index 4a7bae27..bac4506a 100644 --- a/examples/app.py +++ b/examples/app.py @@ -1,7 +1,7 @@ import pathlib from investing_algorithm_framework import create_app, PortfolioConfiguration, \ - RESOURCE_DIRECTORY, TimeUnit, TradingDataType, TradingTimeFrame + RESOURCE_DIRECTORY, TimeUnit, TradingDataType, TradingTimeFrame, Algorithm from datetime import datetime, timedelta app = create_app({RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve()}) @@ -10,7 +10,8 @@ market="", api_key="", secret_key="", - trading_symbol="" + trading_symbol="", + ) ) @@ -24,7 +25,7 @@ symbols=["BTC/EUR"], trading_time_frame_start_date=datetime.now() - timedelta(days=60), ) -def perform_strategy(algorithm, market_data): +def perform_strategy(algorithm: Algorithm, market_data): print(algorithm.get_portfolio()) print(algorithm.get_positions()) print(algorithm.get_orders()) @@ -34,9 +35,9 @@ def perform_strategy(algorithm, market_data): algorithm.close_position("") else: algorithm.create_limit_order( - symbol="", + target_symbol="", side="buy", - percentage_portfolio=20, + percentage_of_portfolio=20, price=market_data["tickers"][""]["bid"] ) diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py index 626f218c..7fadf2ec 100644 --- a/investing_algorithm_framework/app/algorithm.py +++ b/investing_algorithm_framework/app/algorithm.py @@ -5,7 +5,7 @@ from investing_algorithm_framework.domain import OrderStatus, OrderFee, \ Position, Order, Portfolio, OrderType, OrderSide, ApiException, \ - parse_decimal_to_string, PositionCost + parse_decimal_to_string logger = logging.getLogger("investing_algorithm_framework") @@ -17,7 +17,6 @@ def __init__( portfolio_configuration_service, portfolio_service, position_service, - position_cost_service, order_service, market_service, market_data_service, @@ -25,7 +24,6 @@ def __init__( ): self.portfolio_service = portfolio_service self.position_service = position_service - self.position_cost_service = position_cost_service self.order_service = order_service self.market_service = market_service self._config = None @@ -552,9 +550,5 @@ def has_open_orders(self, target_symbol, identifier=None, market=None): query_params["status"] = OrderStatus.OPEN.value return self.order_service.exists(query_params) - def get_position_costs(self, symbol, market=None, identifier=None) -> List[PositionCost]: - position = self.get_position(symbol, market, identifier) - return self.position_cost_service.get_all({"position": position.id, "itemized": True}) - def check_pending_orders(self): self.order_service.check_pending_orders() diff --git a/investing_algorithm_framework/dependency_container.py b/investing_algorithm_framework/dependency_container.py index 7b536a12..f6a24c88 100644 --- a/investing_algorithm_framework/dependency_container.py +++ b/investing_algorithm_framework/dependency_container.py @@ -1,12 +1,12 @@ from dependency_injector import containers, providers +from investing_algorithm_framework.app.algorithm import Algorithm from investing_algorithm_framework.infrastructure import SQLOrderRepository, \ SQLPositionRepository, MarketService, SQLPortfolioRepository, \ - SQLPositionCostRepository, SQLOrderFeeRepository + SQLOrderFeeRepository from investing_algorithm_framework.services import OrderService, \ PositionService, PortfolioService, StrategyOrchestratorService, \ - PortfolioConfigurationService, MarketDataService, PositionCostService -from investing_algorithm_framework.app.algorithm import Algorithm + PortfolioConfigurationService, MarketDataService def setup_dependency_container(app, modules=None, packages=None): @@ -21,7 +21,6 @@ class DependencyContainer(containers.DeclarativeContainer): order_repository = providers.Factory(SQLOrderRepository) order_fee_repository = providers.Factory(SQLOrderFeeRepository) position_repository = providers.Factory(SQLPositionRepository) - position_cost_repository = providers.Factory(SQLPositionCostRepository) portfolio_repository = providers.Factory(SQLPortfolioRepository) market_service = providers.Factory(MarketService) market_data_service = providers.Factory( @@ -36,7 +35,6 @@ class DependencyContainer(containers.DeclarativeContainer): ) order_service = providers.Factory( OrderService, - position_cost_repository=position_cost_repository, order_repository=order_repository, order_fee_repository=order_fee_repository, portfolio_repository=portfolio_repository, @@ -44,10 +42,6 @@ class DependencyContainer(containers.DeclarativeContainer): market_service=market_service, portfolio_configuration_service=portfolio_configuration_service, ) - position_cost_service = providers.Factory( - PositionCostService, - repository=position_cost_repository - ) position_service = providers.Factory( PositionService, repository=position_repository, @@ -75,5 +69,4 @@ class DependencyContainer(containers.DeclarativeContainer): market_service=market_service, strategy_orchestrator_service=strategy_orchestrator_service, market_data_service=market_data_service, - position_cost_service=position_cost_service, ) diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py index 357e58e5..65bd1186 100644 --- a/investing_algorithm_framework/domain/__init__.py +++ b/investing_algorithm_framework/domain/__init__.py @@ -2,7 +2,7 @@ from .models import OrderStatus, OrderSide, OrderType, TimeInterval, \ TimeUnit, TimeFrame, TradingTimeFrame, TradingDataType, Ticker, \ OHLCV, OrderBook, PortfolioConfiguration, AssetPrice, Portfolio, \ - Position, Order, PositionCost, OrderFee + Position, Order, OrderFee from .exceptions import OperationalException, ApiException, \ PermissionDeniedApiException, ImproperlyConfigured from .constants import ITEMIZE, ITEMIZED, PER_PAGE, PAGE, ENVIRONMENT, \ @@ -61,7 +61,6 @@ "Strategy", "DATETIME_FORMAT", "StatelessActions", - "PositionCost", "OrderFee", "parse_decimal_to_string", "parse_string_to_decimal" diff --git a/investing_algorithm_framework/domain/models/__init__.py b/investing_algorithm_framework/domain/models/__init__.py index 2908cd86..f51be8f2 100644 --- a/investing_algorithm_framework/domain/models/__init__.py +++ b/investing_algorithm_framework/domain/models/__init__.py @@ -6,7 +6,7 @@ from .trading_data_types import TradingDataType from .trading_time_frame import TradingTimeFrame from .portfolio import PortfolioConfiguration, Portfolio -from .position import Position, PositionCost +from .position import Position __all__ = [ "OrderStatus", @@ -24,7 +24,6 @@ "PortfolioConfiguration", "AssetPrice", "Position", - "PositionCost", "Portfolio", "OrderFee" ] diff --git a/investing_algorithm_framework/domain/models/order/order.py b/investing_algorithm_framework/domain/models/order/order.py index b7b49136..3f4273a6 100644 --- a/investing_algorithm_framework/domain/models/order/order.py +++ b/investing_algorithm_framework/domain/models/order/order.py @@ -30,6 +30,7 @@ def __init__( updated_at=None, trade_closed_at=None, trade_closed_price=None, + trade_closed_amount=None, external_id=None, filled_amount=None, remaining_amount=None, @@ -65,6 +66,7 @@ def __init__( self.net_gain = parse_decimal_to_string(net_gain) self.trade_closed_at = trade_closed_at self.trade_closed_price = trade_closed_price + self.trade_closed_amount = trade_closed_amount self.created_at = created_at self.updated_at = updated_at self.filled_amount = parse_decimal_to_string(filled_amount) @@ -129,6 +131,12 @@ def get_trade_closed_price(self): def set_trade_closed_price(self, trade_closed_price): self.trade_closed_price = trade_closed_price + def get_trade_closed_amount(self): + return parse_string_to_decimal(self.trade_closed_amount) + + def set_trade_closed_amount(self, trade_closed_amount): + self.trade_closed_amount = trade_closed_amount + def get_created_at(self): return self.created_at @@ -218,26 +226,34 @@ def from_ccxt_order(ccxt_order): def update(self, data): - if 'amount' in data: + if 'amount' in data and data['amount'] is not None: amount = data.pop('amount') self.amount = parse_decimal_to_string(amount) - if 'price' in data: + if 'price' in data and data['price'] is not None: price = data.pop('price') self.price = parse_decimal_to_string(price) - if 'filled' in data: + if 'filled' in data and data['filled'] is not None: filled_amount = data.pop('filled') self.filled_amount = parse_decimal_to_string(filled_amount) - if 'remaining' in data: + if 'remaining' in data and data['remaining'] is not None: remaining_amount = data.pop('remaining') self.remaining_amount = parse_decimal_to_string(remaining_amount) - if 'net_gain' in data: + if 'net_gain' in data and data['net_gain'] is not None: net_gain = data.pop('net_gain') self.net_gain = parse_decimal_to_string(net_gain) + if 'trade_closed_price' in data and data['trade_closed_price'] is not None: + trade_closed_price = data.pop('trade_closed_price') + self.trade_closed_price = parse_decimal_to_string(trade_closed_price) + + if 'trade_closed_amount' in data and data['trade_closed_amount'] is not None: + trade_closed_amount = data.pop('trade_closed_amount') + self.trade_closed_amount = parse_decimal_to_string(trade_closed_amount) + super().update(data) def __repr__(self): @@ -249,8 +265,9 @@ def __repr__(self): return self.repr( id=id_value, - price=self.price, - amount=self.amount, + price=self.get_price(), + amount=self.get_amount(), + net_gain=self.get_net_gain(), external_id=self.external_id, status=self.status, target_symbol=self.target_symbol, @@ -259,5 +276,5 @@ def __repr__(self): order_type=self.order_type, filled_amount=self.get_filled(), remaining_amount=self.get_remaining(), - cost=self.cost, + cost=self.get_cost(), ) diff --git a/investing_algorithm_framework/domain/models/position/__init__.py b/investing_algorithm_framework/domain/models/position/__init__.py index a35d7e28..6ef56461 100644 --- a/investing_algorithm_framework/domain/models/position/__init__.py +++ b/investing_algorithm_framework/domain/models/position/__init__.py @@ -1,4 +1,3 @@ from .position import Position -from .position_cost import PositionCost -__all__ = ["Position", "PositionCost"] +__all__ = ["Position"] diff --git a/investing_algorithm_framework/domain/models/position/position.py b/investing_algorithm_framework/domain/models/position/position.py index 53bc627e..cabbc2b2 100644 --- a/investing_algorithm_framework/domain/models/position/position.py +++ b/investing_algorithm_framework/domain/models/position/position.py @@ -9,10 +9,12 @@ def __init__( self, symbol=None, amount=0, + cost=0, portfolio_id=None ): self.symbol = symbol self.amount = amount + self.cost = cost self.portfolio_id = portfolio_id def get_symbol(self): @@ -24,6 +26,12 @@ def set_symbol(self, symbol): def get_amount(self): return parse_string_to_decimal(self.amount) + def get_cost(self): + return parse_string_to_decimal(self.cost) + + def set_cost(self, cost): + self.cost = cost + def set_amount(self, amount): self.amount = amount diff --git a/investing_algorithm_framework/infrastructure/__init__.py b/investing_algorithm_framework/infrastructure/__init__.py index 246a065d..5b50b2d2 100644 --- a/investing_algorithm_framework/infrastructure/__init__.py +++ b/investing_algorithm_framework/infrastructure/__init__.py @@ -1,16 +1,14 @@ from .repositories import SQLOrderRepository, SQLPositionRepository, \ - SQLPortfolioRepository, SQLPositionCostRepository, SQLOrderFeeRepository + SQLPortfolioRepository, SQLOrderFeeRepository from .services import MarketService from .database import setup_sqlalchemy, Session, \ create_all_tables -from .models import SQLPortfolio, SQLOrder, SQLPosition, SQLOrderFee, \ - SQLPositionCost +from .models import SQLPortfolio, SQLOrder, SQLPosition, SQLOrderFee __all__ = [ "create_all_tables", "SQLPositionRepository", "SQLPortfolioRepository", - "SQLPositionCostRepository", "SQLOrderRepository", "SQLOrderFeeRepository", "MarketService", @@ -20,5 +18,4 @@ "SQLOrder", "SQLOrderFee", "SQLPosition", - "SQLPositionCost", ] diff --git a/investing_algorithm_framework/infrastructure/models/__init__.py b/investing_algorithm_framework/infrastructure/models/__init__.py index 48af40f1..dbabe807 100644 --- a/investing_algorithm_framework/infrastructure/models/__init__.py +++ b/investing_algorithm_framework/infrastructure/models/__init__.py @@ -1,7 +1,7 @@ from .order import SQLOrder, SQLOrderFee from .portfolio import SQLPortfolio -from .position import SQLPosition, SQLPositionCost +from .position import SQLPosition __all__ = [ - "SQLOrder", "SQLPosition", "SQLPortfolio", "SQLPositionCost", "SQLOrderFee" + "SQLOrder", "SQLPosition", "SQLPortfolio", "SQLOrderFee" ] diff --git a/investing_algorithm_framework/infrastructure/models/order/order.py b/investing_algorithm_framework/infrastructure/models/order/order.py index 7f0073ab..f809c26e 100644 --- a/investing_algorithm_framework/infrastructure/models/order/order.py +++ b/investing_algorithm_framework/infrastructure/models/order/order.py @@ -33,6 +33,7 @@ class SQLOrder(Order, SQLBaseModel, SQLAlchemyModelExtension): updated_at = Column(DateTime, default=datetime.utcnow) trade_closed_at = Column(DateTime, default=None) trade_closed_price = Column(String, default=None) + trade_closed_amount = Column(String, default=None) net_gain = Column(String, default=0) fee = relationship( "SQLOrderFee", @@ -56,6 +57,7 @@ def from_order(order): updated_at=order.get_updated_at(), trade_closed_at=order.get_trade_closed_at(), trade_closed_price=order.get_trade_closed_price(), + trade_closed_amount=order.get_trade_closed_amount(), net_gain=order.get_net_gain(), ) @@ -79,3 +81,7 @@ def from_ccxt_order(ccxt_order): fee=OrderFee.from_ccxt_fee(ccxt_order.get("fee", None)), created_at=ccxt_order.get("datetime", None), ) + + def __lt__(self, other): + # Define the less-than comparison based on created_at attribute + return self.created_at < other.created_at diff --git a/investing_algorithm_framework/infrastructure/models/position/__init__.py b/investing_algorithm_framework/infrastructure/models/position/__init__.py index 324d9280..922a60df 100644 --- a/investing_algorithm_framework/infrastructure/models/position/__init__.py +++ b/investing_algorithm_framework/infrastructure/models/position/__init__.py @@ -1,4 +1,3 @@ from .position import SQLPosition -from .position_cost import SQLPositionCost -__all__ = ["SQLPosition", "SQLPositionCost"] +__all__ = ["SQLPosition"] diff --git a/investing_algorithm_framework/infrastructure/models/position/position.py b/investing_algorithm_framework/infrastructure/models/position/position.py index 1cfed737..f4ee00d2 100644 --- a/investing_algorithm_framework/infrastructure/models/position/position.py +++ b/investing_algorithm_framework/infrastructure/models/position/position.py @@ -15,18 +15,13 @@ class SQLPosition(SQLBaseModel, Position, SQLAlchemyModelExtension): id = Column(Integer, primary_key=True, unique=True) symbol = Column(String) amount = Column(String) + cost = Column(String) orders = relationship( "SQLOrder", back_populates="position", lazy="dynamic", cascade="all, delete-orphan" ) - position_costs = relationship( - "SQLPositionCost", - back_populates="position", - lazy="dynamic", - cascade="all, delete-orphan" - ) portfolio_id = Column(Integer, ForeignKey('portfolios.id')) portfolio = relationship("SQLPortfolio", back_populates="positions") __table_args__ = ( @@ -34,7 +29,6 @@ class SQLPosition(SQLBaseModel, Position, SQLAlchemyModelExtension): 'symbol', 'portfolio_id', name='_symbol_portfolio_uc' ), ) - _cost = 0 def __init__( self, @@ -60,6 +54,9 @@ def update(self, data): if 'amount' in data: self.amount = parse_decimal_to_string(data.pop('amount')) + if 'cost' in data: + self.cost = parse_decimal_to_string(data.pop('cost')) + super(SQLPosition, self).update(data) @property diff --git a/investing_algorithm_framework/infrastructure/models/position/position_cost.py b/investing_algorithm_framework/infrastructure/models/position/position_cost.py deleted file mode 100644 index 8ca4a80a..00000000 --- a/investing_algorithm_framework/infrastructure/models/position/position_cost.py +++ /dev/null @@ -1,41 +0,0 @@ -from sqlalchemy import Column, Integer, Numeric, ForeignKey, DateTime -from sqlalchemy.orm import relationship - -from investing_algorithm_framework.domain import PositionCost -from investing_algorithm_framework.infrastructure.database import SQLBaseModel -from investing_algorithm_framework.infrastructure.models.model_extension \ - import SQLAlchemyModelExtension -from investing_algorithm_framework.domain import parse_decimal_to_string - - -class SQLPositionCost(PositionCost, SQLBaseModel, SQLAlchemyModelExtension): - __tablename__ = "position_costs" - id = Column(Integer, primary_key=True, unique=True) - price = Column(Numeric(precision=24, scale=20)) - amount = Column(Numeric(precision=24, scale=20)) - created_at = Column(DateTime) - position_id = Column(Integer, ForeignKey('positions.id')) - position = relationship("SQLPosition", back_populates="position_costs") - - def __init__( - self, - price=0, - amount=0, - position_id=None, - created_at=None, - ): - super(SQLPositionCost, self).__init__() - self.amount = parse_decimal_to_string(amount) - self.price = parse_decimal_to_string(price) - self.position_id = position_id - self.created_at = created_at - - def update(self, data): - - if "price" in data: - self.price = parse_decimal_to_string(data.pop("price")) - - if "amount" in data: - self.amount = parse_decimal_to_string(data.pop("amount")) - - super(SQLPositionCost, self).update(data) diff --git a/investing_algorithm_framework/infrastructure/repositories/__init__.py b/investing_algorithm_framework/infrastructure/repositories/__init__.py index 6feaf588..067e521e 100644 --- a/investing_algorithm_framework/infrastructure/repositories/__init__.py +++ b/investing_algorithm_framework/infrastructure/repositories/__init__.py @@ -2,12 +2,10 @@ from .order_fee_repository import SQLOrderFeeRepository from .position_repository import SQLPositionRepository from .portfolio_repository import SQLPortfolioRepository -from .position_cost_repository import SQLPositionCostRepository __all__ = [ "SQLOrderFeeRepository", "SQLOrderRepository", "SQLPositionRepository", "SQLPortfolioRepository", - "SQLPositionCostRepository" ] diff --git a/investing_algorithm_framework/infrastructure/repositories/position_cost_repository.py b/investing_algorithm_framework/infrastructure/repositories/position_cost_repository.py deleted file mode 100644 index c3466ea5..00000000 --- a/investing_algorithm_framework/infrastructure/repositories/position_cost_repository.py +++ /dev/null @@ -1,16 +0,0 @@ -from investing_algorithm_framework.infrastructure.models import SQLPositionCost -from .repository import Repository - - -class SQLPositionCostRepository(Repository): - base_class = SQLPositionCost - DEFAULT_NOT_FOUND_MESSAGE = "Position cost not found" - - def _apply_query_params(self, db, query, query_params): - position_query_param = self.get_query_param("position", query_params) - - if position_query_param: - query = query.filter_by(position_id=position_query_param) - - query = query.order_by(SQLPositionCost.created_at.desc()) - return query diff --git a/investing_algorithm_framework/services/__init__.py b/investing_algorithm_framework/services/__init__.py index 4e7b0ee4..a2c6d063 100644 --- a/investing_algorithm_framework/services/__init__.py +++ b/investing_algorithm_framework/services/__init__.py @@ -1,7 +1,6 @@ from .order_service import OrderService from .portfolio_service import PortfolioService from .position_service import PositionService -from .position_cost_service import PositionCostService from .repository_service import RepositoryService from .strategy_orchestrator_service import StrategyOrchestratorService from .portfolio_configuration_service import PortfolioConfigurationService @@ -15,5 +14,4 @@ "PositionService", "PortfolioConfigurationService", "MarketDataService", - "PositionCostService" ] diff --git a/investing_algorithm_framework/services/order_service.py b/investing_algorithm_framework/services/order_service.py index ebf100da..faa77b7c 100644 --- a/investing_algorithm_framework/services/order_service.py +++ b/investing_algorithm_framework/services/order_service.py @@ -1,4 +1,5 @@ import logging +from queue import PriorityQueue from datetime import datetime from investing_algorithm_framework.domain import OrderType, OrderSide, \ @@ -18,7 +19,6 @@ def __init__( order_fee_repository, market_service, position_repository, - position_cost_repository, portfolio_repository, portfolio_configuration_service, ): @@ -27,7 +27,6 @@ def __init__( self.order_fee_repository = order_fee_repository self.market_service = market_service self.position_repository = position_repository - self.position_cost_repository = position_cost_repository self.portfolio_repository = portfolio_repository self.portfolio_configuration_service = portfolio_configuration_service @@ -74,7 +73,7 @@ def create(self, data, execute=True, validate=True, sync=True): return order def update(self, object_id, data): - previous_order = self.get(object_id) + previous_order = self.order_repository.get(object_id) if "fee" in data: order_fee_data = data.pop("fee") @@ -373,7 +372,8 @@ def _sync_with_buy_order_filled(self, previous_order, current_order): self.position_repository.update( position.id, { - "amount": position.get_amount() + filled_difference + "amount": position.get_amount() + filled_difference, + "cost": position.get_cost() + filled_size, } ) @@ -388,35 +388,6 @@ def _sync_with_buy_order_filled(self, previous_order, current_order): } ) - # Update position cost - if self.position_cost_repository.exists( - { - "position": position.id, - "created_at": current_order.created_at - } - ): - position_cost = self.position_cost_repository.find( - { - "position": position.id, - "created_at": current_order.created_at - } - ) - self.position_cost_repository.update( - position_cost.id, - { - "amount": position_cost.get_amount() + filled_difference - } - ) - else: - self.position_cost_repository.create( - { - "position_id": position.id, - "amount": filled_difference, - "price": current_order.get_price(), - "created_at": current_order.get_created_at() - } - ) - def _sync_with_sell_order_filled(self, previous_order, current_order): filled_difference = current_order.get_filled() - \ previous_order.get_filled() @@ -430,7 +401,6 @@ def _sync_with_sell_order_filled(self, previous_order, current_order): # Update the portfolio portfolio = self.portfolio_repository.get(position.portfolio_id) - self.portfolio_repository.update( portfolio.id, { @@ -455,20 +425,11 @@ def _sync_with_sell_order_filled(self, previous_order, current_order): } ) - self._close_position_costs(filled_difference, current_order) + self._close_trades(filled_difference, current_order) def _sync_with_buy_order_cancelled(self, order): remaining = order.get_amount() - order.get_filled() size = remaining * order.get_price() - position_cost = self.position_cost_repository.find( - { - "position": order.position_id, - "created_at": order.created_at - } - ) - - if position_cost.get_amount() == 0: - self.position_cost_repository.delete(position_cost.id) # Add the remaining amount to the portfolio portfolio = self.portfolio_repository.find( @@ -512,15 +473,6 @@ def _sync_with_sell_order_cancelled(self, order): def _sync_with_buy_order_failed(self, order): remaining = order.get_amount() - order.get_filled() size = remaining * order.get_price() - position_cost = self.position_cost_repository.find( - { - "position": order.position_id, - "created_at": order.created_at - } - ) - - if position_cost.get_amount() == 0: - self.position_cost_repository.delete(position_cost.id) # Add the remaining amount to the portfolio portfolio = self.portfolio_repository.find( @@ -564,15 +516,6 @@ def _sync_with_sell_order_failed(self, order): def _sync_with_buy_order_expired(self, order): remaining = order.get_amount() - order.get_filled() size = remaining * order.get_price() - position_cost = self.position_cost_repository.find( - { - "position": order.position_id, - "created_at": order.created_at - } - ) - - if position_cost.get_amount() == 0: - self.position_cost_repository.delete(position_cost.id) # Add the remaining amount to the portfolio portfolio = self.portfolio_repository.find( @@ -616,15 +559,6 @@ def _sync_with_sell_order_expired(self, order): def _sync_with_buy_order_rejected(self, order): remaining = order.get_amount() - order.get_filled() size = remaining * order.get_price() - position_cost = self.position_cost_repository.find( - { - "position": order.position_id, - "created_at": order.created_at - } - ) - - if position_cost.get_amount() == 0: - self.position_cost_repository.delete(position_cost.id) # Add the remaining amount to the portfolio portfolio = self.portfolio_repository.find( @@ -665,79 +599,90 @@ def _sync_with_sell_order_rejected(self, order): } ) - - def _close_position_costs(self, amount_to_close, current_order): - total_net_gain = 0 - position_costs = self.position_cost_repository.get_all( + def _close_trades(self, amount_to_close, sell_order): + matching_buy_orders = self.order_repository.get_all( { - "position": current_order.position_id + "position": sell_order.position_id, + "order_side": "buy", + "trade_closed_at": None } ) + order_queue = PriorityQueue() + + for order in matching_buy_orders: + order_queue.put(order) - while amount_to_close > 0 and len(position_costs) > 0: - position_cost = position_costs[0] + total_net_gain = 0 + total_cost = 0 - matching_buy_order = self.position_cost_repository.find({ - "position": current_order.position_id, - "side": OrderSide.BUY.value, - "created_at": position_cost.get_created_at() - }) - net_gain = matching_buy_order.get_price() \ - - position_cost.get_price() + while amount_to_close > 0 and not order_queue.empty(): + buy_order = order_queue.get() - if amount_to_close > position_cost.get_amount(): - amount_to_close -= position_cost.get_amount() + if buy_order.get_trade_closed_amount() != None: + available_to_close = buy_order.get_filled() \ + - buy_order.get_trade_closed_amount() + else: + available_to_close = buy_order.get_filled() + + if amount_to_close >= available_to_close: + to_be_closed = available_to_close + remaining = amount_to_close - to_be_closed + cost = buy_order.get_price() * to_be_closed + net_gain = (sell_order.get_price() - buy_order.get_price()) * to_be_closed + amount_to_close = remaining self.order_repository.update( - matching_buy_order.id, + buy_order.id, { - "net_gain": net_gain, - "trade_closed_at": current_order.get_created_at(), - "TradeClosedPrice": current_order.get_price() + "trade_closed_amount": buy_order.get_filled(), + "trade_closed_at": datetime.utcnow(), + "trade_closed_price": sell_order.get_price(), + "net_gain": buy_order.get_net_gain() + net_gain } ) - self.position_cost_repository.delete(position_cost.id) else: + to_be_closed = amount_to_close + net_gain = (sell_order.get_price() - buy_order.get_price()) * to_be_closed + cost = buy_order.get_price() * amount_to_close self.order_repository.update( - matching_buy_order.id, + buy_order.id, { - "net_gain": net_gain, - "trade_closed_at": current_order.get_created_at(), - "trade_close_price": current_order.get_price() + "trade_closed_amount": buy_order.get_trade_closed_amount() + to_be_closed, + "trade_closed_price": sell_order.get_price(), + "net_gain": buy_order.get_net_gain() + net_gain } ) - position_cost = self.position_cost_repository.update( - position_cost.id, - { - "amount": position_cost.get_amount() - - amount_to_close - } - ) - - self.position_cost_repository.delete(position_cost.id) amount_to_close = 0 - if position_cost.get_amount() > 0: - self.position_cost_repository.delete(position_cost.id) - total_net_gain += net_gain + total_cost += cost - # Update the portfolio - position = self.position_repository.get(current_order.position_id) + position = self.position_repository.get(sell_order.position_id) + + # Update portfolio portfolio = self.portfolio_repository.get(position.portfolio_id) self.portfolio_repository.update( portfolio.id, { - "total_net_gain": portfolio.get_total_net_gain() - + total_net_gain + "total_net_gain": portfolio.get_total_net_gain() + total_net_gain, + "total_cost": portfolio.get_total_cost() - total_cost + } + ) + # Update the position + position = self.position_repository.get(sell_order.position_id) + self.position_repository.update( + position.id, + { + "cost": position.get_cost() - total_cost } ) # Update the sell order self.order_repository.update( - current_order.id, + sell_order.id, { - "net_gain": total_net_gain, - "trade_closed_at": current_order.created_at, - "TradeClosedPrice": current_order.get_price() + "trade_closed_amount": sell_order.get_filled(), + "trade_closed_at": datetime.utcnow(), + "trade_closed_price": sell_order.get_price(), + "net_gain": sell_order.get_net_gain() + total_net_gain } ) diff --git a/tests/app/algorithm/test_create_market_sell_order.py b/tests/app/algorithm/test_create_market_sell_order.py index 7b807dc7..4b5a9989 100644 --- a/tests/app/algorithm/test_create_market_sell_order.py +++ b/tests/app/algorithm/test_create_market_sell_order.py @@ -71,4 +71,4 @@ def test_create_market_sell_order(self): btc_position = position_service.find({"symbol": "BTC"}) trading_symbol_position = position_service.find({"symbol": "USDT"}) self.assertEqual(Decimal(0), btc_position.get_amount()) - self.assertNotEqual(Decimal(1000), trading_symbol_position.get_amount()) + self.assertEqual(Decimal(1000), trading_symbol_position.get_amount()) diff --git a/tests/app/algorithm/test_has_open_sell_orders.py b/tests/app/algorithm/test_has_open_sell_orders.py index 0f849d3b..069ce594 100644 --- a/tests/app/algorithm/test_has_open_sell_orders.py +++ b/tests/app/algorithm/test_has_open_sell_orders.py @@ -33,9 +33,9 @@ def setUp(self) -> None: ) ) self.app.container.market_service.override(MarketServiceStub()) - self.app.create_portfolios() + self.app.initialize() - def test_has_open_buy_orders(self): + def test_has_open_sell_orders(self): self.app.run(number_of_iterations=1, sync=False) trading_symbol_position = self.app.algorithm.get_position("USDT") self.assertEqual(1000, trading_symbol_position.get_amount()) diff --git a/tests/services/test_order_service.py b/tests/services/test_order_service.py index 41bc158d..586282d1 100644 --- a/tests/services/test_order_service.py +++ b/tests/services/test_order_service.py @@ -110,3 +110,119 @@ def test_update_buy_order_with_cancelled_order(self): def test_update_sell_order_with_cancelled_order(self): pass + + def test_trade_closing_winning_trade(self): + order_service = self.app.container.order_service() + buy_order = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "USDT", + "amount": Decimal('1000'), + "side": "BUY", + "price": Decimal('0.2'), + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CREATED", + } + ) + updated_buy_order = order_service.update( + buy_order.id, + { + "status": "CLOSED", + "filled": Decimal('1000'), + "remaining": Decimal('0'), + } + ) + self.assertEqual(updated_buy_order.amount, '1000') + self.assertEqual(updated_buy_order.filled_amount, '1000') + self.assertEqual(updated_buy_order.remaining_amount, '0') + + # Create a sell order with a higher price + sell_order = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "USDT", + "amount": Decimal('1000'), + "side": "SELL", + "price": Decimal('0.3'), + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CREATED", + } + ) + self.assertEqual(Decimal('0.3'), sell_order.get_price()) + updated_sell_order = order_service.update( + sell_order.id, + { + "status": "CLOSED", + "filled": Decimal('1000'), + "remaining": Decimal('0'), + } + ) + self.assertEqual(Decimal('0.3'), updated_sell_order.get_price()) + self.assertEqual(updated_sell_order.amount, '1000') + self.assertEqual(updated_sell_order.filled_amount, '1000') + self.assertEqual(updated_sell_order.remaining_amount, '0') + buy_order = order_service.get(buy_order.id) + self.assertEqual(buy_order.status, "CLOSED") + self.assertIsNotNone(buy_order.get_trade_closed_at()) + self.assertIsNotNone(buy_order.get_trade_closed_price()) + self.assertEqual(100, buy_order.get_net_gain()) + + def test_trade_closing_losing_trade(self): + order_service = self.app.container.order_service() + buy_order = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "USDT", + "amount": Decimal('1000'), + "side": "BUY", + "price": Decimal('0.2'), + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CREATED", + } + ) + updated_buy_order = order_service.update( + buy_order.id, + { + "status": "CLOSED", + "filled": Decimal('1000'), + "remaining": Decimal('0'), + } + ) + self.assertEqual(updated_buy_order.amount, '1000') + self.assertEqual(updated_buy_order.filled_amount, '1000') + self.assertEqual(updated_buy_order.remaining_amount, '0') + + # Create a sell order with a higher price + sell_order = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "USDT", + "amount": Decimal('1000'), + "side": "SELL", + "price": Decimal('0.1'), + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CREATED", + } + ) + self.assertEqual(Decimal('0.1'), sell_order.get_price()) + updated_sell_order = order_service.update( + sell_order.id, + { + "status": "CLOSED", + "filled": Decimal('1000'), + "remaining": Decimal('0'), + } + ) + self.assertEqual(Decimal('0.1'), updated_sell_order.get_price()) + self.assertEqual(updated_sell_order.amount, '1000') + self.assertEqual(updated_sell_order.filled_amount, '1000') + self.assertEqual(updated_sell_order.remaining_amount, '0') + buy_order = order_service.get(buy_order.id) + self.assertEqual(buy_order.status, "CLOSED") + self.assertIsNotNone(buy_order.get_trade_closed_at()) + self.assertIsNotNone(buy_order.get_trade_closed_price()) + self.assertEqual(-100, buy_order.get_net_gain()) diff --git a/version.py b/version.py index 8684d0d4..32162ee1 100644 --- a/version.py +++ b/version.py @@ -1,4 +1,4 @@ -VERSION = (1, 4, 1, 'alpha', 0) +VERSION = (1, 4, 2, 'alpha', 0) def get_version(version=None): From edeca54c21924c42c21a947233147488c6a7e9a6 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 9 Oct 2023 22:37:19 +0200 Subject: [PATCH 2/3] Fix percentage order --- investing_algorithm_framework/app/algorithm.py | 16 ++++++++++++++-- version.py | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py index 7fadf2ec..8fab2a03 100644 --- a/investing_algorithm_framework/app/algorithm.py +++ b/investing_algorithm_framework/app/algorithm.py @@ -98,11 +98,23 @@ def create_limit_order( ): portfolio = self.portfolio_service.find({"market": market}) - if percentage_of_portfolio is not None and OrderSide.BUY.equals(side): + if percentage_of_portfolio is not None: + + if not OrderSide.BUY.equals(side): + raise ApiException( + "Percentage of portfolio is only supported for BUY orders." + ) + size = float(portfolio.net_size) * (percentage_of_portfolio / 100) amount = floor(size / price) - if percentage_of_position is not None and OrderSide.SELL.equals(side): + elif percentage_of_position is not None: + + if not OrderSide.SELL.equals(side): + raise ApiException( + "Percentage of position is only supported for SELL orders." + ) + position = self.position_service.find( { "symbol": target_symbol, diff --git a/version.py b/version.py index 32162ee1..6c06e264 100644 --- a/version.py +++ b/version.py @@ -1,4 +1,4 @@ -VERSION = (1, 4, 2, 'alpha', 0) +VERSION = (1, 4, 3, 'alpha', 0) def get_version(version=None): From 79c72007168f2bf15196dd22c5d2dbdf21729202 Mon Sep 17 00:00:00 2001 From: Marc van Duyn Date: Sun, 15 Oct 2023 19:44:50 +0200 Subject: [PATCH 3/3] Fix/percentage order (#246) * Fix percentage order * Remove debug * Bump version --- investing_algorithm_framework/app/algorithm.py | 3 +-- investing_algorithm_framework/app/app.py | 1 + .../infrastructure/repositories/order_repository.py | 2 +- version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py index 8fab2a03..af3aa707 100644 --- a/investing_algorithm_framework/app/algorithm.py +++ b/investing_algorithm_framework/app/algorithm.py @@ -99,14 +99,13 @@ def create_limit_order( portfolio = self.portfolio_service.find({"market": market}) if percentage_of_portfolio is not None: - if not OrderSide.BUY.equals(side): raise ApiException( "Percentage of portfolio is only supported for BUY orders." ) size = float(portfolio.net_size) * (percentage_of_portfolio / 100) - amount = floor(size / price) + amount = size / price elif percentage_of_position is not None: diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 9f437151..047bb72f 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -302,6 +302,7 @@ def create_portfolios(self): continue balances = market_service.get_balance() + if portfolio_configuration.trading_symbol.upper() not in balances: raise OperationalException( f"Trading symbol balance not available " diff --git a/investing_algorithm_framework/infrastructure/repositories/order_repository.py b/investing_algorithm_framework/infrastructure/repositories/order_repository.py index 08b653dd..6ef37e1c 100644 --- a/investing_algorithm_framework/infrastructure/repositories/order_repository.py +++ b/investing_algorithm_framework/infrastructure/repositories/order_repository.py @@ -24,7 +24,7 @@ def _apply_query_params(self, db, query, query_params): "position", query_params, many=True ) target_symbol_query_param = self.get_query_param( - "symbol", query_params + "target_symbol", query_params ) trading_symbol_query_param = self.get_query_param( "trading_symbol", query_params diff --git a/version.py b/version.py index 6c06e264..e1e2f05e 100644 --- a/version.py +++ b/version.py @@ -1,4 +1,4 @@ -VERSION = (1, 4, 3, 'alpha', 0) +VERSION = (1, 4, 4, 'alpha', 0) def get_version(version=None):