Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Index #1040

Merged
merged 6 commits into from
Mar 24, 2024
Merged

Index #1040

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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ 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.67] - 2024-03-23
### Updated
- [TradingMode] Requirements for indexes
- [Orders] Use creation_time in to_dict

## [2.4.66] - 2024-03-19
### Updated
- [CCXT] to 4.2.77
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.66](https://github.com/Drakkar-Software/OctoBot-Trading/blob/master/CHANGELOG.md)
# OctoBot-Trading [2.4.67](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.66" # major.minor.revision
VERSION = "2.4.67" # major.minor.revision
2 changes: 2 additions & 0 deletions octobot_trading/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
from octobot_trading.api.modes import (
get_trading_modes,
get_trading_mode_symbol,
is_trading_mode_symbol_wildcard,
get_trading_mode_followed_strategy_signals_identifier,
get_trading_mode_current_state,
get_activated_trading_mode,
Expand Down Expand Up @@ -301,6 +302,7 @@
"stop_exchange",
"get_trading_modes",
"get_trading_mode_symbol",
"is_trading_mode_symbol_wildcard",
"get_trading_mode_followed_strategy_signals_identifier",
"get_trading_mode_current_state",
"get_activated_trading_mode",
Expand Down
4 changes: 4 additions & 0 deletions octobot_trading/api/modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def get_trading_mode_symbol(trading_mode) -> list:
return trading_mode.symbol


def is_trading_mode_symbol_wildcard(trading_mode) -> bool:
return trading_mode.get_is_symbol_wildcard()


def get_trading_mode_followed_strategy_signals_identifier(trading_mode) -> str:
if trading_mode.is_following_trading_signals():
return trading_mode.trading_config.get(commons_constants.CONFIG_TRADING_SIGNALS_STRATEGY, "")
Expand Down
1 change: 1 addition & 0 deletions octobot_trading/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@

DEFAULT_INITIALIZATION_EVENT_TOPICS = [
commons_enums.InitializationEventExchangeTopics.BALANCE,
commons_enums.InitializationEventExchangeTopics.PROFITABILITY,
commons_enums.InitializationEventExchangeTopics.ORDERS,
commons_enums.InitializationEventExchangeTopics.TRADES,
commons_enums.InitializationEventExchangeTopics.CANDLES,
Expand Down
36 changes: 21 additions & 15 deletions octobot_trading/modes/channel/abstract_mode_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,24 @@ async def internal_callback(self, trading_mode_name, cryptocurrency, symbol, tim
f"the order being refused by the exchange.")

def get_minimal_funds_error(self, symbol, final_note):
market_status = self.exchange_manager.exchange.get_market_status(symbol, price_example=None, with_fixer=False)
try:
base, quote = symbol_util.parse_symbol(symbol).base_and_quote()
portfolio = self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio
funds = {
base: portfolio.get_currency_portfolio(base),
quote: portfolio.get_currency_portfolio(quote)
}
except Exception as err:
self.logger.error(f"Error when getting funds for {symbol}: {err}")
funds = {}
if symbol is None:
return (
f"Not enough funds to create new orders after {final_note} evaluation: "
f"{self.exchange_manager.exchange_name} exchange minimal order "
f"volume has not been reached."
)
Comment on lines +73 to +77
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the reason why symbol is None?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in index trading, we don't send a given symbol to buy, we tell the consumer to "rebalance the index" and the consumer will create orders accordingly (it's not symbol related).

Currently:

  • producer decides if a rebalance should happen
  • consumer executes the rebalance

Copy link
Member

@Herklos Herklos Mar 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(it's not symbol related).

That's my point, I don't understand why we trigger this error here when symbol is None. I don't think it's a good idea to trigger this error due to the expected caller

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function's goal is to log an error, if it's called it's that the error already occured and we want inform the user that an error occured. This symbol condition allows to create the error without given symbols, which is a case that can now happen

else:
market_status = self.exchange_manager.exchange.get_market_status(symbol, price_example=None, with_fixer=False)
try:
base, quote = symbol_util.parse_symbol(symbol).base_and_quote()
portfolio = self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio
funds = {
base: portfolio.get_currency_portfolio(base),
quote: portfolio.get_currency_portfolio(quote)
}
except Exception as err:
self.logger.error(f"Error when getting funds for {symbol}: {err}")
funds = {}
return (
f"Not enough funds to create a new {symbol} order after {final_note} evaluation: "
f"{self.exchange_manager.exchange_name} exchange minimal order "
Expand Down Expand Up @@ -135,6 +142,9 @@ async def create_order_if_possible(self, symbol, final_note, state, **kwargs) ->

# Can be overwritten
async def can_create_order(self, symbol, state):
if symbol is None:
# can't check
return True
currency, market = symbol_util.parse_symbol(symbol).base_and_quote()
portfolio = self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio

Expand Down Expand Up @@ -191,10 +201,6 @@ async def can_create_order(self, symbol, state):
self.logger.debug("can_create_order: return False")
return False

def get_holdings_ratio(self, currency):
return self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder \
.get_currency_holding_ratio(currency)

def get_number_of_traded_assets(self):
return len(self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder
.origin_crypto_currencies_values)
Expand Down
10 changes: 10 additions & 0 deletions octobot_trading/modes/channel/abstract_mode_producer.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,16 @@ async def _apply_exchange_side_config(self, context):
common_enums.InitializationEventExchangeTopics.CONTRACTS.value)
await script_keywords.set_leverage(context, await script_keywords.user_select_leverage(context))

async def _wait_for_symbol_prices_and_profitability_init(self, timeout) -> bool:
try:
await util.wait_for_topic_init(self.exchange_manager, timeout,
common_enums.InitializationEventExchangeTopics.PRICE.value)
await util.wait_for_topic_init(self.exchange_manager, timeout,
common_enums.InitializationEventExchangeTopics.PROFITABILITY.value)
except (asyncio.TimeoutError, concurrent.futures.TimeoutError):
self.logger.error(f"Symbol price initialization took more than {timeout} seconds")
return False

@classmethod
def producer_exchange_wide_lock(cls, exchange_manager) -> asyncio_tools.RLock():
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,6 @@ async def available_account_balance(context, side=trading_enums.TradeOrderSide.B
- already_locked_amount


def get_holdings_value(context, assets, target_unit):
total_value = trading_constants.ZERO
portfolio = context.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio
converter = context.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter
for asset, asset_holdings in portfolio.items():
if asset not in assets:
continue
try:
total_value += converter.evaluate_value(
asset, asset_holdings.total, raise_error=True, target_currency=target_unit, init_price_fetchers=False
)
except trading_errors.MissingPriceDataError:
context.logger.info(f"Missing {asset} price conversion, ignoring {float(asset_holdings.total)} holdings.")
return total_value


def _get_locked_amount_in_stop_orders(context, side):
locked_amount = trading_constants.ZERO
for order in context.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(context.symbol):
Expand Down
12 changes: 8 additions & 4 deletions octobot_trading/modes/script_keywords/basic_keywords/amount.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ async def get_amount_from_input_amount(
if not context.symbol:
raise trading_errors.InvalidArgumentError(f"{amount_type} input types requires context.symbol to be set")
base, quote = commons_symbols.parse_symbol(context.symbol).base_and_quote()
total_symbol_assets_holdings_value = account_balance.get_holdings_value(context, (base, quote), base)
total_symbol_assets_holdings_value = context.exchange_manager.exchange_personal_data.portfolio_manager.\
portfolio_value_holder.get_assets_holdings_value(
(base, quote), commons_symbols.parse_symbol(context.symbol).base
)
amount_value = total_symbol_assets_holdings_value * amount_value / trading_constants.ONE_HUNDRED
elif amount_type is dsl.QuantityType.TRADED_SYMBOLS_ASSETS_PERCENT:
if not context.symbol:
Expand All @@ -68,9 +71,10 @@ async def get_amount_from_input_amount(
for symbol in context.exchange_manager.exchange_config.traded_symbols:
assets.add(symbol.base)
assets.add(symbol.quote)
total_symbol_assets_holdings_value = account_balance.get_holdings_value(
context, assets, commons_symbols.parse_symbol(context.symbol).base
)
total_symbol_assets_holdings_value = context.exchange_manager.exchange_personal_data.portfolio_manager.\
portfolio_value_holder.get_assets_holdings_value(
assets, commons_symbols.parse_symbol(context.symbol).base
)
amount_value = total_symbol_assets_holdings_value * amount_value / trading_constants.ONE_HUNDRED
elif amount_type is dsl.QuantityType.POSITION_PERCENT:
raise NotImplementedError(amount_type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# License along with this library.
import octobot_commons.logging as logging
import octobot_commons.symbols as symbol_util
import octobot_commons.tree as commons_tree
import octobot_commons.enums as commons_enums

import octobot_trading.util as util
import octobot_trading.constants as constants
Expand Down Expand Up @@ -66,14 +68,25 @@ def update_profitability(self, force_recompute_origin_portfolio=False):
"""
self._reset_before_profitability_calculation()
try:
set_init_event = self.portfolio_manager.portfolio_value_holder.portfolio_current_value == constants.ZERO
self.portfolio_manager.handle_profitability_recalculation(force_recompute_origin_portfolio)
self._update_profitability_calculation()
if set_init_event:
self._set_initialized_event()
return self.profitability_diff != constants.ZERO
except KeyError as missing_data_exception:
self.logger.warning(f"Missing {missing_data_exception} ticker data to calculate profitability")
except Exception as missing_data_exception:
self.logger.exception(missing_data_exception, True, str(missing_data_exception))

def _set_initialized_event(self):
commons_tree.EventProvider.instance().trigger_event(
self.portfolio_manager.exchange_manager.bot_id, commons_tree.get_exchange_path(
self.portfolio_manager.exchange_manager.exchange_name,
commons_enums.InitializationEventExchangeTopics.PROFITABILITY.value
)
)

def _reset_before_profitability_calculation(self):
"""
Prepare profitability calculation
Expand Down
55 changes: 44 additions & 11 deletions octobot_trading/personal_data/portfolios/portfolio_value_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import octobot_trading.constants as constants
import octobot_trading.errors as errors
import octobot_trading.enums as enums
import octobot_trading.personal_data.portfolios.value_converter as value_converter


Expand Down Expand Up @@ -116,17 +117,49 @@ def get_current_holdings_values(self):
for currency in holdings.keys()
}

def get_currency_holding_ratio(self, currency):
"""
Return the holdings ratio for the specified currency
:return: the holdings ratio
"""
if self.portfolio_current_value:
return self.value_converter.evaluate_value(
currency,
self.portfolio_manager.portfolio.get_currency_portfolio(currency).total
) / self.portfolio_current_value
return constants.ZERO
def get_assets_holdings_value(self, assets, target_unit, init_price_fetchers=False):
total_value = constants.ZERO
for asset, asset_holdings in self.portfolio_manager.portfolio.portfolio.items():
if asset not in assets:
continue
try:
total_value += self.value_converter.evaluate_value(
asset, asset_holdings.total, raise_error=True,
target_currency=target_unit, init_price_fetchers=init_price_fetchers
)
except errors.MissingPriceDataError:
self.logger.info(f"Missing {asset} price conversion, ignoring {float(asset_holdings.total)} holdings.")
return total_value

def get_holdings_ratio(self, currency, traded_symbols_only=False, include_assets_in_open_orders=False):
if traded_symbols_only:
# only consider traded assets for total_holdings_value
assets = set()
for symbol in self.portfolio_manager.exchange_manager.exchange_config.traded_symbols:
assets.add(symbol.base)
assets.add(symbol.quote)
total_holdings_value = self.get_assets_holdings_value(
assets, self.portfolio_manager.reference_market
)
else:
# consider all assets for total_holdings_value
total_holdings_value = self.portfolio_current_value
if not total_holdings_value:
return constants.ZERO
currency_holdings = self.portfolio_manager.portfolio.get_currency_portfolio(currency).total
if include_assets_in_open_orders:
# add assets in open orders to currency_holdings
assets_in_open_orders = constants.ZERO
for order in self.portfolio_manager.exchange_manager.exchange_personal_data.orders_manager.get_open_orders():
symbol = symbol_util.parse_symbol(order.symbol)
if order.side is enums.TradeOrderSide.BUY and symbol.base == currency:
assets_in_open_orders += order.origin_quantity
elif order.side is enums.TradeOrderSide.SELL and symbol.quote == currency:
assets_in_open_orders += order.total_cost
currency_holdings += assets_in_open_orders
# compute ration
current_holdings_value = self.value_converter.evaluate_value(currency, currency_holdings)
return current_holdings_value / total_holdings_value

def handle_profitability_recalculation(self, force_recompute_origin_portfolio):
"""
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ numpy==1.26.3
# Drakkar-Software requirements
OctoBot-Backtesting>=1.9, <1.10
Async-Channel>=2.2, <2.3
OctoBot-Commons>=1.9, <1.10
OctoBot-Commons>=1.9.43, <1.10
OctoBot-Tentacles-Manager>=2.9, <2.10
trading-backend>=1.2.7

Expand Down
18 changes: 0 additions & 18 deletions tests/modes/script_keywords/basic_keywords/test_account_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,6 @@ async def test_available_account_balance(mock_context):
_get_locked_amount_in_stop_orders_mock.reset_mock()


async def test_get_holdings_value(mock_context):
portfolio = mock_context.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio
last_prices = mock_context.exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.\
value_converter.last_prices_by_trading_pair
# no provided input asset
assert account_balance.get_holdings_value(mock_context, (), "USDT") == trading_constants.ZERO
# missing currency
assert account_balance.get_holdings_value(mock_context, {"BTC", "ETH"}, "PLOP") == trading_constants.ZERO
assert account_balance.get_holdings_value(mock_context, ("BTC"), "BTC") == portfolio["BTC"].total
assert account_balance.get_holdings_value(mock_context, ("BTC"), "BTC") != portfolio["BTC"].available
assert account_balance.get_holdings_value(mock_context, ("BTC"), "USDT") == \
portfolio["BTC"].total * last_prices["BTC/USDT"]
assert account_balance.get_holdings_value(mock_context, {"BTC", "ETH"}, "USDT") == \
portfolio["BTC"].total * last_prices["BTC/USDT"] + portfolio["ETH"].total * last_prices["ETH/USDT"]
assert account_balance.get_holdings_value(mock_context, {"BTC", "ETH"}, "ETH") == \
portfolio["BTC"].total / last_prices["ETH/BTC"] + portfolio["ETH"].total


async def test_adapt_amount_to_holdings(null_context):
with mock.patch.object(account_balance, "available_account_balance",
mock.AsyncMock(return_value=decimal.Decimal(1))) as available_account_balance_mock:
Expand Down
Loading
Loading