diff --git a/derive_client/data_types/channel_models.py b/derive_client/data_types/channel_models.py index 7a0e7635..212468ee 100644 --- a/derive_client/data_types/channel_models.py +++ b/derive_client/data_types/channel_models.py @@ -23,6 +23,8 @@ PublicGetInstrumentParamsSchema, PublicGetOptionSettlementPricesParamsSchema, PublicMarginWatchResultSchema, + QuoteResultSchema, + RFQResultPublicSchema, RPCErrorFormatSchema, Status, TickerSlimSchema, @@ -139,32 +141,6 @@ class SubaccountIdQuotesChannelSchema(SubaccountIdBalancesChannelSchema): pass -class QuoteResultSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - direction: Direction - fee: Decimal - fill_pct: Decimal - is_transfer: bool - label: str - last_update_timestamp: int - legs: List[LegPricedSchema] - legs_hash: str - liquidity_role: LiquidityRole - max_fee: Decimal - mmp: bool - nonce: int - quote_id: str - rfq_id: str - signature: str - signature_expiry_sec: int - signer: str - status: Status - subaccount_id: int - tx_status: TxStatus - tx_hash: Optional[str] = None - - class SubaccountIdTradesChannelSchema(SubaccountIdBalancesChannelSchema): pass @@ -241,6 +217,10 @@ class WalletRfqsChannelSchema(PrivateGetAllPortfoliosParamsSchema): pass +class LegUnpricedSchema1(LegUnpricedSchema): + pass + + class AuctionResultSchema(Struct): state: State subaccount_id: int @@ -267,29 +247,6 @@ class SubaccountIdBalancesNotificationParamsSchema(Struct): data: List[BalanceUpdateSchema] -class QuoteResultPublicSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - direction: Direction - fill_pct: Decimal - last_update_timestamp: int - legs: List[LegPricedSchema] - legs_hash: str - liquidity_role: LiquidityRole - quote_id: str - rfq_id: str - status: Status - subaccount_id: int - tx_status: TxStatus - wallet: str - tx_hash: Optional[str] = None - - -class SubaccountIdQuotesNotificationParamsSchema(Struct): - channel: str - data: List[QuoteResultSchema] - - class SubaccountIdTradesTxStatusNotificationParamsSchema(Struct): channel: str data: List[TradeResponseSchema] @@ -314,24 +271,6 @@ class TradesInstrumentTypeCurrencyTxStatusNotificationParamsSchema(Struct): data: List[TradeSettledPublicResponseSchema] -class RFQResultPublicSchema(Struct): - cancel_reason: CancelReason - creation_timestamp: int - filled_direction: Direction - filled_pct: Decimal - last_update_timestamp: int - legs: List[LegUnpricedSchema] - partial_fill_step: Decimal - rfq_id: str - status: Status - subaccount_id: int - valid_until: int - wallet: str - fill_rate: Optional[Decimal] = None - recent_fill_rate: Optional[Decimal] = None - total_cost: Optional[Decimal] = None - - class AuctionsWatchNotificationParamsSchema(Struct): channel: str data: List[AuctionResultSchema] @@ -367,8 +306,22 @@ class SubaccountIdBalancesPubSubSchema(Struct): notification: SubaccountIdBalancesNotificationSchema -class RFQGetBestQuoteResultSchema(PrivateRfqGetBestQuoteResultSchema): - pass +class QuoteResultPublicSchema(Struct): + cancel_reason: CancelReason + creation_timestamp: int + direction: Direction + fill_pct: Decimal + last_update_timestamp: int + legs: List[LegPricedSchema] + legs_hash: str + liquidity_role: LiquidityRole + quote_id: str + rfq_id: str + status: Status + subaccount_id: int + tx_status: TxStatus + wallet: str + tx_hash: Optional[str] = None class SubaccountIdOrdersNotificationParamsSchema(Struct): @@ -376,14 +329,9 @@ class SubaccountIdOrdersNotificationParamsSchema(Struct): data: List[OrderResponseSchema] -class SubaccountIdQuotesNotificationSchema(Struct): - method: str - params: SubaccountIdQuotesNotificationParamsSchema - - -class SubaccountIdQuotesPubSubSchema(Struct): - channel_params: SubaccountIdQuotesChannelSchema - notification: SubaccountIdQuotesNotificationSchema +class SubaccountIdQuotesNotificationParamsSchema(Struct): + channel: str + data: List[QuoteResultSchema] class SubaccountIdTradesNotificationParamsSchema(SubaccountIdTradesTxStatusNotificationParamsSchema): @@ -425,11 +373,6 @@ class TradesInstrumentTypeCurrencyTxStatusPubSubSchema(Struct): notification: TradesInstrumentTypeCurrencyTxStatusNotificationSchema -class WalletRfqsNotificationParamsSchema(Struct): - channel: str - data: List[RFQResultPublicSchema] - - class AuctionsWatchNotificationSchema(Struct): method: str params: AuctionsWatchNotificationParamsSchema @@ -460,10 +403,8 @@ class SpotFeedCurrencyPubSubSchema(Struct): notification: SpotFeedCurrencyNotificationSchema -class BestQuoteChannelResultSchema(Struct): - rfq_id: str - error: Optional[RPCErrorFormatSchema] = None - result: Optional[RFQGetBestQuoteResultSchema] = None +class RFQGetBestQuoteResultSchema(PrivateRfqGetBestQuoteResultSchema): + pass class SubaccountIdOrdersNotificationSchema(Struct): @@ -476,6 +417,16 @@ class SubaccountIdOrdersPubSubSchema(Struct): notification: SubaccountIdOrdersNotificationSchema +class SubaccountIdQuotesNotificationSchema(Struct): + method: str + params: SubaccountIdQuotesNotificationParamsSchema + + +class SubaccountIdQuotesPubSubSchema(Struct): + channel_params: SubaccountIdQuotesChannelSchema + notification: SubaccountIdQuotesNotificationSchema + + class SubaccountIdTradesNotificationSchema(Struct): method: str params: SubaccountIdTradesNotificationParamsSchema @@ -491,6 +442,27 @@ class TickerSlimInstrumentNameIntervalNotificationParamsSchema(Struct): data: TickerSlimInstrumentNameIntervalPublisherDataSchema +class WalletRfqsNotificationParamsSchema(Struct): + channel: str + data: List[RFQResultPublicSchema] + + +class BestQuoteChannelResultSchema(Struct): + rfq_id: str + error: Optional[RPCErrorFormatSchema] = None + result: Optional[RFQGetBestQuoteResultSchema] = None + + +class TickerSlimInstrumentNameIntervalNotificationSchema(Struct): + method: str + params: TickerSlimInstrumentNameIntervalNotificationParamsSchema + + +class TickerSlimInstrumentNameIntervalPubSubSchema(Struct): + channel_params: TickerSlimInstrumentNameIntervalChannelSchema + notification: TickerSlimInstrumentNameIntervalNotificationSchema + + class WalletRfqsNotificationSchema(Struct): method: str params: WalletRfqsNotificationParamsSchema @@ -506,16 +478,6 @@ class SubaccountIdBestQuotesNotificationParamsSchema(Struct): data: List[BestQuoteChannelResultSchema] -class TickerSlimInstrumentNameIntervalNotificationSchema(Struct): - method: str - params: TickerSlimInstrumentNameIntervalNotificationParamsSchema - - -class TickerSlimInstrumentNameIntervalPubSubSchema(Struct): - channel_params: TickerSlimInstrumentNameIntervalChannelSchema - notification: TickerSlimInstrumentNameIntervalNotificationSchema - - class SubaccountIdBestQuotesNotificationSchema(Struct): method: str params: SubaccountIdBestQuotesNotificationParamsSchema diff --git a/examples/rfqs/00_create_rfq.py b/examples/rfqs/00_create_rfq.py new file mode 100644 index 00000000..c520807e --- /dev/null +++ b/examples/rfqs/00_create_rfq.py @@ -0,0 +1,116 @@ +""" +Simple demonstration of creating and executing an RFQ. +""" + +import asyncio +from typing import List + +from config import OWNER_TEST_WALLET, SESSION_KEY_PRIVATE_KEY, TAKER_SUBACCOUNT_ID + +from derive_client import WebSocketClient +from derive_client.data_types import Environment +from derive_client.data_types.channel_models import BestQuoteChannelResultSchema +from derive_client.data_types.generated_models import Direction, LegUnpricedSchema, QuoteResultPublicSchema +from derive_client.data_types.utils import D +from derive_client.utils.logger import get_logger + + +async def create_and_execute_rfq( + legs: List[LegUnpricedSchema], +): + """ + Create an RFQ, wait for quotes, and execute the best one. + + Args: + legs: List of unpriced legs representing the instruments and amounts to trade. + Each leg specifies the instrument name, amount, and direction (buy/sell). + + Flow: + 1. Initialize WebSocket client and connect to the exchange + 2. Send RFQ (Request for Quote) with the specified legs + 3. Subscribe to the best quotes channel to receive quote updates + 4. Wait for market makers to respond with their quotes + 5. Execute the best received quote if available + """ + # Initialize the WebSocket client with authentication credentials and connect to the test environment + client = WebSocketClient( + session_key=SESSION_KEY_PRIVATE_KEY, + wallet=OWNER_TEST_WALLET, + env=Environment.TEST, + subaccount_id=TAKER_SUBACCOUNT_ID, + ) + await client.connect() + logger = get_logger() + + # Send the RFQ to the exchange - this broadcasts the request to all market makers + # who can then respond with their quotes + rfq_result = await client.rfq.send_rfq(legs=legs) + logger.info(f"✓ RFQ created: {rfq_result.rfq_id}") + + # Track the best quote received from market makers + # This will be updated as better quotes arrive + best_quote: QuoteResultPublicSchema | None = None + + def handle_quote(quotes: List[BestQuoteChannelResultSchema]): + """ + Callback function that processes incoming quote updates. + Each time a better quote arrives, we update our tracked best quote. + """ + nonlocal best_quote + for quote in quotes: + if quote.result and quote.result.best_quote: + best_quote = quote.result.best_quote + # Calculate the total price across all legs for logging + total_price = sum(leg.price * leg.amount for leg in best_quote.legs) + logger.info(f"✓ Best quote received: {total_price}") + + # Subscribe to the best quotes channel for this subaccount + # This allows us to receive real-time updates as market makers send quotes + await client.private_channels.best_quotes_by_subaccount_id( + subaccount_id=str(TAKER_SUBACCOUNT_ID), + callback=handle_quote, + ) + + # Wait for market makers to respond with their quotes + # In a production system, you might want to wait until the RFQ expires or use a different timing strategy + await asyncio.sleep(10) + + if not best_quote: + logger.error("✗ No quotes received") + return + + # Execute the best quote we received + # Note: We must take the opposite direction of the quote + # If the market maker is buying (quote.direction == buy), we must sell to them + execute_direction = Direction.sell if best_quote.direction == Direction.buy else Direction.buy + await client.rfq.execute_quote( + direction=execute_direction, + legs=best_quote.legs, + rfq_id=best_quote.rfq_id, + quote_id=best_quote.quote_id, + ) + # Log the successful execution with the final price + logger.info( + f"✓ Quote {best_quote.quote_id} executed at total price: " + + f"{sum(leg.price * leg.amount for leg in best_quote.legs)}" + ) + + +if __name__ == "__main__": + # Example usage: Create an RFQ to sell 1 ETH put option + # This will: + # 1. Send the RFQ to market makers + # 2. Wait for quotes to come in + # 3. Execute the best quote automatically + legs = [ + LegUnpricedSchema( + instrument_name="ETH-20260327-4800-P", # ETH put option expiring March 27, 2026 with strike 4800 + amount=D("1"), # Trade 1 contract + direction=Direction.sell, # We want to sell this option + ), + ] + asyncio.run( + create_and_execute_rfq( + legs=legs, + ) + ) diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py new file mode 100644 index 00000000..b56abe76 --- /dev/null +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -0,0 +1,227 @@ +""" +Example of how to act as a simple RFQ quoter that prices incoming RFQs and sends quotes. + +This example demonstrates the market maker side of RFQ trading: +- Connects to the WebSocket API and listens for incoming RFQs on a specified wallet +- Prices the legs using current market prices with a small spread +- Sends quotes back to the RFQs +- Listens for quote updates to track the status of quotes sent (filled, expired, etc.) + +IMPORTANT: This is a simplified example for educational purposes. +IT SHOULD NOT BE USED AS A TRADING STRATEGY!!! +The pricing logic here does not account for: +- Risk management (position limits, exposure limits) +- Proper spread calculation based on volatility and liquidity +- Greeks hedging (delta, gamma, vega, theta) +- Inventory management +""" + +import asyncio +import warnings +from logging import Logger +from typing import List + +from config import ADMIN_TEST_WALLET as TEST_WALLET +from config import SESSION_KEY_PRIVATE_KEY + +from derive_client import WebSocketClient +from derive_client.data_types import Environment +from derive_client.data_types.channel_models import QuoteResultSchema +from derive_client.data_types.generated_models import ( + Direction, + LegPricedSchema, + PrivateSendQuoteResultSchema, + RFQResultPublicSchema, + Status, +) +from derive_client.data_types.utils import D + +warnings.filterwarnings("ignore", category=DeprecationWarning) + +SLEEP_TIME = 1 +SUBACCOUNT_ID = 31049 + + +class SimpleRfqQuoter: + """ + A simple RFQ quoter that listens for incoming RFQs and responds with quotes. + + This class demonstrates the basic flow of: + 1. Receiving RFQs from the exchange + 2. Pricing the requested instruments + 3. Sending quotes back to the taker + 4. Tracking quote status updates + """ + + logger: Logger + client: WebSocketClient + quotes: dict[str, PrivateSendQuoteResultSchema] = {} # Track all active quotes by RFQ ID + + def __init__(self, client: WebSocketClient): + self.client = client + self.logger = client._logger + + async def price_rfq(self, rfq): + """ + Price all legs of an RFQ using a simple strategy based on mark prices. + + Pricing strategy: + - For legs where we would BUY from the taker: Quote 0.1% below mark price (we pay less) + - For legs where we would SELL to the taker: Quote 0.1% above mark price (we receive more) + + This ensures we make a small profit on each leg, but is NOT a proper trading strategy. + A real market maker would consider: + - Current bid-ask spread + - Market volatility + - Position exposure and risk limits + - Greeks hedging costs + + Returns: + List of priced legs, or empty list if pricing fails + """ + # Price legs using current market prices NOTE! This is just an example and not a trading strategy!!! + self.logger.info(f" - Pricing legs for RFQ {rfq.rfq_id}...") + priced_legs = [] + for unpriced_leg in rfq.legs: + # Fetch current market data for this instrument + ticker = await self.client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) + + base_price = ticker.mark_price + + # Apply a simple spread: Quote slightly favorable prices to us + # If taker wants to BUY (we SELL), we quote 0.1% above mark + # If taker wants to SELL (we BUY), we quote 0.1% below mark + price = base_price * D("0.999") if unpriced_leg.direction == Direction.buy else base_price * D("1.001") + + # Round to the instrument's tick size (minimum price increment) + price = price.quantize(ticker.tick_size) + priced_leg = LegPricedSchema( + price=price, + amount=unpriced_leg.amount, + direction=unpriced_leg.direction, + instrument_name=unpriced_leg.instrument_name, + ) + priced_legs.append(priced_leg) + self.logger.info( + f" ✓ Priced legs for RFQ {rfq.rfq_id} at total price {sum(leg.price * leg.amount for leg in priced_legs)}" + ) + return priced_legs + + async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): + """ + Handle incoming RFQ updates. + + This callback is triggered when: + - New RFQs are created + - Existing RFQs change status (expired, cancelled, etc.) + + Flow: + 1. Clean up any quotes for expired/cancelled RFQs + 2. Filter for open RFQs that need quotes + 3. Price all open RFQs in parallel + 4. Send quotes for all successfully priced RFQs + """ + # First, clean up quotes for RFQs that are no longer active + for rfq in rfqs: + if rfq.status in {Status.expired, Status.cancelled} and rfq.rfq_id in self.quotes: + del self.quotes[rfq.rfq_id] + + # Filter for RFQs that are still open and need quotes + open_rfqs = [r for r in rfqs if r.status == Status.open] + if not open_rfqs: + return + + # Price all open RFQs in parallel for efficiency + priced = await asyncio.gather(*(self.price_rfq(r) for r in open_rfqs)) + quotable = [(r, legs) for r, legs in zip(open_rfqs, priced) if legs] + + if not quotable: + return + + # Send quotes for all successfully priced RFQs + # We always quote as SELL direction (we're the market maker taking the other side) + results = await asyncio.gather( + *(self.client.rfq.send_quote(rfq_id=r.rfq_id, legs=legs, direction=Direction.sell) for r, legs in quotable), + return_exceptions=True, + ) + # Track successfully sent quotes + for r, result in zip(quotable, results): + rfq, _ = r + if isinstance(result, PrivateSendQuoteResultSchema): + self.quotes[rfq.rfq_id] = result + else: + self.logger.info(f" ❌ Failed to send quote for RFQ {rfq.rfq_id}: {result}") + + async def on_quote(self, quotes_list: List[QuoteResultSchema]): + """ + Handle quote status updates. + + This callback is triggered when quotes we sent change status: + - FILLED: The taker executed our quote (we have a trade!) + - EXPIRED: The quote expired before being executed + - CANCELLED: The quote was cancelled + + When a quote is filled, this is where you would typically: + - Update position tracking + - Initiate hedging trades + - Update risk metrics + """ + for quote in quotes_list: + self.logger.info(f" - Quote {quote.quote_id} {quote.rfq_id}: {quote.status}") + if quote.status == Status.filled and quote.rfq_id in self.quotes: + del self.quotes[quote.rfq_id] + self.logger.info(f" ✓ Our quote {quote.quote_id} was accepted!") + # Here we could proceed to perform some type of hedging or other action based on the filled quote. + # For example: hedge delta exposure, update inventory, adjust risk limits, etc. + if quote.status == Status.expired and quote.rfq_id in self.quotes: + del self.quotes[quote.rfq_id] + self.logger.info(f" ✗ Our quote {quote.quote_id} expired. Better luck next time!") + + async def run(self): + """ + Main execution loop for the RFQ quoter. + + Sets up WebSocket connections and subscriptions: + 1. Subscribe to RFQs for our wallet - receive incoming RFQ requests + 2. Subscribe to quotes for our subaccount - receive status updates on our quotes + 3. Keep the connection alive indefinitely + """ + await self.client.connect() + # Subscribe to RFQs targeted at our wallet + await self.client.private_channels.rfqs_by_wallet(wallet=TEST_WALLET, callback=self.on_rfq) + # Subscribe to quote updates for quotes we've sent + await self.client.private_channels.quotes_by_subaccount_id( + subaccount_id=str(SUBACCOUNT_ID), + callback=self.on_quote, + ) + await asyncio.Event().wait() # Keep the connection alive indefinitely + + +async def main(): + """ + Initialize and run the simple RFQ quoter. + + This will: + 1. Create a WebSocket client connected to the test environment + 2. Initialize the RFQ quoter + 3. Run continuously, quoting on all incoming RFQs + 4. Automatically reconnect if the connection drops + """ + + client = WebSocketClient( + session_key=SESSION_KEY_PRIVATE_KEY, + wallet=TEST_WALLET, + env=Environment.TEST, + subaccount_id=SUBACCOUNT_ID, + ) + rfq_quoter = SimpleRfqQuoter(client) + # Run indefinitely with automatic reconnection on failure + while True: + try: + await rfq_quoter.run() + except KeyboardInterrupt: + break + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/rfqs/02_delta_hedged_quoter.py b/examples/rfqs/02_delta_hedged_quoter.py new file mode 100644 index 00000000..67474188 --- /dev/null +++ b/examples/rfqs/02_delta_hedged_quoter.py @@ -0,0 +1,607 @@ +""" +Example of how to act as a RFQ quoter with delta hedging. + +This is a more advanced example that demonstrates a realistic market-making strategy: +- Connects to the WebSocket API and listens for incoming RFQs +- Prices legs using current market bid-ask prices (not just mark prices) +- Implements a delta-hedging strategy to manage options exposure +- Monitors portfolio delta and automatically hedges when outside risk limits + +Delta hedging explained: +- Options have "delta" - how much their value changes when the underlying asset moves +- When we sell options, we take on delta exposure to the underlying asset +- Delta hedging means trading the underlying (perpetuals) to neutralize this exposure +- This protects us from directional price risk, leaving us with pure volatility exposure + +Strategy flow: +1. Receive RFQ -> Calculate if accepting would exceed our delta limits +2. If safe to quote -> Price using current bid-ask + spread +3. When quote is filled -> Calculate new portfolio delta +4. If delta exceeds limits -> Execute hedge trade in the underlying perpetual +5. Continuously monitor and rebalance delta exposure + +IMPORTANT: While more realistic than the simple quoter, this is still educational. +IT SHOULD NOT BE USED AS A TRADING STRATEGY without additional risk management! +""" + +import asyncio +import warnings +from datetime import UTC, datetime, timedelta +from decimal import Decimal +from logging import Logger +from typing import List + +from config import ADMIN_TEST_WALLET as TEST_WALLET +from config import SESSION_KEY_PRIVATE_KEY + +from derive_client import WebSocketClient +from derive_client.data_types import Environment +from derive_client.data_types.channel_models import QuoteResultSchema +from derive_client.data_types.generated_models import ( + AssetType, + Direction, + LegPricedSchema, + OrderResponseSchema, + OrderType, + PositionResponseSchema, + PrivateSendQuoteResultSchema, + PublicGetTickerResultSchema, + RFQResultPublicSchema, + Status, + TradeResponseSchema, + TxStatus4, +) +from derive_client.data_types.utils import D + +warnings.filterwarnings("ignore", category=DeprecationWarning) + +SUBACCOUNT_ID = 31049 + +# Quoting parameters +UNDERLYING_TO_QUOTE = "ETH" # Only quote RFQs for ETH options +QUOTE_SPREAD_BPS = D("0") # Additional spread to add to bid-ask (0 = quote at market) +FALLBACK_TO_MARK_PRICE_PREMIUM_BPS = D("1000") # 10% premium if no bid-ask available + +# Hedging parameters - these define our risk limits +MAX_DELTA_TO_QUOTE = D("100.0") # Maximum delta impact from a single RFQ we'll accept +MIN_DELTA_EXPOSURE = D("-0.1") # Minimum portfolio delta before hedging (slightly short) +MAX_DELTA_EXPOSURE = D("0.1") # Maximum portfolio delta before hedging (slightly long) +HEDGE_INTERVAL = 30 # Seconds between periodic delta checks +HEDGE_ORDER_LABEL = "delta_hedge" # Label for hedge orders to track them +HEDGE_ORDER_TIMEOUT_S = 60 # How long to wait for hedge order to fill before cancelling + + +class DeltaQuoterStrategy: + """ + Strategy for deciding which RFQs to quote and how to price them. + + This class handles: + - Filtering RFQs based on criteria (underlying, instrument type, delta impact) + - Calculating delta exposure from options positions + - Pricing legs using market bid-ask prices + """ + + client: WebSocketClient + logger: Logger + + def __init__(self, client: WebSocketClient, logger: Logger): + self.client = client + self.logger = logger + + async def should_quote(self, rfq: RFQResultPublicSchema) -> bool: + """ + Determine whether to quote on this RFQ based on risk limits and filters. + + Checks: + 1. Is the RFQ for our target underlying (ETH)? + 2. Are all legs options (not perps or spot)? + 3. Would accepting this RFQ exceed our single-trade delta limit? + 4. Can we successfully calculate delta for all legs? + + Returns: + True if we should quote, False otherwise + """ + # Implement logic to decide whether to quote the RFQ based on current portfolio delta exposure + # we have a simple descrimintaor here that only quotes RFQs on a specific underlying + is_for_target_underlying = all([UNDERLYING_TO_QUOTE in leg.instrument_name for leg in rfq.legs]) + + def is_option(instrument_name: str) -> bool: + """Check if instrument is an option (ends with -C for call or -P for put)""" + return instrument_name.endswith(("-C", "-P")) + + is_only_options = all([is_option(leg.instrument_name) for leg in rfq.legs]) + if not is_for_target_underlying or not is_only_options: + return False + + # Calculate the delta impact of this RFQ on our portfolio + total_delta, is_error = await self.calculate_delta_from_quote(rfq) + self.logger.info(f" - RFQ {rfq.rfq_id} total delta impact would be {total_delta}") + + # Only quote if all conditions are met + return all( + [ + is_for_target_underlying, + is_only_options, + abs(total_delta) <= MAX_DELTA_TO_QUOTE, # Delta impact is within limits + not is_error, # No errors calculating delta + ] + ) + + async def calculate_delta_from_quote( + self, quote: QuoteResultSchema | RFQResultPublicSchema + ) -> tuple[Decimal, bool]: + """ + Calculate the net delta exposure from accepting a quote or RFQ. + + Delta calculation logic: + - Each option has a delta value (from 0 to 1 for calls, 0 to -1 for puts) + - Multiply delta by amount to get total delta per leg + - As the SELLER of the quote, we take the opposite side of the taker + - If taker is buying (we sell), we subtract delta + - If taker is selling (we buy), we add delta + + Returns: + tuple: (total_delta, is_error) + - total_delta: Net delta exposure we would have after this trade + - is_error: True if we couldn't calculate delta (missing data) + """ + total_delta = D("0.0") + is_error = False + for leg in quote.legs: + ticker = await self.client.markets.get_ticker(instrument_name=leg.instrument_name) + if not ticker.option_pricing: + self.logger.info( + f" - Cannot calculate delta for leg {leg.instrument_name} due to missing option pricing data." + ) + is_error = True + break + leg_delta = ticker.option_pricing.delta * leg.amount + # we are the SELLER of the quote so we are in effect taking the opposite side of the leg direction + # we therefore subtract the delta for buy legs and add for sell legs + if leg.direction == Direction.sell: + total_delta += leg_delta + else: + total_delta -= leg_delta + return total_delta, is_error + + async def price_legs(self, rfq: RFQResultPublicSchema) -> List[LegPricedSchema]: + """ + Price all legs of an RFQ using current market bid-ask prices. + + Pricing strategy: + - Use bid-ask prices from the order book (better than mark price for execution) + - For legs we BUY: Use best ask price (we pay the ask) + - For legs we SELL: Use best bid price (we receive the bid) + - Fallback to mark price + premium if no bid-ask available + - Verify delta impact is within our limits before pricing + + Returns: + List of priced legs, or empty list if we shouldn't quote + """ + # Implement logic to price the legs of the RFQ + priced_legs = [] + # First check if the delta impact is acceptable + expected_delta, is_error = await self.calculate_delta_from_quote(rfq) + if is_error or abs(expected_delta) > MAX_DELTA_TO_QUOTE: + return [] + + for unpriced_leg in rfq.legs: + ticker: PublicGetTickerResultSchema = await self.client.markets.get_ticker( + instrument_name=unpriced_leg.instrument_name + ) + # We base pricing on the current order book bid-ask prices + # This is more accurate than mark price for immediate execution + if unpriced_leg.direction == Direction.buy: + # Taker wants to buy, we sell -> quote at ask price + if ticker.best_ask_price is None or ticker.best_ask_price == D("0"): + self.logger.info( + f" - fallback pricing used as no ask price for: {unpriced_leg.instrument_name}." + ) + # Fallback: Use mark price with a premium to compensate for illiquidity + base_price = ticker.mark_price * (D("1") + FALLBACK_TO_MARK_PRICE_PREMIUM_BPS / D("10000")) + else: + base_price = ticker.best_ask_price + else: + # Taker wants to sell, we buy -> quote at bid price + if ticker.best_bid_price is None or ticker.best_bid_price == D("0"): + self.logger.info( + f" - fallback pricing used as no bid price for: {unpriced_leg.instrument_name}." + ) + # Fallback: Use mark price with a discount for illiquidity + base_price = ticker.mark_price * (D("1") - FALLBACK_TO_MARK_PRICE_PREMIUM_BPS / D("10000")) + else: + base_price = ticker.best_bid_price + # Round to the instrument's tick size + price = base_price.quantize(ticker.tick_size) + priced_leg = LegPricedSchema( + price=price, + amount=unpriced_leg.amount, + direction=unpriced_leg.direction, + instrument_name=unpriced_leg.instrument_name, + ) + priced_legs.append(priced_leg) + return priced_legs + + +class PortfolioDeltaCalculator: + """ + Calculator for determining the total delta exposure across our entire portfolio. + + This class: + - Fetches all open positions (options, perpetuals, spot) + - Calculates delta contribution from each position type + - Returns the total portfolio delta + + Portfolio delta is the sum of: + - Options: delta * amount (from option pricing models) + - Perpetuals: amount (perps have delta = 1) + - Spot: amount (spot has delta = 1) + """ + + client: WebSocketClient + logger: Logger + + def __init__(self, client: WebSocketClient, logger: Logger): + self.client = client + self.logger = logger + + async def calculate_portfolio_delta(self) -> Decimal: + """ + Calculate total portfolio delta across all positions. + + Returns: + Decimal: Net delta exposure (positive = long, negative = short) + """ + # Implement logic to calculate the current portfolio delta + # Fetch all open positions for our underlying + positions: List[PositionResponseSchema] = await self.client.positions.list( + is_open=True, currency=UNDERLYING_TO_QUOTE + ) + # Separate positions by type for clarity + option_positions = [ + p + for p in positions + if p.instrument_name + and p.instrument_type == AssetType.option + and p.instrument_name.startswith(UNDERLYING_TO_QUOTE) + ] + + perp_positions = [ + p + for p in positions + if p.instrument_name + and p.instrument_type == AssetType.perp + and p.instrument_name.startswith(UNDERLYING_TO_QUOTE) + ] + spot_positions = [ + p + for p in positions + if p.instrument_name + and p.instrument_type == AssetType.erc20 + and p.instrument_name.startswith(UNDERLYING_TO_QUOTE) + ] + # Calculate delta contribution from each position type + option_deltas = sum([p.delta * p.amount for p in option_positions]) + perp_delta = sum([p.amount for p in perp_positions]) # Perp has delta of 1 per unit + spot_delta = sum([p.amount for p in spot_positions]) # Spot has delta of 1 per unit + total_delta = option_deltas + perp_delta + spot_delta + return Decimal(total_delta) + + +class DeltaHedgerRfqQuoter: + """ + Complete RFQ quoter with automated delta hedging. + + This is the main class that coordinates: + 1. Receiving and filtering RFQs + 2. Pricing and sending quotes + 3. Monitoring quote fills + 4. Calculating portfolio delta exposure + 5. Executing hedge trades when needed + + The class maintains several concurrent tasks: + - Main event loop: Handles incoming RFQs and quotes + - Hedging task: Monitors delta and executes hedges + - WebSocket callbacks: Process real-time updates + """ + + logger: Logger + client: WebSocketClient + + def __init__(self, client: WebSocketClient): + self.client = client + self.logger = client._logger + self.portfolio_delta_calculator = PortfolioDeltaCalculator(client, self.logger) + self.delta_quoter_strategy = DeltaQuoterStrategy(client, self.logger) + self.quotes: dict[str, PrivateSendQuoteResultSchema] = {} # Track active quotes + # Hedging state management + self.hedging_queue = asyncio.Queue() # Queue delta calculations for hedging task + self.hedger_task: asyncio.Task[None] # Background task that executes hedges + self.hedge_order: OrderResponseSchema | None = None # Current active hedge order + self.hedge_lock = asyncio.Lock() # Prevent concurrent hedge executions + # State locks to prevent race conditions + self.quoting_lock = asyncio.Lock() + + async def create_quote(self, rfq): + """ + Create a quote for an RFQ if it passes our strategy filters. + + Returns: + List of priced legs if we should quote, empty list otherwise + """ + # Price legs using current market prices NOTE! This is just an example and not a trading strategy!!! + if not await self.delta_quoter_strategy.should_quote(rfq): + self.logger.info(f" - Skipping quoting for RFQ {rfq.rfq_id} based on strategy decision.") + return [] + priced_legs = await self.delta_quoter_strategy.price_legs(rfq) + self.logger.info( + f" ✓ Priced legs for RFQ {rfq.rfq_id} at total price {sum(leg.price * leg.amount for leg in priced_legs)}" + ) + return priced_legs + + async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): + """ + Handle incoming RFQ updates from the exchange. + + Similar to simple quoter but with delta-aware filtering. + """ + # Clean up quotes for expired/cancelled RFQs + for rfq in rfqs: + async with self.quoting_lock: + if rfq.status in {Status.expired, Status.cancelled} and rfq.rfq_id in self.quotes: + del self.quotes[rfq.rfq_id] + + # Filter for open RFQs + open_rfqs = [r for r in rfqs if r.status == Status.open] + if not open_rfqs: + return + + # Price RFQs in parallel (strategy will filter based on delta limits) + priced = await asyncio.gather(*(self.create_quote(r) for r in open_rfqs)) + quotable = [(r, legs) for r, legs in zip(open_rfqs, priced) if legs] + + if not quotable: + return + + # Send quotes (always as seller - we're the market maker) + results = await asyncio.gather( + *(self.client.rfq.send_quote(rfq_id=r.rfq_id, legs=legs, direction=Direction.sell) for r, legs in quotable), + return_exceptions=True, + ) + # Track successfully sent quotes + for r, result in zip(quotable, results): + rfq, _ = r + if isinstance(result, PrivateSendQuoteResultSchema): + async with self.quoting_lock: + self.quotes[rfq.rfq_id] = result + else: + self.logger.info(f" ❌ Failed to send quote for RFQ {rfq.rfq_id}: {result}") + + async def on_quote(self, quotes_list: List[QuoteResultSchema]): + """ + Handle quote status updates. + + When quotes expire or fill, we clean up tracking. + No hedging is triggered here - that happens in on_trade_settlement. + """ + for quote in quotes_list: + self.logger.info(f" - Quote {quote.quote_id} {quote.rfq_id}: {quote.status}") + async with self.quoting_lock: + if quote.status == Status.expired and quote.rfq_id in self.quotes: + del self.quotes[quote.rfq_id] + self.logger.info(f" ✗ Our quote {quote.quote_id} expired. Better luck next time!") + elif quote.status == Status.filled and quote.rfq_id in self.quotes: + del self.quotes[quote.rfq_id] + self.logger.info(f" ✓ Our quote {quote.quote_id} was accepted!") + + async def execute_hedge(self, delta_to_hedge: Decimal): + """ + Execute a hedge trade to neutralize delta exposure. + + Hedging logic: + - If portfolio delta is negative (short), buy perpetuals to neutralize + - If portfolio delta is positive (long), sell perpetuals to neutralize + - Use market bid-ask prices for immediate execution + - Set order timeout to prevent hanging orders + + Args: + delta_to_hedge: Amount of delta to hedge (negative = need to buy, positive = need to sell) + """ + instrument_name = f"{UNDERLYING_TO_QUOTE}-PERP" + # Only allow one hedge order at a time + if self.hedge_order is not None: + self.logger.info( + f" - Existing hedge {self.hedge_order.order_id} in progress, skipping new hedge {delta_to_hedge}." + ) + return + + # Fetch current market prices for the perpetual + ticker = await self.client.markets.get_ticker(instrument_name=instrument_name) + + # Determine trade direction: + # If delta_to_hedge is negative, we need to increase delta (buy) + # If delta_to_hedge is positive, we need to decrease delta (sell) + trade_direction = Direction.sell if delta_to_hedge < 0 else Direction.buy + price = ticker.best_bid_price if trade_direction == Direction.sell else ticker.best_ask_price + if price is None or price == D("0"): + # Fallback to mark price if no bid-ask available + price = ticker.mark_price + + trade_amount = abs(delta_to_hedge) + # Check if order meets minimum size requirements + if trade_amount < ticker.minimum_amount: + self.logger.info( + f" - Hedge amount {trade_amount} is below min order size {ticker.minimum_amount}, skipping." + ) + return + + self.logger.info(f" - Executing hedge for delta amount: {delta_to_hedge} in direction {trade_direction}") + # Place hedge order with timeout to prevent hanging orders + self.hedge_order = await self.client.orders.create( + amount=trade_amount, + instrument_name=instrument_name, + limit_price=price.quantize(ticker.tick_size), + direction=trade_direction, + order_type=OrderType.limit, + reduce_only=False, + label=HEDGE_ORDER_LABEL, # Label helps us track hedge orders + reject_timestamp=int((datetime.now(UTC) + timedelta(seconds=HEDGE_ORDER_TIMEOUT_S)).timestamp() * 1000), + ) + + async def on_trade_settlement(self, trades: List[TradeResponseSchema]): + """ + Handle trade settlement notifications. + + When RFQ trades settle, we need to recalculate our portfolio delta + and potentially execute a hedge. This is the key trigger for hedging. + """ + + rfq_trades = [] + for trade in trades: + self.logger.info( + f" - {trade.instrument_name}-{trade.direction} {trade.trade_amount} at {trade.trade_price}" + ) + # Identify trades from RFQ fills (they have a quote_id) + if trade.quote_id: + rfq_trades.append(trade) + + if rfq_trades: + self.logger.info(f" ✓ Detected {len(rfq_trades)} RFQ trades settled, re-evaluating portfolio delta.") + # Queue a delta recalculation which will trigger hedging if needed + await self.hedging_queue.put(await self.portfolio_delta_calculator.calculate_portfolio_delta()) + + async def on_order(self, orders: List[OrderResponseSchema]): + """ + Handle order status updates. + + When our hedge orders complete (filled/cancelled/expired), we: + 1. Clear the hedge_order state to allow new hedges + 2. Recalculate portfolio delta to see if more hedging is needed + """ + for order in orders: + self.logger.info(f" - Order {order.order_id} status update: {order.order_status}") + # Check if this is one of our hedge orders + if order.label == HEDGE_ORDER_LABEL and order.order_status in { + Status.filled, + Status.cancelled, + Status.expired, + }: + self.logger.info( + f" ✓ Hedge order {order.order_id} status {order.order_status}, re-evaluating total delta." + ) + self.hedge_order = None # Clear hedge order state + # Queue delta recalculation to check if we need more hedging + await self.hedging_queue.put(await self.portfolio_delta_calculator.calculate_portfolio_delta()) + + async def run(self): + """ + Start the quoter and all its background tasks. + + Sets up: + 1. WebSocket subscriptions for RFQs, quotes, trades, and orders + 2. Background hedging task that monitors portfolio delta + 3. Initial delta calculation on startup + """ + await self.client.connect() + # Subscribe to all the channels we need + await self.client.private_channels.rfqs_by_wallet(wallet=TEST_WALLET, callback=self.on_rfq) + await self.client.private_channels.quotes_by_subaccount_id( + subaccount_id=str(SUBACCOUNT_ID), + callback=self.on_quote, + ) + await self.client.private_channels.trades_tx_status_by_subaccount_id( + subaccount_id=SUBACCOUNT_ID, + callback=self.on_trade_settlement, + tx_status=TxStatus4.settled, # Only get settled trades + ) + await self.client.private_channels.orders_by_subaccount_id( + subaccount_id=str(SUBACCOUNT_ID), + callback=self.on_order, + ) + # Start background hedging task + self.hedger_task = asyncio.create_task(self.portfolio_hedging_task()) + # Calculate initial portfolio delta on startup + await self.hedging_queue.put(await self.portfolio_delta_calculator.calculate_portfolio_delta()) + await asyncio.Event().wait() # Keep running indefinitely + + async def portfolio_hedging_task(self): + """ + Background task that continuously monitors and hedges portfolio delta. + + This task runs in parallel with the main event loop and: + 1. Processes delta calculations from the queue (triggered by trades/orders) + 2. Periodically recalculates delta (every HEDGE_INTERVAL seconds) + 3. Executes hedge trades when delta exceeds MIN/MAX_DELTA_EXPOSURE limits + + The queue-based approach ensures we don't miss hedging opportunities + even during rapid trading, while periodic checks catch any drift. + """ + # Implement periodic portfolio delta checking and hedging if necessary + + last_check_time = datetime.now(UTC) + + while True: + # Drain the queue - get the latest delta if available + portfolio_delta: Decimal | None = None + while True: + try: + portfolio_delta = self.hedging_queue.get_nowait() + except asyncio.QueueEmpty: + break + + # If no queued delta, check if it's time for periodic recalculation + if portfolio_delta is None: + now = datetime.now(UTC) + if (now - last_check_time).total_seconds() >= HEDGE_INTERVAL: + portfolio_delta = await self.portfolio_delta_calculator.calculate_portfolio_delta() + last_check_time = now + else: + # Not time yet, sleep and try again + await asyncio.sleep(1) + continue + + # Check if delta is outside our acceptable range and hedge if needed + async with self.hedge_lock: + if portfolio_delta < MIN_DELTA_EXPOSURE or portfolio_delta > MAX_DELTA_EXPOSURE: + # Calculate how much to hedge to bring delta back to neutral + delta_to_hedge = -portfolio_delta + self.logger.info( + f" - Portfolio delta {portfolio_delta} outside exposure limits, hedging {delta_to_hedge}." + ) + await self.execute_hedge(delta_to_hedge) + + +async def main(): + """ + Initialize and run the delta-hedged RFQ quoter. + + This creates a sophisticated market maker that: + - Quotes on incoming ETH options RFQs + - Automatically hedges delta exposure by trading perpetuals + - Maintains delta within configured risk limits + - Runs continuously with automatic reconnection + + To use this effectively, you should: + 1. Adjust risk parameters (MAX_DELTA_TO_QUOTE, MIN/MAX_DELTA_EXPOSURE) + 2. Monitor the logs to understand hedging behavior + 3. Test thoroughly on testnet before production use + 4. Consider adding additional risk controls (position limits, PnL stops, etc.) + """ + + client = WebSocketClient( + session_key=SESSION_KEY_PRIVATE_KEY, + wallet=TEST_WALLET, + env=Environment.TEST, + subaccount_id=SUBACCOUNT_ID, + ) + rfq_quoter = DeltaHedgerRfqQuoter(client) + # Run indefinitely with automatic reconnection on failure + while True: + try: + await rfq_quoter.run() + except KeyboardInterrupt: + break + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/rfqs/create_rfq.py b/examples/rfqs/create_rfq.py deleted file mode 100644 index 7b025c1d..00000000 --- a/examples/rfqs/create_rfq.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Create an rfq using the REST API. -""" - -import asyncio -from typing import List - -import rich_click as click -from config import OWNER_TEST_WALLET, SESSION_KEY_PRIVATE_KEY, TAKER_SUBACCOUNT_ID -from rich import print - -from derive_client import WebSocketClient -from derive_client.data_types import Environment -from derive_client.data_types.channel_models import BestQuoteChannelResultSchema -from derive_client.data_types.generated_models import AssetType, Direction, LegUnpricedSchema -from derive_client.data_types.utils import D - -SLEEP_TIME = 1 - - -async def main( - side: str, - amount: float, - instrument: str, - instrument_type: AssetType = AssetType.option, -): - """ - Sample of polling for RFQs and printing their status. - """ - client: WebSocketClient = WebSocketClient( - session_key=SESSION_KEY_PRIVATE_KEY, - wallet=OWNER_TEST_WALLET, - env=Environment.TEST, - subaccount_id=TAKER_SUBACCOUNT_ID, - ) - await client.connect() - - # we get an option market - markets = await client.markets.fetch_instruments( - instrument_type=instrument_type, - expired=False, - ) - - if instrument: - markets = [m for m in markets if m == instrument] - if not markets: - click.echo(f"No market found for instrument {instrument}. Please check the instrument name and try again.") - return - - request_direction: Direction = Direction.buy if side.lower() == 'buy' else Direction.sell - - result = await client.rfq.send_rfq( - legs=[ - LegUnpricedSchema( - amount=D(amount), - instrument_name=instrument, - direction=request_direction, - ) - ], - ) - print("RFQ created with id:", result.rfq_id) - - def on_new_quote(quotes: List[BestQuoteChannelResultSchema]): - """ - Handle a new quote received for the RFQ. - """ - for quote in quotes: - if quote.result and quote.result.best_quote: - print(f"New best quote received: {quote.result.best_quote}") - - await client.private_channels.best_quotes_by_subaccount_id( - subaccount_id=str(TAKER_SUBACCOUNT_ID), - callback=on_new_quote, - ) - - await asyncio.sleep(30) - print("Final quotes:") - - -@click.group() -def rfq(): - """RFQ related commands.""" - pass - - -@rfq.command(help="Create an RFQ and poll for quotes") -@click.option( - '-s', - '--side', - type=click.Choice(['buy', 'sell'], case_sensitive=False), - required=True, - help="Side of the RFQ (buy or sell)", -) -@click.option( - '-a', - '--amount', - type=click.FLOAT, - required=False, - default=1.0, - help="Amount of the Leg of the RFQ", -) -@click.option( - '-i', - '--instrument', - type=click.STRING, - required=False, - default=None, - help="Instrument name to use for the RFQ (e.g. ETH-30JUN23-1500-C)", -) -@click.option( - '-it', - '--instrument-type', - type=AssetType, - required=False, - default=AssetType.option, - help="Instrument name to use for the RFQ (e.g. ETH-30JUN23-1500-C)", -) -def create(side: str, amount: float, instrument: str, instrument_type: AssetType = AssetType.option): - """ - Sample of polling for RFQs and printing their status. - """ - click.echo( - f"Creating RFQ: side={side}, amount={amount}, instrument={instrument}, instrument_type={instrument_type}" - ) - - asyncio.run( - main( - side=side, - amount=amount, - instrument=instrument, - instrument_type=instrument_type, - ) - ) - - click.echo("RFQ process completed.") - - -if __name__ == "__main__": - rfq() diff --git a/examples/rfqs/poll_rfq.py b/examples/rfqs/poll_rfq.py deleted file mode 100644 index 19741f0a..00000000 --- a/examples/rfqs/poll_rfq.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Example of how to poll RFQ (Request for Quote) status and handle transfers between subaccount and funding account. -""" - -import asyncio -from time import sleep - -from config import ADMIN_TEST_WALLET as TEST_WALLET -from config import SESSION_KEY_PRIVATE_KEY -from rich import print - -from derive_client import WebSocketClient -from derive_client._clients.utils import DeriveJSONRPCError -from derive_client.data_types import Environment -from derive_client.data_types.generated_models import ( - Direction, - LegPricedSchema, - RFQResultPublicSchema, - Status, -) -from derive_client.data_types.utils import D - -SLEEP_TIME = 1 -SUBACCOUNT_ID = 31049 - - -async def create_priced_legs(client: WebSocketClient, rfq): - # Price legs using current market prices - priced_legs = [] - for unpriced_leg in rfq.legs: - ticker = await client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) - - base_price = ticker.index_price - - price = base_price * D("1.02") if unpriced_leg.direction == Direction.buy else base_price * D("0.98") - - price = price.quantize(ticker.tick_size) - priced_leg = LegPricedSchema( - price=price, - amount=unpriced_leg.amount, - direction=unpriced_leg.direction, - instrument_name=unpriced_leg.instrument_name, - ) - priced_legs.append(priced_leg) - - return priced_legs - - -async def main(): - """ - Sample of polling for RFQs and printing their status. - """ - - client = WebSocketClient( - session_key=SESSION_KEY_PRIVATE_KEY, - wallet=TEST_WALLET, - env=Environment.TEST, - subaccount_id=SUBACCOUNT_ID, - ) - - async def on_rfq(rfq: RFQResultPublicSchema): - # here we get a price for the rfq. - # we first get the index price for the instrument - print(f"Received RFQ: {rfq}") - priced_legs = await create_priced_legs(client, rfq) - print(f"Total legs price: {sum([leg.price * leg.amount for leg in priced_legs])}") - try: - await client.rfq.send_quote(rfq_id=rfq.rfq_id, legs=priced_legs, direction=Direction.sell) - except DeriveJSONRPCError as e: - print(f"Error creating quote for RFQ {rfq.rfq_id}: {e}") - return - - await client.connect() - - rfqs = [] - from_timestamp = 0 - while True: - new_rfqs = await client.rfq.poll_rfqs(from_timestamp=from_timestamp) - for rfq in new_rfqs.rfqs: - if rfq.last_update_timestamp > from_timestamp: - from_timestamp = rfq.last_update_timestamp + 1 - if rfq.status is Status.open: - task = asyncio.create_task(on_rfq(rfq)) - rfqs.append(task) - for task in rfqs: - if task.done(): - rfqs.remove(task) - - sleep(SLEEP_TIME) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/specs/channels/channel.subaccount_id.best.quotes.json b/specs/channels/channel.subaccount_id.best.quotes.json index e5515e84..503ccced 100644 --- a/specs/channels/channel.subaccount_id.best.quotes.json +++ b/specs/channels/channel.subaccount_id.best.quotes.json @@ -453,42 +453,7 @@ "additionalProperties": false }, "LegPricedSchema": { - "type": "object", - "required": [ - "amount", - "direction", - "instrument_name", - "price" - ], - "properties": { - "amount": { - "title": "amount", - "type": "string", - "format": "decimal", - "description": "Amount in units of the base" - }, - "direction": { - "title": "direction", - "type": "string", - "enum": [ - "buy", - "sell" - ], - "description": "Leg direction" - }, - "instrument_name": { - "title": "instrument_name", - "type": "string", - "description": "Instrument name" - }, - "price": { - "title": "price", - "type": "string", - "format": "decimal", - "description": "Leg price" - } - }, - "additionalProperties": false + "$ref": "./openapi-spec.json#/components/schemas/LegPricedSchema" }, "SubaccountIdBestQuotesPubSubSchema": { "type": "object", diff --git a/specs/channels/channel.subaccount_id.quotes.json b/specs/channels/channel.subaccount_id.quotes.json index ff28c13f..f8e9740b 100644 --- a/specs/channels/channel.subaccount_id.quotes.json +++ b/specs/channels/channel.subaccount_id.quotes.json @@ -59,240 +59,11 @@ "additionalProperties": false }, "QuoteResultSchema": { - "type": "object", - "required": [ - "cancel_reason", - "creation_timestamp", - "direction", - "fee", - "fill_pct", - "is_transfer", - "label", - "last_update_timestamp", - "legs", - "legs_hash", - "liquidity_role", - "max_fee", - "mmp", - "nonce", - "quote_id", - "rfq_id", - "signature", - "signature_expiry_sec", - "signer", - "status", - "subaccount_id", - "tx_hash", - "tx_status" - ], - "properties": { - "cancel_reason": { - "title": "cancel_reason", - "type": "string", - "enum": [ - "", - "user_request", - "insufficient_margin", - "signed_max_fee_too_low", - "mmp_trigger", - "cancel_on_disconnect", - "session_key_deregistered", - "subaccount_withdrawn", - "rfq_no_longer_open", - "compliance" - ], - "description": "Cancel reason, if any" - }, - "creation_timestamp": { - "title": "creation_timestamp", - "type": "integer", - "description": "Creation timestamp in ms since Unix epoch" - }, - "direction": { - "title": "direction", - "type": "string", - "enum": [ - "buy", - "sell" - ], - "description": "Quote direction" - }, - "fee": { - "title": "fee", - "type": "string", - "format": "decimal", - "description": "Fee paid for this quote (if executed)" - }, - "fill_pct": { - "title": "fill_pct", - "type": "string", - "format": "decimal", - "description": "Percentage of the RFQ that this quote would fill, from 0 to 1." - }, - "is_transfer": { - "title": "is_transfer", - "type": "boolean", - "description": "Whether the order was generated through `private/transfer_position`" - }, - "label": { - "title": "label", - "type": "string", - "description": "User-defined label, if any" - }, - "last_update_timestamp": { - "title": "last_update_timestamp", - "type": "integer", - "description": "Last update timestamp in ms since Unix epoch" - }, - "legs": { - "title": "legs", - "type": "array", - "description": "Quote legs", - "items": { - "type": "object", - "$ref": "#/definitions/LegPricedSchema", - "field_many": true - } - }, - "legs_hash": { - "title": "legs_hash", - "type": "string", - "description": "Hash of the legs of the best quote to be signed by the taker." - }, - "liquidity_role": { - "title": "liquidity_role", - "type": "string", - "enum": [ - "maker", - "taker" - ], - "description": "Liquidity role" - }, - "max_fee": { - "title": "max_fee", - "type": "string", - "format": "decimal", - "description": "Signed max fee" - }, - "mmp": { - "title": "mmp", - "type": "boolean", - "description": "Whether the quote is tagged for market maker protections (default false)" - }, - "nonce": { - "title": "nonce", - "type": "integer", - "description": "Nonce" - }, - "quote_id": { - "title": "quote_id", - "type": "string", - "format": "uuid", - "description": "Quote ID" - }, - "rfq_id": { - "title": "rfq_id", - "type": "string", - "format": "uuid", - "description": "RFQ ID" - }, - "signature": { - "title": "signature", - "type": "string", - "description": "Ethereum signature of the quote" - }, - "signature_expiry_sec": { - "title": "signature_expiry_sec", - "type": "integer", - "description": "Unix timestamp in seconds" - }, - "signer": { - "title": "signer", - "type": "string", - "description": "Owner wallet address or registered session key that signed the quote" - }, - "status": { - "title": "status", - "type": "string", - "enum": [ - "open", - "filled", - "cancelled", - "expired" - ], - "description": "Status" - }, - "subaccount_id": { - "title": "subaccount_id", - "type": "integer", - "description": "Subaccount ID" - }, - "tx_hash": { - "title": "tx_hash", - "type": [ - "string", - "null" - ], - "default": null, - "description": "Blockchain transaction hash (only for executed quotes)" - }, - "tx_status": { - "title": "tx_status", - "type": [ - "string", - "null" - ], - "default": null, - "enum": [ - "requested", - "pending", - "settled", - "reverted", - "ignored", - "timed_out" - ], - "description": "Blockchain transaction status (only for executed quotes)" - } - }, - "additionalProperties": false + "$ref": "./openapi-spec.json#/components/schemas/QuoteResultSchema" + }, "LegPricedSchema": { - "type": "object", - "required": [ - "amount", - "direction", - "instrument_name", - "price" - ], - "properties": { - "amount": { - "title": "amount", - "type": "string", - "format": "decimal", - "description": "Amount in units of the base" - }, - "direction": { - "title": "direction", - "type": "string", - "enum": [ - "buy", - "sell" - ], - "description": "Leg direction" - }, - "instrument_name": { - "title": "instrument_name", - "type": "string", - "description": "Instrument name" - }, - "price": { - "title": "price", - "type": "string", - "format": "decimal", - "description": "Leg price" - } - }, - "additionalProperties": false + "$ref": "./openapi-spec.json#/components/schemas/LegPricedSchema" }, "SubaccountIdQuotesPubSubSchema": { "type": "object", diff --git a/specs/channels/channel.wallet.rfqs.json b/specs/channels/channel.wallet.rfqs.json index 7ec6d96a..960cae50 100644 --- a/specs/channels/channel.wallet.rfqs.json +++ b/specs/channels/channel.wallet.rfqs.json @@ -59,151 +59,7 @@ "additionalProperties": false }, "RFQResultPublicSchema": { - "type": "object", - "required": [ - "cancel_reason", - "creation_timestamp", - "fill_rate", - "filled_direction", - "filled_pct", - "last_update_timestamp", - "legs", - "partial_fill_step", - "recent_fill_rate", - "rfq_id", - "status", - "subaccount_id", - "total_cost", - "valid_until", - "wallet" - ], - "properties": { - "cancel_reason": { - "title": "cancel_reason", - "type": "string", - "enum": [ - "", - "user_request", - "insufficient_margin", - "signed_max_fee_too_low", - "mmp_trigger", - "cancel_on_disconnect", - "session_key_deregistered", - "subaccount_withdrawn", - "rfq_no_longer_open", - "compliance" - ], - "description": "Cancel reason, if any" - }, - "creation_timestamp": { - "title": "creation_timestamp", - "type": "integer", - "description": "Creation timestamp in ms since Unix epoch" - }, - "fill_rate": { - "title": "fill_rate", - "type": [ - "string", - "null" - ], - "format": "decimal", - "default": null, - "description": "Average taker fill rate, from 0 to 1. Returns null for users with insufficient RFQ history." - }, - "filled_direction": { - "title": "filled_direction", - "type": [ - "string", - "null" - ], - "default": null, - "enum": [ - "buy", - "sell" - ], - "description": "Direction at which the RFQ was filled (only if filled)" - }, - "filled_pct": { - "title": "filled_pct", - "type": "string", - "format": "decimal", - "description": "Percentage of the RFQ that has been filled, from 0 to 1." - }, - "last_update_timestamp": { - "title": "last_update_timestamp", - "type": "integer", - "description": "Last update timestamp in ms since Unix epoch" - }, - "legs": { - "title": "legs", - "type": "array", - "description": "RFQ legs", - "items": { - "type": "object", - "$ref": "#/definitions/LegUnpricedSchema", - "field_many": true - } - }, - "partial_fill_step": { - "title": "partial_fill_step", - "type": "string", - "format": "decimal", - "description": "Step size for partial fills (default: 1)" - }, - "recent_fill_rate": { - "title": "recent_fill_rate", - "type": [ - "string", - "null" - ], - "format": "decimal", - "default": null, - "description": "Taker fill rate, weighted towards the recent several days of activity, from 0 to 1. Returns null for users with insufficient recent RFQ history." - }, - "rfq_id": { - "title": "rfq_id", - "type": "string", - "format": "uuid", - "description": "RFQ ID" - }, - "status": { - "title": "status", - "type": "string", - "enum": [ - "open", - "filled", - "cancelled", - "expired" - ], - "description": "Status" - }, - "subaccount_id": { - "title": "subaccount_id", - "type": "integer", - "description": "Subaccount ID" - }, - "total_cost": { - "title": "total_cost", - "type": [ - "string", - "null" - ], - "format": "decimal", - "default": null, - "description": "Total cost for the RFQ (only if filled)" - }, - "valid_until": { - "title": "valid_until", - "type": "integer", - "description": "RFQ expiry timestamp in ms since Unix epoch" - }, - "wallet": { - "title": "wallet", - "type": "string", - "description": "Wallet address of the RFQ sender" - } - }, - "additionalProperties": false + "$ref": "./openapi-spec.json#/components/schemas/RFQResultPublicSchema" }, "LegUnpricedSchema": { "type": "object", diff --git a/specs/websocket-channels.json b/specs/websocket-channels.json index 191a210e..c2e6de01 100644 --- a/specs/websocket-channels.json +++ b/specs/websocket-channels.json @@ -1393,42 +1393,7 @@ "additionalProperties": false }, "LegPricedSchema": { - "type": "object", - "required": [ - "amount", - "direction", - "instrument_name", - "price" - ], - "properties": { - "amount": { - "title": "amount", - "type": "string", - "format": "decimal", - "description": "Amount in units of the base" - }, - "direction": { - "title": "direction", - "type": "string", - "enum": [ - "buy", - "sell" - ], - "description": "Leg direction" - }, - "instrument_name": { - "title": "instrument_name", - "type": "string", - "description": "Instrument name" - }, - "price": { - "title": "price", - "type": "string", - "format": "decimal", - "description": "Leg price" - } - }, - "additionalProperties": false + "$ref": "./openapi-spec.json#/components/schemas/LegPricedSchema" }, "SubaccountIdBestQuotesPubSubSchema": { "type": "object", @@ -1588,202 +1553,7 @@ "additionalProperties": false }, "QuoteResultSchema": { - "type": "object", - "required": [ - "cancel_reason", - "creation_timestamp", - "direction", - "fee", - "fill_pct", - "is_transfer", - "label", - "last_update_timestamp", - "legs", - "legs_hash", - "liquidity_role", - "max_fee", - "mmp", - "nonce", - "quote_id", - "rfq_id", - "signature", - "signature_expiry_sec", - "signer", - "status", - "subaccount_id", - "tx_hash", - "tx_status" - ], - "properties": { - "cancel_reason": { - "title": "cancel_reason", - "type": "string", - "enum": [ - "", - "user_request", - "insufficient_margin", - "signed_max_fee_too_low", - "mmp_trigger", - "cancel_on_disconnect", - "session_key_deregistered", - "subaccount_withdrawn", - "rfq_no_longer_open", - "compliance" - ], - "description": "Cancel reason, if any" - }, - "creation_timestamp": { - "title": "creation_timestamp", - "type": "integer", - "description": "Creation timestamp in ms since Unix epoch" - }, - "direction": { - "title": "direction", - "type": "string", - "enum": [ - "buy", - "sell" - ], - "description": "Quote direction" - }, - "fee": { - "title": "fee", - "type": "string", - "format": "decimal", - "description": "Fee paid for this quote (if executed)" - }, - "fill_pct": { - "title": "fill_pct", - "type": "string", - "format": "decimal", - "description": "Percentage of the RFQ that this quote would fill, from 0 to 1." - }, - "is_transfer": { - "title": "is_transfer", - "type": "boolean", - "description": "Whether the order was generated through `private/transfer_position`" - }, - "label": { - "title": "label", - "type": "string", - "description": "User-defined label, if any" - }, - "last_update_timestamp": { - "title": "last_update_timestamp", - "type": "integer", - "description": "Last update timestamp in ms since Unix epoch" - }, - "legs": { - "title": "legs", - "type": "array", - "description": "Quote legs", - "items": { - "type": "object", - "$ref": "#/definitions/LegPricedSchema", - "field_many": true - } - }, - "legs_hash": { - "title": "legs_hash", - "type": "string", - "description": "Hash of the legs of the best quote to be signed by the taker." - }, - "liquidity_role": { - "title": "liquidity_role", - "type": "string", - "enum": [ - "maker", - "taker" - ], - "description": "Liquidity role" - }, - "max_fee": { - "title": "max_fee", - "type": "string", - "format": "decimal", - "description": "Signed max fee" - }, - "mmp": { - "title": "mmp", - "type": "boolean", - "description": "Whether the quote is tagged for market maker protections (default false)" - }, - "nonce": { - "title": "nonce", - "type": "integer", - "description": "Nonce" - }, - "quote_id": { - "title": "quote_id", - "type": "string", - "format": "uuid", - "description": "Quote ID" - }, - "rfq_id": { - "title": "rfq_id", - "type": "string", - "format": "uuid", - "description": "RFQ ID" - }, - "signature": { - "title": "signature", - "type": "string", - "description": "Ethereum signature of the quote" - }, - "signature_expiry_sec": { - "title": "signature_expiry_sec", - "type": "integer", - "description": "Unix timestamp in seconds" - }, - "signer": { - "title": "signer", - "type": "string", - "description": "Owner wallet address or registered session key that signed the quote" - }, - "status": { - "title": "status", - "type": "string", - "enum": [ - "open", - "filled", - "cancelled", - "expired" - ], - "description": "Status" - }, - "subaccount_id": { - "title": "subaccount_id", - "type": "integer", - "description": "Subaccount ID" - }, - "tx_hash": { - "title": "tx_hash", - "type": [ - "string", - "null" - ], - "default": null, - "description": "Blockchain transaction hash (only for executed quotes)" - }, - "tx_status": { - "title": "tx_status", - "type": [ - "string", - "null" - ], - "default": null, - "enum": [ - "requested", - "pending", - "settled", - "reverted", - "ignored", - "timed_out" - ], - "description": "Blockchain transaction status (only for executed quotes)" - } - }, - "additionalProperties": false + "$ref": "./openapi-spec.json#/components/schemas/QuoteResultSchema" }, "SubaccountIdQuotesPubSubSchema": { "type": "object", @@ -2869,151 +2639,7 @@ "additionalProperties": false }, "RFQResultPublicSchema": { - "type": "object", - "required": [ - "cancel_reason", - "creation_timestamp", - "fill_rate", - "filled_direction", - "filled_pct", - "last_update_timestamp", - "legs", - "partial_fill_step", - "recent_fill_rate", - "rfq_id", - "status", - "subaccount_id", - "total_cost", - "valid_until", - "wallet" - ], - "properties": { - "cancel_reason": { - "title": "cancel_reason", - "type": "string", - "enum": [ - "", - "user_request", - "insufficient_margin", - "signed_max_fee_too_low", - "mmp_trigger", - "cancel_on_disconnect", - "session_key_deregistered", - "subaccount_withdrawn", - "rfq_no_longer_open", - "compliance" - ], - "description": "Cancel reason, if any" - }, - "creation_timestamp": { - "title": "creation_timestamp", - "type": "integer", - "description": "Creation timestamp in ms since Unix epoch" - }, - "fill_rate": { - "title": "fill_rate", - "type": [ - "string", - "null" - ], - "format": "decimal", - "default": null, - "description": "Average taker fill rate, from 0 to 1. Returns null for users with insufficient RFQ history." - }, - "filled_direction": { - "title": "filled_direction", - "type": [ - "string", - "null" - ], - "default": null, - "enum": [ - "buy", - "sell" - ], - "description": "Direction at which the RFQ was filled (only if filled)" - }, - "filled_pct": { - "title": "filled_pct", - "type": "string", - "format": "decimal", - "description": "Percentage of the RFQ that has been filled, from 0 to 1." - }, - "last_update_timestamp": { - "title": "last_update_timestamp", - "type": "integer", - "description": "Last update timestamp in ms since Unix epoch" - }, - "legs": { - "title": "legs", - "type": "array", - "description": "RFQ legs", - "items": { - "type": "object", - "$ref": "#/definitions/LegUnpricedSchema", - "field_many": true - } - }, - "partial_fill_step": { - "title": "partial_fill_step", - "type": "string", - "format": "decimal", - "description": "Step size for partial fills (default: 1)" - }, - "recent_fill_rate": { - "title": "recent_fill_rate", - "type": [ - "string", - "null" - ], - "format": "decimal", - "default": null, - "description": "Taker fill rate, weighted towards the recent several days of activity, from 0 to 1. Returns null for users with insufficient recent RFQ history." - }, - "rfq_id": { - "title": "rfq_id", - "type": "string", - "format": "uuid", - "description": "RFQ ID" - }, - "status": { - "title": "status", - "type": "string", - "enum": [ - "open", - "filled", - "cancelled", - "expired" - ], - "description": "Status" - }, - "subaccount_id": { - "title": "subaccount_id", - "type": "integer", - "description": "Subaccount ID" - }, - "total_cost": { - "title": "total_cost", - "type": [ - "string", - "null" - ], - "format": "decimal", - "default": null, - "description": "Total cost for the RFQ (only if filled)" - }, - "valid_until": { - "title": "valid_until", - "type": "integer", - "description": "RFQ expiry timestamp in ms since Unix epoch" - }, - "wallet": { - "title": "wallet", - "type": "string", - "description": "Wallet address of the RFQ sender" - } - }, - "additionalProperties": false + "$ref": "./openapi-spec.json#/components/schemas/RFQResultPublicSchema" }, "LegUnpricedSchema": { "type": "object",