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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.4.237] - 2024-12-12
### Added
[Trades] add get_real_or_estimated_trade_fee
[TradingMode] add is_first_trading_mode_on_this_matrix
[TradingMode] add get_is_using_trading_mode_on_exchange
[Websockets] add additional config support to ws exchanges
### Updated
[Orders] make apply_inactive_orders more flexible
### Fixed
[Orders] fix partially fill channel notification

## [2.4.236] - 2024-12-08
### Tools
[Tools] use gather_waiting_for_all_before_raising
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# OctoBot-Trading [2.4.236](https://github.com/Drakkar-Software/OctoBot-Trading/blob/master/CHANGELOG.md)
# OctoBot-Trading [2.4.237](https://github.com/Drakkar-Software/OctoBot-Trading/blob/master/CHANGELOG.md)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/903b6b22bceb4661b608a86fea655f69)](https://app.codacy.com/gh/Drakkar-Software/OctoBot-Trading?utm_source=github.com&utm_medium=referral&utm_content=Drakkar-Software/OctoBot-Trading&utm_campaign=Badge_Grade_Dashboard)
[![PyPI](https://img.shields.io/pypi/v/OctoBot-Trading.svg)](https://pypi.python.org/pypi/OctoBot-Trading/)
[![Coverage Status](https://coveralls.io/repos/github/Drakkar-Software/OctoBot-Trading/badge.svg?branch=master)](https://coveralls.io/github/Drakkar-Software/OctoBot-Trading?branch=master)
Expand Down
2 changes: 1 addition & 1 deletion octobot_trading/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
# License along with this library.

PROJECT_NAME = "OctoBot-Trading"
VERSION = "2.4.236" # major.minor.revision
VERSION = "2.4.237" # major.minor.revision
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ def get_ccxt_order_type(self, order_type: enums.TraderOrderType):
return enums.TradeOrderType.MARKET.value
raise RuntimeError(f"Unknown order type: {order_type}")

def get_trade_fee(self, symbol: str, order_type: enums.TraderOrderType, quantity, price, taker_or_maker):
def get_trade_fee(self, symbol: str, order_type: enums.TraderOrderType, quantity, price, taker_or_maker) -> dict:
fees = self.calculate_fees(symbol, order_type, quantity, price, taker_or_maker)
fees[enums.FeePropertyColumns.IS_FROM_EXCHANGE.value] = False
fees[enums.FeePropertyColumns.COST.value] = decimal.Decimal(
Expand Down
17 changes: 13 additions & 4 deletions octobot_trading/exchanges/exchange_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ async def _build_exchange_manager(self):
await self._build_trader()

# create trading modes
await self._build_trading_modes_if_required(trading_mode_class)
await self._build_trading_modes_if_required(
trading_mode_class, self.exchange_manager.tentacles_setup_config
)

# add to global exchanges
self.exchange_manager.update_debug_info()
Expand Down Expand Up @@ -126,17 +128,24 @@ async def _register_trading_modes_requirements(self, trading_mode_class, tentacl
not trading_mode_class.is_ignoring_cancelled_orders_trades()
)

async def _build_trading_modes_if_required(self, trading_mode_class):
async def _build_trading_modes_if_required(self, trading_mode_class, tentacles_setup_config):
if self._is_using_trading_modes:
# self.exchange_manager.trader can be None if neither simulator or real trader has be set
if self.exchange_manager.is_trading:
if self.exchange_manager.is_trading and (
self.exchange_manager.trader is None or (
trading_mode_class.get_is_using_trading_mode_on_exchange(
self.exchange_name, tentacles_setup_config
)
)
):
if self.exchange_manager.trader is None:
self.logger.warning(f"There wont be any order created on {self.exchange_name}: neither "
f"simulated nor real trader has been activated.")
else:
self.exchange_manager.trading_modes = await self.build_trading_modes(trading_mode_class)
else:
self.logger.info(f"{self.exchange_name} exchange is online and won't be trading")
no_action = "trading without using direct trading mode" if self.exchange_manager.is_trading else "trading"
self.logger.info(f"{self.exchange_name} exchange is online and won't be {no_action}")

async def build_trading_modes(self, trading_mode_class):
try:
Expand Down
2 changes: 1 addition & 1 deletion octobot_trading/exchanges/types/rest_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ async def cancel_order(
) -> enums.OrderStatus:
return await self.connector.cancel_order(exchange_order_id, symbol, order_type, **kwargs)

def get_trade_fee(self, symbol: str, order_type: enums.TraderOrderType, quantity, price, taker_or_maker):
def get_trade_fee(self, symbol: str, order_type: enums.TraderOrderType, quantity, price, taker_or_maker) -> dict:
return self.connector.get_trade_fee(symbol, order_type, quantity, price, taker_or_maker)

def get_fees(self, symbol):
Expand Down
19 changes: 17 additions & 2 deletions octobot_trading/modes/abstract_trading_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,25 @@ def is_following_trading_signals(self) -> bool:
return False

@classmethod
def get_is_trading_on_exchange(cls, exchange_name,
tentacles_setup_config: tm_configuration.TentaclesSetupConfiguration) -> bool:
def get_is_trading_on_exchange(
cls, exchange_name, tentacles_setup_config: tm_configuration.TentaclesSetupConfiguration
) -> bool:
"""
:return: When returning false, the associated exchange_manager.is_trading will be set to false, which will
prevent the initialization of trade related elements. Default is True
"""
return True

@classmethod
def get_is_using_trading_mode_on_exchange(
cls, exchange_name, tentacles_setup_config: tm_configuration.TentaclesSetupConfiguration
) -> bool:
"""
:return: When returning false, no trading mode will be created on this exchange, even if
get_is_trading_on_exchange() returns True. Default is falling back to get_is_trading_on_exchange()
"""
return cls.get_is_trading_on_exchange(exchange_name, tentacles_setup_config)

@classmethod
def is_ignoring_cancelled_orders_trades(cls) -> bool:
"""
Expand Down Expand Up @@ -672,3 +683,7 @@ def get_trading_mode_consumers(self):
for consumer in self.consumers
if isinstance(consumer, abstract_mode_consumer.AbstractTradingModeConsumer)
]

def is_first_trading_mode_on_this_matrix(self) -> bool:
all_trading_modes = modes_util.get_trading_modes_of_this_type_on_this_matrix(self)
return bool(all_trading_modes and all_trading_modes[0] is self)
2 changes: 2 additions & 0 deletions octobot_trading/personal_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
TradePnl,
compute_win_rate,
aggregate_trades_by_exchange_order_id,
get_real_or_estimated_trade_fee,
)
from octobot_trading.personal_data import transactions
from octobot_trading.personal_data.transactions import (
Expand Down Expand Up @@ -458,6 +459,7 @@
"TradePnl",
"compute_win_rate",
"aggregate_trades_by_exchange_order_id",
"get_real_or_estimated_trade_fee",
"ExchangePersonalData",
"get_asset_price_from_converter_or_tickers",
"resolve_sub_portfolios",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,23 @@ class ActiveOrderSwapStrategy:
def is_priority_order(self, order) -> bool:
raise NotImplementedError("is_priority_order is not implemented")

async def apply_inactive_orders(self, orders: list):
async def apply_inactive_orders(
self, orders: list,
trigger_above_by_order_id: typing.Optional[dict[str, bool]] = None
):
for order in orders:
trigger_above = trigger_above_by_order_id.get(
order.order_id, order.trigger_above
) if trigger_above_by_order_id else order.trigger_above
trigger_price = self._get_trigger_price(order)
if self.is_priority_order(order):
# still register active trigger in case this order becomes inactive
order.update(
active_trigger=order_util.create_order_price_trigger(order, trigger_price, order.trigger_above)
active_trigger=order_util.create_order_price_trigger(order, trigger_price, trigger_above)
)
else:
await order.set_as_inactive(
order_util.create_order_price_trigger(order, trigger_price, order.trigger_above)
order_util.create_order_price_trigger(order, trigger_price, trigger_above)
)

def on_order_update(self, order, update_time):
Expand Down
8 changes: 5 additions & 3 deletions octobot_trading/personal_data/orders/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,8 +931,9 @@ def update_from_raw(self, raw_order):
)
filled_price = decimal.Decimal(str(price))
# set average price with real average price if available, use filled_price otherwise
average_price = decimal.Decimal(str(raw_order.get(enums.ExchangeConstantsOrderColumns.AVERAGE.value, 0.0)
or filled_price))
average_price = decimal.Decimal(str(
raw_order.get(enums.ExchangeConstantsOrderColumns.AVERAGE.value, 0.0) or filled_price
))

return self.update(
symbol=str(raw_order.get(enums.ExchangeConstantsOrderColumns.SYMBOL.value, None)),
Expand Down Expand Up @@ -1187,12 +1188,13 @@ def to_string(self):
)
trailing_profile = f"Trailing profile : {self.trailing_profile} | " if self.trailing_profile else ""
cancel_policy = f"Cancel policy : {self.cancel_policy} | " if self.cancel_policy else ""
filled_quantity = f" ({self.filled_quantity} Filled)" if self.filled_quantity else ""
return (
f"{inactive}{self.symbol} | "
f"{chained_order}"
f"{self.order_type.name if self.order_type is not None else 'Unknown'} | "
f"Price : {str(self.origin_price)} | "
f"Quantity : {str(self.origin_quantity)}{' (Reduce only)' if self.reduce_only else ''} | "
f"Quantity : {str(self.origin_quantity)}{filled_quantity}{' (Reduce only)' if self.reduce_only else ''} | "
f"State : {self.state.state.value if self.state is not None else 'Unknown'} | "
f"{trailing_profile}"
f"{cancel_policy}"
Expand Down
2 changes: 1 addition & 1 deletion octobot_trading/personal_data/orders/order_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def get_logger(self):
"""
:return: the order logger
"""
return logging.get_logger(self.order.get_logger_name() if self.order is not None else
return logging.get_logger(f"[{self.__class__.__name__}] {self.order.get_logger_name()}" if self.order is not None else
f"{self.__class__.__name__}_without_order")

def log_event_message(self, state_message, error=None):
Expand Down
10 changes: 7 additions & 3 deletions octobot_trading/personal_data/orders/states/fill_order_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ async def on_refresh_successful(self):
can also be still pending
or be fully filled
"""
if self.order.status is enums.OrderStatus.PARTIALLY_FILLED:
# TODO manage partially filled
await self.update()
self.get_logger().info(f"on_refresh_successful [{self.order.status}] for {self.order}")
if self.order.is_partially_filled():
self.get_logger().info(f"Partially filled order: {str(self.order)}")
# notify order partially filled
await self.order.exchange_manager.exchange_personal_data.handle_order_update_notification(
self.order, enums.OrderUpdateType.STATE_CHANGE
)
elif self.order.status in constants.FILL_ORDER_STATUS_SCOPE:
self.state = enums.OrderStates.FILLED
await self.update()
Expand Down
12 changes: 9 additions & 3 deletions octobot_trading/personal_data/orders/states/open_order_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import octobot_trading.enums as enums
import octobot_trading.personal_data.orders.order_state as order_state
import octobot_trading.personal_data.orders.states.order_state_factory as order_state_factory
import octobot_trading.personal_data.portfolios.portfolio_util as portfolio_util


class OpenOrderState(order_state.OrderState):
Expand Down Expand Up @@ -55,11 +56,16 @@ async def initialize_impl(self, forced=False) -> None:
# update the availability of the currency in the portfolio if order is not
# from exchange initialization (otherwise it's already taken into account in portfolio)
portfolio_manager = self.order.exchange_manager.exchange_personal_data.portfolio_manager
before_order_details = str(portfolio_manager.portfolio)
before_order_details = portfolio_util.filter_empty_values(
portfolio_util.portfolio_to_float(portfolio_manager.portfolio.portfolio)
)
portfolio_manager.refresh_portfolio_available_from_order(self.order, True)
after_order_details = portfolio_util.filter_empty_values(
portfolio_util.portfolio_to_float(portfolio_manager.portfolio.portfolio)
)
self.get_logger().debug(
f"Updated portfolio available after new open order. "
f"Before order: {before_order_details}. After order: {portfolio_manager.portfolio}"
f"Updated [{self.order.exchange_manager.exchange_name}] portfolio available after new open order. "
f"Before order: {before_order_details}. After order: {after_order_details}"
)

return await super().initialize_impl()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __contains__(self, item):

def __repr__(self):
return f"{self.__class__.__name__} " \
f"[timestamp: {HistoricalAssetValue}, _value_by_currency{self._value_by_currency}]"
f"[timestamp: {self._timestamp}, _value_by_currency{self._value_by_currency}]"

def get(self, currency):
return self._value_by_currency[currency]
Expand Down
2 changes: 2 additions & 0 deletions octobot_trading/personal_data/trades/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from octobot_trading.personal_data.trades.trades_util import (
compute_win_rate,
aggregate_trades_by_exchange_order_id,
get_real_or_estimated_trade_fee,
)

__all__ = [
Expand All @@ -57,4 +58,5 @@
"TradePnl",
"compute_win_rate",
"aggregate_trades_by_exchange_order_id",
"get_real_or_estimated_trade_fee",
]
3 changes: 3 additions & 0 deletions octobot_trading/personal_data/trades/trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ def from_dict(cls, trader, trade_dict):
trade.creation_time = trade_dict.get(enums.TradeExtraConstants.CREATION_TIME.value)
return trade

def duplicate(self):
return self.__class__.from_dict(self.trader, self.to_dict())

def clear(self):
self.trader = None # type: ignore
self.exchange_manager = None # type: ignore
21 changes: 12 additions & 9 deletions octobot_trading/personal_data/trades/trade_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import typing

import octobot_commons.logging as logging
import octobot_trading.personal_data.trades.trade as trade_class
import octobot_trading.personal_data.orders.order as order_class # pylint: disable=unused-import
import octobot_trading.personal_data.orders.order_factory as order_factory
import octobot_trading.enums as enums
import octobot_trading.constants as constants


def create_trade_instance_from_raw(trader, raw_trade):
def create_trade_instance_from_raw(trader, raw_trade: dict[str, typing.Any]) -> "trade_class.Trade":
order = create_closed_order_instance_from_raw_trade(trader, raw_trade)
exchange_trade_id = raw_trade.get(enums.ExchangeConstantsOrderColumns.EXCHANGE_TRADE_ID.value)
return create_trade_from_order(order, exchange_trade_id=exchange_trade_id)


def create_closed_order_instance_from_raw_trade(trader, raw_trade):
def create_closed_order_instance_from_raw_trade(trader, raw_trade: dict[str, typing.Any]) -> "order_class.Order":
order = order_factory.create_order_from_raw(trader, raw_trade)
order.update_from_raw(raw_trade)
if order.status is enums.OrderStatus.CANCELED:
Expand All @@ -38,12 +41,12 @@ def create_closed_order_instance_from_raw_trade(trader, raw_trade):
return order


def create_trade_from_order(order,
close_status=None,
creation_time=0,
canceled_time=0,
executed_time=0,
exchange_trade_id=None):
def create_trade_from_order(order: "order_class.Order",
close_status: typing.Optional[enums.OrderStatus] = None,
creation_time: int = 0,
canceled_time: int = 0,
executed_time: int = 0,
exchange_trade_id: typing.Optional[str] = None) -> "trade_class.Trade":
if close_status is not None:
order.status = close_status
trade = trade_class.Trade(order.trader)
Expand All @@ -58,5 +61,5 @@ def create_trade_from_order(order,
return trade


def create_trade_from_dict(trader, trade_dict):
def create_trade_from_dict(trader, trade_dict: dict[str, typing.Any]) -> "trade_class.Trade":
return trade_class.Trade.from_dict(trader, trade_dict)
Loading
Loading