strategy_sizing.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. from __future__ import annotations
  2. from typing import Any
  3. def _call_context_suggest_order_amount(context: Any, kwargs: dict[str, Any]) -> float | None:
  4. if not hasattr(context, "suggest_order_amount"):
  5. return None
  6. variants: list[dict[str, Any]] = []
  7. drop_sets = (
  8. (),
  9. ("quote_notional",),
  10. ("dust_collect",),
  11. ("order_size",),
  12. ("quote_notional", "dust_collect"),
  13. ("quote_notional", "order_size"),
  14. ("dust_collect", "order_size"),
  15. ("quote_notional", "dust_collect", "order_size"),
  16. )
  17. for drop_keys in drop_sets:
  18. variant = {key: value for key, value in kwargs.items() if key not in drop_keys}
  19. if variant not in variants:
  20. variants.append(variant)
  21. last_error: TypeError | None = None
  22. for variant in variants:
  23. try:
  24. return float(context.suggest_order_amount(**variant) or 0.0)
  25. except TypeError as exc:
  26. last_error = exc
  27. if last_error is not None:
  28. raise last_error
  29. return None
  30. def suggest_quote_sized_amount(
  31. context: Any,
  32. *,
  33. side: str,
  34. price: float,
  35. levels: int,
  36. min_notional: float,
  37. fee_rate: float,
  38. order_notional_quote: float = 0.0,
  39. max_order_notional_quote: float = 0.0,
  40. dust_collect: bool = False,
  41. order_size: float = 0.0,
  42. ) -> float:
  43. side = str(side or "").strip().lower()
  44. price = float(price or 0.0)
  45. levels = int(levels or 0)
  46. min_notional = max(float(min_notional or 0.0), 0.0)
  47. fee_rate = max(float(fee_rate or 0.0), 0.0)
  48. order_notional_quote = max(float(order_notional_quote or 0.0), 0.0)
  49. max_order_notional_quote = max(float(max_order_notional_quote or 0.0), 0.0)
  50. order_size = max(float(order_size or 0.0), 0.0)
  51. if levels <= 0 or price <= 0 or side not in {"buy", "sell"}:
  52. return 0.0
  53. kwargs = {
  54. "side": side,
  55. "price": price,
  56. "levels": levels,
  57. "min_notional": min_notional,
  58. "fee_rate": fee_rate,
  59. "quote_notional": order_notional_quote,
  60. "max_notional_per_order": max_order_notional_quote,
  61. "dust_collect": bool(dust_collect),
  62. "order_size": order_size,
  63. }
  64. amount = _call_context_suggest_order_amount(context, kwargs)
  65. if amount is not None:
  66. if amount <= 0:
  67. return 0.0
  68. if side == "buy":
  69. if min_notional > 0 and (amount * price * (1 + fee_rate)) < min_notional:
  70. return 0.0
  71. else:
  72. if min_notional > 0 and (amount * price) < min_notional:
  73. return 0.0
  74. return amount
  75. if order_notional_quote <= 0:
  76. return 0.0
  77. effective_quote = order_notional_quote
  78. if max_order_notional_quote > 0:
  79. effective_quote = min(effective_quote, max_order_notional_quote)
  80. if side == "buy":
  81. min_quote_needed = min_notional * (1 + fee_rate)
  82. if min_notional > 0 and effective_quote < min_quote_needed:
  83. return 0.0
  84. amount = effective_quote / (price * (1 + fee_rate))
  85. else:
  86. amount = effective_quote / price
  87. min_amount = (min_notional / price) if side == "sell" and min_notional > 0 else 0.0
  88. if min_amount > 0 and amount < min_amount:
  89. return 0.0
  90. return max(amount, 0.0)
  91. def cap_amount_to_balance_target(
  92. *,
  93. suggested_amount: float,
  94. side: str,
  95. price: float,
  96. fee_rate: float,
  97. balance_target: float,
  98. base_available: float,
  99. counter_available: float,
  100. min_notional: float = 0.0,
  101. ) -> float:
  102. amount = max(float(suggested_amount or 0.0), 0.0)
  103. side = str(side or "").strip().lower()
  104. price = float(price or 0.0)
  105. fee_rate = max(float(fee_rate or 0.0), 0.0)
  106. target = min(max(float(balance_target if balance_target is not None else 1.0), 0.0), 1.0)
  107. base_available = max(float(base_available or 0.0), 0.0)
  108. counter_available = max(float(counter_available or 0.0), 0.0)
  109. min_notional = max(float(min_notional or 0.0), 0.0)
  110. if amount <= 0 or price <= 0 or side not in {"buy", "sell"}:
  111. return 0.0
  112. def meets_min_notional(candidate: float) -> bool:
  113. if min_notional <= 0:
  114. return True
  115. if side == "buy":
  116. return (candidate * price * (1 + fee_rate)) >= min_notional
  117. return (candidate * price) >= min_notional
  118. if side == "buy":
  119. max_amount = counter_available / (price * (1 + fee_rate)) if counter_available > 0 else 0.0
  120. else:
  121. max_amount = base_available
  122. amount = min(amount, max(max_amount, 0.0))
  123. if amount <= 0:
  124. return 0.0
  125. if not meets_min_notional(amount):
  126. return 0.0
  127. if target >= 1.0:
  128. return amount
  129. base_value = base_available * price
  130. total_value = base_value + counter_available
  131. if total_value <= 0:
  132. return 0.0
  133. capped_amount = amount
  134. if side == "buy":
  135. remaining_quote = (target * total_value) - base_value
  136. if remaining_quote <= 0:
  137. return 0.0
  138. target_amount = remaining_quote / (price * (1 + target * fee_rate))
  139. capped_amount = min(amount, target_amount)
  140. else:
  141. target_base_ratio = 1.0 - target
  142. remaining_quote = base_value - (target_base_ratio * total_value)
  143. if remaining_quote <= 0:
  144. return 0.0
  145. denominator = price * max(1.0 - (target_base_ratio * fee_rate), 1e-12)
  146. target_amount = remaining_quote / denominator
  147. capped_amount = min(amount, target_amount)
  148. if capped_amount <= 0:
  149. return 0.0
  150. if not meets_min_notional(capped_amount):
  151. return 0.0
  152. return max(capped_amount, 0.0)
  153. def suggest_rebalance_amount(
  154. *,
  155. side: str,
  156. price: float,
  157. fee_rate: float,
  158. base_available: float,
  159. counter_available: float,
  160. target_ratio: float,
  161. step_ratio: float,
  162. balance_tolerance: float,
  163. min_order_notional_quote: float = 0.0,
  164. max_order_notional_quote: float = 0.0,
  165. ) -> float:
  166. side = str(side or "").strip().lower()
  167. price = float(price or 0.0)
  168. fee_rate = max(float(fee_rate or 0.0), 0.0)
  169. base_available = max(float(base_available or 0.0), 0.0)
  170. counter_available = max(float(counter_available or 0.0), 0.0)
  171. target_ratio = min(max(float(target_ratio or 0.0), 0.0), 1.0)
  172. step_ratio = max(float(step_ratio or 0.0), 0.0)
  173. balance_tolerance = max(float(balance_tolerance or 0.0), 0.0)
  174. min_order_notional_quote = max(float(min_order_notional_quote or 0.0), 0.0)
  175. max_order_notional_quote = max(float(max_order_notional_quote or 0.0), 0.0)
  176. if side not in {"buy", "sell"} or price <= 0:
  177. return 0.0
  178. base_value = base_available * price
  179. total_value = base_value + counter_available
  180. if total_value <= 0:
  181. return 0.0
  182. current_ratio = base_value / total_value
  183. drift = abs(current_ratio - target_ratio)
  184. if drift <= balance_tolerance:
  185. return 0.0
  186. target_quote = total_value * min(drift, step_ratio)
  187. if min_order_notional_quote > 0:
  188. target_quote = max(target_quote, min_order_notional_quote)
  189. if max_order_notional_quote > 0:
  190. target_quote = min(target_quote, max_order_notional_quote)
  191. if target_quote <= 0:
  192. return 0.0
  193. if min_order_notional_quote > 0 and target_quote < min_order_notional_quote:
  194. return 0.0
  195. if side == "buy":
  196. max_affordable_quote = counter_available
  197. if max_affordable_quote < min_order_notional_quote * (1 + fee_rate):
  198. return 0.0
  199. capped_quote = min(target_quote, max_affordable_quote)
  200. amount = capped_quote / (price * (1 + fee_rate))
  201. else:
  202. max_affordable_quote = base_available * price
  203. if max_affordable_quote < min_order_notional_quote:
  204. return 0.0
  205. capped_quote = min(target_quote, max_affordable_quote)
  206. amount = capped_quote / (price * (1 + fee_rate))
  207. if amount <= 0:
  208. return 0.0
  209. return max(amount, 0.0)