Skip to content
This repository was archived by the owner on Feb 3, 2026. It is now read-only.
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ 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.5.11] - 2026-01-29
### Fixed
[PositionValueHolder] Fix futures open orders value calculation when using symbol instead of currency

## [2.5.10] - 2026-01-29
### Fixed
[PositionsUpdater] Missing is_option is should_run check
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.5.10](https://github.com/Drakkar-Software/OctoBot-Trading/blob/master/CHANGELOG.md)
# OctoBot-Trading [2.5.11](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.5.10" # major.minor.revision
VERSION = "2.5.11" # major.minor.revision
8 changes: 6 additions & 2 deletions octobot_trading/exchange_data/ohlcv/channel/ohlcv_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ async def _candle_update_loop(self, time_frame, pair):
except errors.UnSupportedSymbolError as err:
self.logger.warning(
f"{self.channel.exchange_manager.exchange_name} is not supporting {pair} on {time_frame.value}: {err}")
await self._remove_unsupported_pairs(pair)
if await self._remove_unsupported_pairs(pair):
# Exit the loop after removing the pair to avoid endless retries
return
except errors.NotSupported as err:
self.logger.warning(
f"{self.channel.exchange_manager.exchange_name} is not supporting updates: {err}")
Expand Down Expand Up @@ -386,11 +388,13 @@ def _set_initialized(self, pair, time_frame, initialized):
self.initialized_candles_by_tf_by_symbol[pair] = {}
self.initialized_candles_by_tf_by_symbol[pair][time_frame] = initialized

async def _remove_unsupported_pairs(self, pair: str):
async def _remove_unsupported_pairs(self, pair: str) -> bool:
# For now only remove the pair from the traded pairs if it's an option exchange
if self.channel.exchange_manager.is_option:
self.logger.warning(f"Removing {pair} from traded pairs...")
await self.channel.exchange_manager.exchange_config.remove_traded_symbols([pair])
return True
return False

async def resume(self) -> None:
await super().resume()
Expand Down
2 changes: 1 addition & 1 deletion octobot_trading/personal_data/orders/orders_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def create_group(

async def upsert_order_from_raw(self, exchange_order_id: str, raw_order: dict, is_from_exchange: bool) -> tuple[bool, order_class.Order]:
if not self.has_order(None, exchange_order_id=exchange_order_id):
self.logger.info(f"Including new order fetched from exchange: {raw_order}")
self.logger.debug(f"Including new order fetched from exchange: {raw_order}")
new_order = order_factory.create_order_instance_from_raw(self.trader, raw_order)
# replace new_order by previously created pending_order if any relevant pending_order
new_order = await self.get_and_update_pending_order(new_order) or new_order
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ def get_holdings_ratio(
self.portfolio_manager.reference_market, traded_symbols_only=traded_symbols_only, coins_whitelist=coins_whitelist, include_assets_in_open_orders=include_assets_in_open_orders
)

currency_is_full_symbol = symbol_util.is_symbol(currency)
symbol = currency
if not symbol_util.is_symbol(symbol):
if not currency_is_full_symbol:
try:
symbol = symbol_util.merge_currencies(currency, self.portfolio_manager.reference_market, settlement_asset=self.portfolio_manager.reference_market)
position = positions_manager.get_symbol_position(symbol, enums.PositionSide.BOTH)
Expand All @@ -64,8 +65,16 @@ def get_holdings_ratio(
)

if include_assets_in_open_orders:
assets_in_open_orders = self._get_total_holdings_in_open_orders(currency)
current_in_order_value = self.value_converter.evaluate_value(currency, assets_in_open_orders, init_price_fetchers=False)
position_value += current_in_order_value
if currency_is_full_symbol:
# For full symbols get orders by exact symbol
pending_order_value = self._get_open_orders_value_for_symbol(symbol)
position_value += pending_order_value
else:
# For simple currencies (e.g., "ETH"), use currency-based matching
pending_order_holdings = self._get_total_holdings_in_open_orders(currency)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

pending_order_value = self.value_converter.evaluate_value(
currency, pending_order_holdings, init_price_fetchers=False
)
position_value += pending_order_value

return position_value / total_portfolio_value if total_portfolio_value > constants.ZERO else constants.ZERO
19 changes: 19 additions & 0 deletions octobot_trading/personal_data/portfolios/portfolio_value_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,25 @@ def _get_orders_delta(self, currency: str) -> decimal.Decimal:
assets_in_open_orders += order.total_cost
return assets_in_open_orders

def _get_open_orders_value_for_symbol(self, symbol: str) -> decimal.Decimal:
"""
Get the net pending order value for a specific symbol.
For options/futures, orders are matched by their full symbol.
Buy orders increase the value (pending acquisition), sell orders decrease it (pending disposal).
:param symbol: the full symbol (e.g., 'BTC/USDC:USDC')
:return: the net pending order value in the settlement currency
"""
total_order_value = constants.ZERO
orders_manager = self.portfolio_manager.exchange_manager.exchange_personal_data.orders_manager
for order in orders_manager.get_open_orders(symbol=symbol):
pending_quantity = order.origin_quantity - order.filled_quantity
order_value = pending_quantity * order.origin_price
if order.side is enums.TradeOrderSide.BUY:
total_order_value += order_value
elif order.side is enums.TradeOrderSide.SELL:
total_order_value -= order_value
return total_order_value

def handle_profitability_recalculation(self, force_recompute_origin_portfolio):
"""
Initialize values required by portfolio profitability to perform its profitability calculation
Expand Down
82 changes: 82 additions & 0 deletions tests/personal_data/portfolios/test_portfolio_value_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,85 @@ async def test_get_orders_delta(backtesting_trader, currency, order_symbol, orde

assert isinstance(result, decimal.Decimal)
assert result == expected_delta


@pytest.mark.parametrize("backtesting_exchange_manager", ["spot", "margin", "futures", "options"], indirect=True)
@pytest.mark.parametrize("order_side,order_quantity,filled_quantity,order_price,expected_value", [
# Buy order: (quantity - filled) * price = (10 - 0) * 50 = 500
(enums.TradeOrderSide.BUY, decimal.Decimal("10"), decimal.Decimal("0"), decimal.Decimal("50"), decimal.Decimal("500")),
# Buy order partially filled: (10 - 3) * 50 = 350
(enums.TradeOrderSide.BUY, decimal.Decimal("10"), decimal.Decimal("3"), decimal.Decimal("50"), decimal.Decimal("350")),
# Sell order: decreases value by (quantity - filled) * price = -500
(enums.TradeOrderSide.SELL, decimal.Decimal("10"), decimal.Decimal("0"), decimal.Decimal("50"), decimal.Decimal("-500")),
# Sell order partially filled: -(10 - 5) * 50 = -250
(enums.TradeOrderSide.SELL, decimal.Decimal("10"), decimal.Decimal("5"), decimal.Decimal("50"), decimal.Decimal("-250")),
])
async def test_get_open_orders_value_for_symbol(backtesting_trader, order_side, order_quantity, filled_quantity, order_price, expected_value):
config, exchange_manager, trader = backtesting_trader
portfolio_manager = exchange_manager.exchange_personal_data.portfolio_manager
portfolio_value_holder = portfolio_manager.portfolio_value_holder

test_symbol = "slug/USDC:USDC-260331-0-YES"

# Create a real order object based on the side
if order_side == enums.TradeOrderSide.BUY:
order = personal_data.BuyLimitOrder(trader)
else:
order = personal_data.SellLimitOrder(trader)

order.update(order_type=enums.TraderOrderType.BUY_LIMIT if order_side == enums.TradeOrderSide.BUY else enums.TraderOrderType.SELL_LIMIT,
symbol=test_symbol,
current_price=order_price,
quantity=order_quantity,
price=order_price)
order.filled_quantity = filled_quantity

with mock.patch.object(
portfolio_manager.exchange_manager.exchange_personal_data.orders_manager,
"get_open_orders",
return_value=[order]
):
result = portfolio_value_holder._get_open_orders_value_for_symbol(test_symbol)

assert isinstance(result, decimal.Decimal)
assert result == expected_value


@pytest.mark.parametrize("backtesting_exchange_manager", ["spot", "margin", "futures", "options"], indirect=True)
async def test_get_open_orders_value_for_symbol_multiple_orders(backtesting_trader):
"""Test that multiple orders for the same symbol are summed correctly."""
config, exchange_manager, trader = backtesting_trader
portfolio_manager = exchange_manager.exchange_personal_data.portfolio_manager
portfolio_value_holder = portfolio_manager.portfolio_value_holder

test_symbol = "slug/USDC:USDC-260331-0-YES"

# Buy order: 10 * 50 = 500
buy_order = personal_data.BuyLimitOrder(trader)
buy_order.update(order_type=enums.TraderOrderType.BUY_LIMIT,
symbol=test_symbol,
current_price=decimal.Decimal("50"),
quantity=decimal.Decimal("10"),
price=decimal.Decimal("50"))
buy_order.filled_quantity = decimal.Decimal("0")

# Sell order: -5 * 40 = -200
sell_order = personal_data.SellLimitOrder(trader)
sell_order.update(order_type=enums.TraderOrderType.SELL_LIMIT,
symbol=test_symbol,
current_price=decimal.Decimal("40"),
quantity=decimal.Decimal("5"),
price=decimal.Decimal("40"))
sell_order.filled_quantity = decimal.Decimal("0")

# Net value: 500 - 200 = 300
with mock.patch.object(
portfolio_manager.exchange_manager.exchange_personal_data.orders_manager,
"get_open_orders",
return_value=[buy_order, sell_order]
):
result = portfolio_value_holder._get_open_orders_value_for_symbol(test_symbol)

assert isinstance(result, decimal.Decimal)
assert result == decimal.Decimal("300")