Coverage for adrs/oms/ops/order_utils.py: 78%

37 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-29 05:07 +0800

1import uuid 

2import logging 

3 

4from decimal import Decimal 

5from datetime import datetime, timezone 

6from typing import TYPE_CHECKING 

7 

8from cybotrade import Symbol 

9from cybotrade.io import ExchangeClient 

10 

11from adrs.oms.rate_limit.exchange_limit_profiles import OMS_DEPTH_LIMIT, Endpoints 

12 

13if TYPE_CHECKING: 

14 from adrs.oms.price_feed import PriceFeed 

15 from adrs.oms.rate_limit.rate_limiter import RateLimiter 

16 

17logger = logging.getLogger(__name__) 

18 

19 

20class OrderUtils: 

21 def __init__(self): 

22 pass 

23 

24 @staticmethod 

25 async def get_order_book( 

26 exchange: ExchangeClient, 

27 pair: Symbol, 

28 need_log: bool, 

29 rate_limiter: "RateLimiter", 

30 endpoint: Endpoints = Endpoints.GET_ORDERBOOK_SNAPSHOT, 

31 price_feed: "PriceFeed | None" = None, 

32 ) -> list[Decimal]: 

33 # The websocket feed answers for free when it is proven live. A miss 

34 # (feed down, symbol unseen, or past the staleness backstop) falls 

35 # through to REST below, which is exactly the old behaviour. 

36 if price_feed is not None: 

37 quote = price_feed.get(pair) 

38 if quote is not None: 

39 if need_log: 

40 logger.info(f"best_bid: {quote.bid}, best_ask: {quote.ask} (ws)") 

41 return [quote.bid, quote.ask] 

42 

43 # reserve() waits for a rate-limit slot rather than failing, so 

44 # contention no longer needs a retry loop. Genuine fetch errors 

45 # propagate to the caller (placement backlogs it, expiry skips it). 

46 # 

47 # This is the OMS's only depth call site, and the limit it sends is the 

48 # same constant the limiter charges for — the two cannot drift. 

49 async with rate_limiter.reserve(endpoint=endpoint): 

50 orderbook = await exchange.get_orderbook_snapshot( 

51 symbol=pair, limit=OMS_DEPTH_LIMIT 

52 ) 

53 (best_bid, best_ask) = ( 

54 max(map(lambda level: level.price, orderbook.bids)), 

55 min(map(lambda level: level.price, orderbook.asks)), 

56 ) 

57 if need_log: 

58 logger.info(f"best_bid: {best_bid}, best_ask: {best_ask}") 

59 return [best_bid, best_ask] 

60 

61 @staticmethod 

62 def make_client_order_id(): 

63 return f"{uuid.uuid4().hex}" 

64 

65 @staticmethod 

66 def convert_ms_to_datetime(milliseconds): 

67 try: 

68 seconds = milliseconds / 1000.0 

69 return datetime.fromtimestamp(seconds, tz=timezone.utc) 

70 except Exception as e: 

71 logger.error( 

72 f"[CONVERT_DATETIME] Failed to convert_ms_to_datetime due to: {e}" 

73 ) 

74 return datetime.now(timezone.utc)