Skip to content

Commit a7119fe

Browse files
fengtalityclaude
andcommitted
feat: add orchestrator support and LP history API
- Add SwapExecutor to executor orchestrator registry - Fix orchestrator exit hang (break vs continue in stop loop) - Fix position hold edge cases (division by zero, NaN handling) - Add LP history JSON API via MQTT endpoint - Add lphistory JSON export for API consumption - Add simple_xemm_gateway example script Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e9e0110 commit a7119fe

8 files changed

Lines changed: 459 additions & 38 deletions

File tree

hummingbot/client/command/lphistory_command.py

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import time
33
from datetime import datetime
44
from decimal import Decimal
5-
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
5+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
66

77
import pandas as pd
88

@@ -69,6 +69,44 @@ def lphistory(self, # type: HummingbotApplication
6969

7070
safe_ensure_future(self._lp_performance_report(start_time, updates, precision))
7171

72+
def get_lp_history_json(self, # type: HummingbotApplication
73+
days: float = 0) -> List[Dict[str, Any]]:
74+
"""Get LP history as JSON for MQTT/API consumption."""
75+
if self.strategy_file_name is None:
76+
return []
77+
start_time = get_timestamp(days) if days > 0 else self.init_time
78+
with self.trading_core.trade_fill_db.get_new_session() as session:
79+
updates: List[RangePositionUpdate] = self._get_lp_updates_from_session(
80+
int(start_time * 1e3),
81+
session=session,
82+
config_file_path=self.strategy_file_name
83+
)
84+
return [self._lp_update_to_json(u) for u in updates]
85+
86+
@staticmethod
87+
def _lp_update_to_json(update: RangePositionUpdate) -> Dict[str, Any]:
88+
"""Convert a RangePositionUpdate record to JSON format for API."""
89+
return {
90+
"id": update.hb_id,
91+
"timestamp": update.timestamp,
92+
"tx_hash": update.tx_hash,
93+
"market": update.market,
94+
"trading_pair": update.trading_pair,
95+
"order_action": update.order_action,
96+
"position_address": update.position_address,
97+
"lower_price": update.lower_price,
98+
"upper_price": update.upper_price,
99+
"mid_price": update.mid_price,
100+
"base_amount": update.base_amount,
101+
"quote_amount": update.quote_amount,
102+
"base_fee": update.base_fee,
103+
"quote_fee": update.quote_fee,
104+
"trade_fee": update.trade_fee,
105+
"trade_fee_in_quote": update.trade_fee_in_quote,
106+
"position_rent": update.position_rent,
107+
"position_rent_refunded": update.position_rent_refunded,
108+
}
109+
72110
def _get_lp_updates_from_session(
73111
self, # type: HummingbotApplication
74112
start_timestamp: int,
@@ -243,28 +281,17 @@ async def _report_pair_performance(self, # type: HummingbotApplication
243281
closed_position_count = len(closed_positions)
244282
lines.append(f"Positions Opened: {open_position_count + closed_position_count} | Positions Closed: {closed_position_count}")
245283

246-
# Closed Positions table - grouped by side (buy=quote only, sell=base only, both=double-sided)
247-
# Determine side based on ADD amounts: base only=sell, quote only=buy, both=both
248-
buy_positions = [(o, c) for o, c in zip(opens, closes) if o.base_amount == 0 or o.base_amount is None]
249-
sell_positions = [(o, c) for o, c in zip(opens, closes) if o.quote_amount == 0 or o.quote_amount is None]
250-
both_positions = [(o, c) for o, c in zip(opens, closes)
251-
if (o, c) not in buy_positions and (o, c) not in sell_positions]
284+
# Closed Positions summary
285+
total_volume_base = sum(Decimal(str(o.base_amount or 0)) + Decimal(str(c.base_amount or 0)) for o, c in zip(opens, closes))
286+
total_volume_quote = sum(Decimal(str(o.quote_amount or 0)) + Decimal(str(c.quote_amount or 0)) for o, c in zip(opens, closes))
252287

253-
# Column order matches side values: both(0), buy(1), sell(2)
254-
pos_columns = ["", "both", "buy", "sell"]
255288
pos_data = [
256-
[f"{'Number of positions':<27}", len(both_positions), len(buy_positions), len(sell_positions)],
257-
[f"{f'Total volume ({base})':<27}",
258-
smart_round(sum(Decimal(str(o.base_amount or 0)) + Decimal(str(c.base_amount or 0)) for o, c in both_positions), precision),
259-
smart_round(sum(Decimal(str(o.base_amount or 0)) + Decimal(str(c.base_amount or 0)) for o, c in buy_positions), precision),
260-
smart_round(sum(Decimal(str(o.base_amount or 0)) + Decimal(str(c.base_amount or 0)) for o, c in sell_positions), precision)],
261-
[f"{f'Total volume ({quote})':<27}",
262-
smart_round(sum(Decimal(str(o.quote_amount or 0)) + Decimal(str(c.quote_amount or 0)) for o, c in both_positions), precision),
263-
smart_round(sum(Decimal(str(o.quote_amount or 0)) + Decimal(str(c.quote_amount or 0)) for o, c in buy_positions), precision),
264-
smart_round(sum(Decimal(str(o.quote_amount or 0)) + Decimal(str(c.quote_amount or 0)) for o, c in sell_positions), precision)],
289+
["Number of positions ", closed_position_count],
290+
[f"Total volume ({base}) ", smart_round(total_volume_base, precision)],
291+
[f"Total volume ({quote}) ", smart_round(total_volume_quote, precision)],
265292
]
266-
pos_df = pd.DataFrame(data=pos_data, columns=pos_columns)
267-
lines.extend(["", " Closed Positions:"] + [" " + line for line in pos_df.to_string(index=False).split("\n")])
293+
pos_df = pd.DataFrame(data=pos_data)
294+
lines.extend(["", " Closed Positions:"] + [" " + line for line in pos_df.to_string(index=False, header=False).split("\n")])
268295

269296
# Assets table
270297
assets_columns = ["", "add", "remove", "fees"]

hummingbot/remote_iface/messages.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ class Response(RPCMessage.Response):
107107
trades: Optional[List[Any]] = []
108108

109109

110+
class LPHistoryCommandMessage(RPCMessage):
111+
class Request(RPCMessage.Request):
112+
days: Optional[float] = 0
113+
verbose: Optional[bool] = False
114+
precision: Optional[int] = None
115+
async_backend: Optional[bool] = True
116+
117+
class Response(RPCMessage.Response):
118+
status: Optional[int] = MQTT_STATUS_CODE.SUCCESS
119+
msg: Optional[str] = ''
120+
lp_updates: Optional[List[Any]] = []
121+
122+
110123
class BalanceLimitCommandMessage(RPCMessage):
111124
class Request(RPCMessage.Request):
112125
exchange: str

hummingbot/remote_iface/mqtt.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
ImportCommandMessage,
4141
InternalEventMessage,
4242
LogMessage,
43+
LPHistoryCommandMessage,
4344
NotifyMessage,
4445
StartCommandMessage,
4546
StatusCommandMessage,
@@ -57,6 +58,7 @@ class CommandTopicSpecs:
5758
IMPORT: str = '/import'
5859
STATUS: str = '/status'
5960
HISTORY: str = '/history'
61+
LP_HISTORY: str = '/lphistory'
6062
BALANCE_LIMIT: str = '/balance/limit'
6163
BALANCE_PAPER: str = '/balance/paper'
6264

@@ -95,6 +97,7 @@ def __init__(self,
9597
self._import_uri = f'{topic_prefix}{TopicSpecs.COMMANDS.IMPORT}'
9698
self._status_uri = f'{topic_prefix}{TopicSpecs.COMMANDS.STATUS}'
9799
self._history_uri = f'{topic_prefix}{TopicSpecs.COMMANDS.HISTORY}'
100+
self._lp_history_uri = f'{topic_prefix}{TopicSpecs.COMMANDS.LP_HISTORY}'
98101
self._balance_limit_uri = f'{topic_prefix}{TopicSpecs.COMMANDS.BALANCE_LIMIT}'
99102
self._balance_paper_uri = f'{topic_prefix}{TopicSpecs.COMMANDS.BALANCE_PAPER}'
100103

@@ -131,6 +134,11 @@ def _init_commands(self):
131134
msg_type=HistoryCommandMessage,
132135
on_request=self._on_cmd_history
133136
)
137+
self._node.create_rpc(
138+
rpc_name=self._lp_history_uri,
139+
msg_type=LPHistoryCommandMessage,
140+
on_request=self._on_cmd_lp_history
141+
)
134142
self._node.create_rpc(
135143
rpc_name=self._balance_limit_uri,
136144
msg_type=BalanceLimitCommandMessage,
@@ -315,6 +323,20 @@ def _on_cmd_history(self, msg: HistoryCommandMessage.Request):
315323
response.msg = str(e)
316324
return response
317325

326+
def _on_cmd_lp_history(self, msg: LPHistoryCommandMessage.Request):
327+
response = LPHistoryCommandMessage.Response()
328+
try:
329+
if msg.async_backend:
330+
self._hb_app.lphistory(msg.days, msg.verbose, msg.precision)
331+
else:
332+
lp_updates = self._hb_app.get_lp_history_json(msg.days)
333+
if lp_updates:
334+
response.lp_updates = lp_updates
335+
except Exception as e:
336+
response.status = MQTT_STATUS_CODE.ERROR
337+
response.msg = str(e)
338+
return response
339+
318340
def _on_cmd_balance_limit(self, msg: BalanceLimitCommandMessage.Request):
319341
response = BalanceLimitCommandMessage.Response()
320342
try:

hummingbot/strategy_v2/executors/data_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
class ExecutorConfigBase(BaseModel):
1515
id: str = None # Make ID optional
1616
type: Literal["position_executor", "dca_executor", "grid_executor", "order_executor",
17-
"xemm_executor", "arbitrage_executor", "twap_executor", "lp_executor"]
17+
"xemm_executor", "arbitrage_executor", "twap_executor", "lp_executor", "swap_executor"]
1818
timestamp: Optional[float] = None
1919
controller_id: str = "main"
2020

hummingbot/strategy_v2/executors/executor_orchestrator.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from hummingbot.strategy_v2.executors.lp_executor.lp_executor import LPExecutor
2121
from hummingbot.strategy_v2.executors.order_executor.order_executor import OrderExecutor
2222
from hummingbot.strategy_v2.executors.position_executor.position_executor import PositionExecutor
23+
from hummingbot.strategy_v2.executors.swap_executor.swap_executor import SwapExecutor
2324
from hummingbot.strategy_v2.executors.twap_executor.twap_executor import TWAPExecutor
2425
from hummingbot.strategy_v2.executors.xemm_executor.xemm_executor import XEMMExecutor
2526
from hummingbot.strategy_v2.models.executor_actions import (
@@ -33,7 +34,7 @@
3334

3435

3536
class PositionHold:
36-
def __init__(self, connector_name: str, trading_pair: str, side: TradeType):
37+
def __init__(self, connector_name: str, trading_pair: str, side: TradeType = None):
3738
self.connector_name = connector_name
3839
self.trading_pair = trading_pair
3940
self.side = side
@@ -67,6 +68,7 @@ def add_orders_from_executor(self, executor: ExecutorInfo):
6768
# Update metrics incrementally
6869
executed_amount_base = Decimal(str(order.get("executed_amount_base", 0)))
6970
executed_amount_quote = Decimal(str(order.get("executed_amount_quote", 0)))
71+
7072
is_buy = order.get("trade_type") == "BUY"
7173

7274
# Update volume traded in quote
@@ -80,16 +82,10 @@ def add_orders_from_executor(self, executor: ExecutorInfo):
8082
self.sell_amount_base += executed_amount_base
8183
self.sell_amount_quote += executed_amount_quote
8284

83-
# Update fees
85+
# Update fees (tx fees paid)
8486
self.cum_fees_quote += Decimal(str(order.get("cumulative_fee_paid_quote", 0)))
8587

8688
def get_position_summary(self, mid_price: Decimal):
87-
# Handle NaN quote amounts by calculating them lazily
88-
if self.buy_amount_quote.is_nan() and self.buy_amount_base > 0:
89-
self.buy_amount_quote = self.buy_amount_base * mid_price
90-
if self.sell_amount_quote.is_nan() and self.sell_amount_base > 0:
91-
self.sell_amount_quote = self.sell_amount_base * mid_price
92-
9389
# Calculate buy and sell breakeven prices
9490
buy_breakeven_price = self.buy_amount_quote / self.buy_amount_base if self.buy_amount_base > 0 else Decimal("0")
9591
sell_breakeven_price = self.sell_amount_quote / self.sell_amount_base if self.sell_amount_base > 0 else Decimal("0")
@@ -112,13 +108,13 @@ def get_position_summary(self, mid_price: Decimal):
112108
# Long position: remaining buy amount
113109
remaining_base = net_amount_base
114110
remaining_quote = self.buy_amount_quote - (matched_amount_base * buy_breakeven_price)
115-
breakeven_price = remaining_quote / remaining_base
111+
breakeven_price = remaining_quote / remaining_base if remaining_base != 0 else Decimal("0")
116112
unrealized_pnl_quote = (mid_price - breakeven_price) * remaining_base
117113
else:
118114
# Short position: remaining sell amount
119115
remaining_base = abs(net_amount_base)
120116
remaining_quote = self.sell_amount_quote - (matched_amount_base * sell_breakeven_price)
121-
breakeven_price = remaining_quote / remaining_base
117+
breakeven_price = remaining_quote / remaining_base if remaining_base != 0 else Decimal("0")
122118
unrealized_pnl_quote = (breakeven_price - mid_price) * remaining_base
123119

124120
return PositionSummary(
@@ -147,6 +143,7 @@ class ExecutorOrchestrator:
147143
"xemm_executor": XEMMExecutor,
148144
"order_executor": OrderExecutor,
149145
"lp_executor": LPExecutor,
146+
"swap_executor": SwapExecutor,
150147
}
151148

152149
@classmethod
@@ -255,7 +252,7 @@ def _load_position_from_db(self, controller_id: str, db_position: Position):
255252
def _create_initial_positions(self):
256253
"""
257254
Create initial positions from config overrides.
258-
Uses NaN for quote amounts initially - they will be calculated lazily when needed.
255+
Quote amounts are calculated using current mid_price.
259256
"""
260257
for controller_id, initial_positions in self.initial_positions_by_controller.items():
261258
if controller_id not in self.cached_performance:
@@ -264,22 +261,28 @@ def _create_initial_positions(self):
264261
self.positions_held[controller_id] = []
265262

266263
for position_config in initial_positions:
264+
# Get mid_price for quote amount calculation
265+
mid_price = self.strategy.market_data_provider.get_price_by_type(
266+
position_config.connector_name, position_config.trading_pair, PriceType.MidPrice
267+
)
268+
quote_amount = position_config.amount * mid_price
269+
267270
# Create PositionHold object
268271
position_hold = PositionHold(
269272
position_config.connector_name,
270273
position_config.trading_pair,
271274
position_config.side
272275
)
273276

274-
# Set amounts based on side, using NaN for quote amounts
277+
# Set amounts based on side
275278
if position_config.side == TradeType.BUY:
276279
position_hold.buy_amount_base = position_config.amount
277-
position_hold.buy_amount_quote = Decimal("NaN") # Will be calculated lazily
280+
position_hold.buy_amount_quote = quote_amount
278281
position_hold.sell_amount_base = Decimal("0")
279282
position_hold.sell_amount_quote = Decimal("0")
280283
else:
281284
position_hold.sell_amount_base = position_config.amount
282-
position_hold.sell_amount_quote = Decimal("NaN") # Will be calculated lazily
285+
position_hold.sell_amount_quote = quote_amount
283286
position_hold.buy_amount_base = Decimal("0")
284287
position_hold.buy_amount_quote = Decimal("0")
285288

@@ -305,7 +308,7 @@ async def stop(self, max_executors_close_attempts: int = 3):
305308
for i in range(max_executors_close_attempts):
306309
if all([executor.executor_info.is_done for executors_list in self.active_executors.values()
307310
for executor in executors_list]):
308-
continue
311+
break # All executors are done, exit early
309312
await asyncio.sleep(2.0)
310313
# Store all positions and executors
311314
self.store_all_positions()
@@ -466,7 +469,7 @@ def _update_positions_from_done_executors(self):
466469
if existing_position:
467470
existing_position.add_orders_from_executor(executor_info)
468471
else:
469-
# Create new position
472+
# Create new position (handles both spot/perp and LP)
470473
position = PositionHold(
471474
executor_info.connector_name,
472475
executor_info.trading_pair,

hummingbot/strategy_v2/models/executors_info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
from hummingbot.strategy_v2.executors.lp_executor.data_types import LPExecutorConfig
1111
from hummingbot.strategy_v2.executors.order_executor.data_types import OrderExecutorConfig
1212
from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig
13+
from hummingbot.strategy_v2.executors.swap_executor.data_types import SwapExecutorConfig
1314
from hummingbot.strategy_v2.executors.twap_executor.data_types import TWAPExecutorConfig
1415
from hummingbot.strategy_v2.executors.xemm_executor.data_types import XEMMExecutorConfig
1516
from hummingbot.strategy_v2.models.base import RunnableStatus
1617
from hummingbot.strategy_v2.models.executors import CloseType
1718

18-
AnyExecutorConfig = Union[PositionExecutorConfig, DCAExecutorConfig, GridExecutorConfig, XEMMExecutorConfig, ArbitrageExecutorConfig, OrderExecutorConfig, TWAPExecutorConfig, LPExecutorConfig]
19+
AnyExecutorConfig = Union[PositionExecutorConfig, DCAExecutorConfig, GridExecutorConfig, XEMMExecutorConfig, ArbitrageExecutorConfig, OrderExecutorConfig, TWAPExecutorConfig, LPExecutorConfig, SwapExecutorConfig]
1920

2021

2122
class ExecutorInfo(BaseModel):

0 commit comments

Comments
 (0)