Skip to content
This repository was archived by the owner on Feb 3, 2026. It is now read-only.

Commit 024bc18

Browse files
committed
[FuturesPortfolioValueHolder] Fix open orders value calculation for symbol valuation
1 parent ebed5e0 commit 024bc18

File tree

8 files changed

+127
-9
lines changed

8 files changed

+127
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.5.11] - 2026-01-29
8+
### Fixed
9+
[PositionValueHolder] Fix futures open orders value calculation when using symbol instead of currency
10+
711
## [2.5.10] - 2026-01-29
812
### Fixed
913
[PositionsUpdater] Missing is_option is should_run check

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# OctoBot-Trading [2.5.10](https://github.com/Drakkar-Software/OctoBot-Trading/blob/master/CHANGELOG.md)
1+
# OctoBot-Trading [2.5.11](https://github.com/Drakkar-Software/OctoBot-Trading/blob/master/CHANGELOG.md)
22
[![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)
33
[![PyPI](https://img.shields.io/pypi/v/OctoBot-Trading.svg)](https://pypi.python.org/pypi/OctoBot-Trading/)
44
[![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)

octobot_trading/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
# License along with this library.
1616

1717
PROJECT_NAME = "OctoBot-Trading"
18-
VERSION = "2.5.10" # major.minor.revision
18+
VERSION = "2.5.11" # major.minor.revision

octobot_trading/exchange_data/ohlcv/channel/ohlcv_updater.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,9 @@ async def _candle_update_loop(self, time_frame, pair):
320320
except errors.UnSupportedSymbolError as err:
321321
self.logger.warning(
322322
f"{self.channel.exchange_manager.exchange_name} is not supporting {pair} on {time_frame.value}: {err}")
323-
await self._remove_unsupported_pairs(pair)
323+
if await self._remove_unsupported_pairs(pair):
324+
# Exit the loop after removing the pair to avoid endless retries
325+
return
324326
except errors.NotSupported as err:
325327
self.logger.warning(
326328
f"{self.channel.exchange_manager.exchange_name} is not supporting updates: {err}")
@@ -386,11 +388,13 @@ def _set_initialized(self, pair, time_frame, initialized):
386388
self.initialized_candles_by_tf_by_symbol[pair] = {}
387389
self.initialized_candles_by_tf_by_symbol[pair][time_frame] = initialized
388390

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

395399
async def resume(self) -> None:
396400
await super().resume()

octobot_trading/personal_data/orders/orders_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def create_group(
150150

151151
async def upsert_order_from_raw(self, exchange_order_id: str, raw_order: dict, is_from_exchange: bool) -> tuple[bool, order_class.Order]:
152152
if not self.has_order(None, exchange_order_id=exchange_order_id):
153-
self.logger.info(f"Including new order fetched from exchange: {raw_order}")
153+
self.logger.debug(f"Including new order fetched from exchange: {raw_order}")
154154
new_order = order_factory.create_order_instance_from_raw(self.trader, raw_order)
155155
# replace new_order by previously created pending_order if any relevant pending_order
156156
new_order = await self.get_and_update_pending_order(new_order) or new_order

octobot_trading/personal_data/portfolios/holders/futures_portfolio_value_holder.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ def get_holdings_ratio(
3838
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
3939
)
4040

41+
currency_is_full_symbol = symbol_util.is_symbol(currency)
4142
symbol = currency
42-
if not symbol_util.is_symbol(symbol):
43+
if not currency_is_full_symbol:
4344
try:
4445
symbol = symbol_util.merge_currencies(currency, self.portfolio_manager.reference_market, settlement_asset=self.portfolio_manager.reference_market)
4546
position = positions_manager.get_symbol_position(symbol, enums.PositionSide.BOTH)
@@ -64,8 +65,16 @@ def get_holdings_ratio(
6465
)
6566

6667
if include_assets_in_open_orders:
67-
assets_in_open_orders = self._get_total_holdings_in_open_orders(currency)
68-
current_in_order_value = self.value_converter.evaluate_value(currency, assets_in_open_orders, init_price_fetchers=False)
69-
position_value += current_in_order_value
68+
if currency_is_full_symbol:
69+
# For full symbols get orders by exact symbol
70+
pending_order_value = self._get_open_orders_value_for_symbol(symbol)
71+
position_value += pending_order_value
72+
else:
73+
# For simple currencies (e.g., "ETH"), use currency-based matching
74+
pending_order_holdings = self._get_total_holdings_in_open_orders(currency)
75+
pending_order_value = self.value_converter.evaluate_value(
76+
currency, pending_order_holdings, init_price_fetchers=False
77+
)
78+
position_value += pending_order_value
7079

7180
return position_value / total_portfolio_value if total_portfolio_value > constants.ZERO else constants.ZERO

octobot_trading/personal_data/portfolios/portfolio_value_holder.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,25 @@ def _get_orders_delta(self, currency: str) -> decimal.Decimal:
147147
assets_in_open_orders += order.total_cost
148148
return assets_in_open_orders
149149

150+
def _get_open_orders_value_for_symbol(self, symbol: str) -> decimal.Decimal:
151+
"""
152+
Get the net pending order value for a specific symbol.
153+
For options/futures, orders are matched by their full symbol.
154+
Buy orders increase the value (pending acquisition), sell orders decrease it (pending disposal).
155+
:param symbol: the full symbol (e.g., 'BTC/USDC:USDC')
156+
:return: the net pending order value in the settlement currency
157+
"""
158+
total_order_value = constants.ZERO
159+
orders_manager = self.portfolio_manager.exchange_manager.exchange_personal_data.orders_manager
160+
for order in orders_manager.get_open_orders(symbol=symbol):
161+
pending_quantity = order.origin_quantity - order.filled_quantity
162+
order_value = pending_quantity * order.origin_price
163+
if order.side is enums.TradeOrderSide.BUY:
164+
total_order_value += order_value
165+
elif order.side is enums.TradeOrderSide.SELL:
166+
total_order_value -= order_value
167+
return total_order_value
168+
150169
def handle_profitability_recalculation(self, force_recompute_origin_portfolio):
151170
"""
152171
Initialize values required by portfolio profitability to perform its profitability calculation

tests/personal_data/portfolios/test_portfolio_value_holder.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,3 +637,85 @@ async def test_get_orders_delta(backtesting_trader, currency, order_symbol, orde
637637

638638
assert isinstance(result, decimal.Decimal)
639639
assert result == expected_delta
640+
641+
642+
@pytest.mark.parametrize("backtesting_exchange_manager", ["spot", "margin", "futures", "options"], indirect=True)
643+
@pytest.mark.parametrize("order_side,order_quantity,filled_quantity,order_price,expected_value", [
644+
# Buy order: (quantity - filled) * price = (10 - 0) * 50 = 500
645+
(enums.TradeOrderSide.BUY, decimal.Decimal("10"), decimal.Decimal("0"), decimal.Decimal("50"), decimal.Decimal("500")),
646+
# Buy order partially filled: (10 - 3) * 50 = 350
647+
(enums.TradeOrderSide.BUY, decimal.Decimal("10"), decimal.Decimal("3"), decimal.Decimal("50"), decimal.Decimal("350")),
648+
# Sell order: decreases value by (quantity - filled) * price = -500
649+
(enums.TradeOrderSide.SELL, decimal.Decimal("10"), decimal.Decimal("0"), decimal.Decimal("50"), decimal.Decimal("-500")),
650+
# Sell order partially filled: -(10 - 5) * 50 = -250
651+
(enums.TradeOrderSide.SELL, decimal.Decimal("10"), decimal.Decimal("5"), decimal.Decimal("50"), decimal.Decimal("-250")),
652+
])
653+
async def test_get_open_orders_value_for_symbol(backtesting_trader, order_side, order_quantity, filled_quantity, order_price, expected_value):
654+
config, exchange_manager, trader = backtesting_trader
655+
portfolio_manager = exchange_manager.exchange_personal_data.portfolio_manager
656+
portfolio_value_holder = portfolio_manager.portfolio_value_holder
657+
658+
test_symbol = "slug/USDC:USDC-260331-0-YES"
659+
660+
# Create a real order object based on the side
661+
if order_side == enums.TradeOrderSide.BUY:
662+
order = personal_data.BuyLimitOrder(trader)
663+
else:
664+
order = personal_data.SellLimitOrder(trader)
665+
666+
order.update(order_type=enums.TraderOrderType.BUY_LIMIT if order_side == enums.TradeOrderSide.BUY else enums.TraderOrderType.SELL_LIMIT,
667+
symbol=test_symbol,
668+
current_price=order_price,
669+
quantity=order_quantity,
670+
price=order_price)
671+
order.filled_quantity = filled_quantity
672+
673+
with mock.patch.object(
674+
portfolio_manager.exchange_manager.exchange_personal_data.orders_manager,
675+
"get_open_orders",
676+
return_value=[order]
677+
):
678+
result = portfolio_value_holder._get_open_orders_value_for_symbol(test_symbol)
679+
680+
assert isinstance(result, decimal.Decimal)
681+
assert result == expected_value
682+
683+
684+
@pytest.mark.parametrize("backtesting_exchange_manager", ["spot", "margin", "futures", "options"], indirect=True)
685+
async def test_get_open_orders_value_for_symbol_multiple_orders(backtesting_trader):
686+
"""Test that multiple orders for the same symbol are summed correctly."""
687+
config, exchange_manager, trader = backtesting_trader
688+
portfolio_manager = exchange_manager.exchange_personal_data.portfolio_manager
689+
portfolio_value_holder = portfolio_manager.portfolio_value_holder
690+
691+
test_symbol = "slug/USDC:USDC-260331-0-YES"
692+
693+
# Buy order: 10 * 50 = 500
694+
buy_order = personal_data.BuyLimitOrder(trader)
695+
buy_order.update(order_type=enums.TraderOrderType.BUY_LIMIT,
696+
symbol=test_symbol,
697+
current_price=decimal.Decimal("50"),
698+
quantity=decimal.Decimal("10"),
699+
price=decimal.Decimal("50"))
700+
buy_order.filled_quantity = decimal.Decimal("0")
701+
702+
# Sell order: -5 * 40 = -200
703+
sell_order = personal_data.SellLimitOrder(trader)
704+
sell_order.update(order_type=enums.TraderOrderType.SELL_LIMIT,
705+
symbol=test_symbol,
706+
current_price=decimal.Decimal("40"),
707+
quantity=decimal.Decimal("5"),
708+
price=decimal.Decimal("40"))
709+
sell_order.filled_quantity = decimal.Decimal("0")
710+
711+
# Net value: 500 - 200 = 300
712+
with mock.patch.object(
713+
portfolio_manager.exchange_manager.exchange_personal_data.orders_manager,
714+
"get_open_orders",
715+
return_value=[buy_order, sell_order]
716+
):
717+
result = portfolio_value_holder._get_open_orders_value_for_symbol(test_symbol)
718+
719+
assert isinstance(result, decimal.Decimal)
720+
assert result == decimal.Decimal("300")
721+

0 commit comments

Comments
 (0)