From 7822febbea3a1301d1deae3eb51f9d580669e2e1 Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 18:13:43 +0000 Subject: [PATCH 01/19] feat:e2e-for-quoting --- derive_client/data_types/channel_models.py | 127 +++---- examples/rfqs/create_rfq.py | 140 +++---- examples/rfqs/poll_rfq.py | 93 ----- examples/rfqs/quote_rfq.py | 106 ++++++ .../channel.subaccount_id.best.quotes.json | 37 +- .../channel.subaccount_id.quotes.json | 235 +----------- specs/channels/channel.wallet.rfqs.json | 146 +------- specs/websocket-channels.json | 343 +----------------- 8 files changed, 221 insertions(+), 1006 deletions(-) delete mode 100644 examples/rfqs/poll_rfq.py create mode 100644 examples/rfqs/quote_rfq.py diff --git a/derive_client/data_types/channel_models.py b/derive_client/data_types/channel_models.py index 7a0e7635..a70e0f6c 100644 --- a/derive_client/data_types/channel_models.py +++ b/derive_client/data_types/channel_models.py @@ -23,6 +23,7 @@ PublicGetInstrumentParamsSchema, PublicGetOptionSettlementPricesParamsSchema, PublicMarginWatchResultSchema, + RFQResultPublicSchema, RPCErrorFormatSchema, Status, TickerSlimSchema, @@ -139,30 +140,8 @@ 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 LegPricedSchemaModel(LegPricedSchema): + pass class SubaccountIdTradesChannelSchema(SubaccountIdBalancesChannelSchema): @@ -241,6 +220,10 @@ class WalletRfqsChannelSchema(PrivateGetAllPortfoliosParamsSchema): pass +class LegUnpricedSchema1(LegUnpricedSchema): + pass + + class AuctionResultSchema(Struct): state: State subaccount_id: int @@ -285,9 +268,30 @@ class QuoteResultPublicSchema(Struct): tx_hash: Optional[str] = None -class SubaccountIdQuotesNotificationParamsSchema(Struct): - channel: str - data: List[QuoteResultSchema] +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[LegPricedSchemaModel] + 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_hash: Optional[str] = None + tx_status: Optional[TxStatus] = None class SubaccountIdTradesTxStatusNotificationParamsSchema(Struct): @@ -314,24 +318,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] @@ -376,16 +362,6 @@ class SubaccountIdOrdersNotificationParamsSchema(Struct): data: List[OrderResponseSchema] -class SubaccountIdQuotesNotificationSchema(Struct): - method: str - params: SubaccountIdQuotesNotificationParamsSchema - - -class SubaccountIdQuotesPubSubSchema(Struct): - channel_params: SubaccountIdQuotesChannelSchema - notification: SubaccountIdQuotesNotificationSchema - - class SubaccountIdTradesNotificationParamsSchema(SubaccountIdTradesTxStatusNotificationParamsSchema): pass @@ -425,11 +401,6 @@ class TradesInstrumentTypeCurrencyTxStatusPubSubSchema(Struct): notification: TradesInstrumentTypeCurrencyTxStatusNotificationSchema -class WalletRfqsNotificationParamsSchema(Struct): - channel: str - data: List[RFQResultPublicSchema] - - class AuctionsWatchNotificationSchema(Struct): method: str params: AuctionsWatchNotificationParamsSchema @@ -476,6 +447,11 @@ class SubaccountIdOrdersPubSubSchema(Struct): notification: SubaccountIdOrdersNotificationSchema +class SubaccountIdQuotesNotificationParamsSchema(Struct): + channel: str + data: List[QuoteResultSchema] + + class SubaccountIdTradesNotificationSchema(Struct): method: str params: SubaccountIdTradesNotificationParamsSchema @@ -491,14 +467,9 @@ class TickerSlimInstrumentNameIntervalNotificationParamsSchema(Struct): data: TickerSlimInstrumentNameIntervalPublisherDataSchema -class WalletRfqsNotificationSchema(Struct): - method: str - params: WalletRfqsNotificationParamsSchema - - -class WalletRfqsPubSubSchema(Struct): - channel_params: WalletRfqsChannelSchema - notification: WalletRfqsNotificationSchema +class WalletRfqsNotificationParamsSchema(Struct): + channel: str + data: List[RFQResultPublicSchema] class SubaccountIdBestQuotesNotificationParamsSchema(Struct): @@ -506,6 +477,16 @@ class SubaccountIdBestQuotesNotificationParamsSchema(Struct): data: List[BestQuoteChannelResultSchema] +class SubaccountIdQuotesNotificationSchema(Struct): + method: str + params: SubaccountIdQuotesNotificationParamsSchema + + +class SubaccountIdQuotesPubSubSchema(Struct): + channel_params: SubaccountIdQuotesChannelSchema + notification: SubaccountIdQuotesNotificationSchema + + class TickerSlimInstrumentNameIntervalNotificationSchema(Struct): method: str params: TickerSlimInstrumentNameIntervalNotificationParamsSchema @@ -516,6 +497,16 @@ class TickerSlimInstrumentNameIntervalPubSubSchema(Struct): notification: TickerSlimInstrumentNameIntervalNotificationSchema +class WalletRfqsNotificationSchema(Struct): + method: str + params: WalletRfqsNotificationParamsSchema + + +class WalletRfqsPubSubSchema(Struct): + channel_params: WalletRfqsChannelSchema + notification: WalletRfqsNotificationSchema + + class SubaccountIdBestQuotesNotificationSchema(Struct): method: str params: SubaccountIdBestQuotesNotificationParamsSchema diff --git a/examples/rfqs/create_rfq.py b/examples/rfqs/create_rfq.py index 7b025c1d..bc5cefe8 100644 --- a/examples/rfqs/create_rfq.py +++ b/examples/rfqs/create_rfq.py @@ -1,139 +1,97 @@ """ -Create an rfq using the REST API. +Simple demonstration of creating and executing an RFQ. """ 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.generated_models import Direction, LegUnpricedSchema from derive_client.data_types.utils import D +from derive_client.utils.logger import get_logger -SLEEP_TIME = 1 - -async def main( +async def create_and_execute_rfq( + instrument: str, side: str, amount: float, - instrument: str, - instrument_type: AssetType = AssetType.option, ): """ - Sample of polling for RFQs and printing their status. + Create an RFQ, wait for quotes, and execute the best one. + + Args: + instrument: Instrument name (e.g. "ETH-30JUN23-1500-C") + side: "buy" or "sell" + amount: Contract amount """ - client: WebSocketClient = WebSocketClient( + # Initialize client + 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() - # 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( + # Send RFQ + direction = Direction.buy if side.lower() == "buy" else Direction.sell + rfq_result = await client.rfq.send_rfq( legs=[ LegUnpricedSchema( amount=D(amount), instrument_name=instrument, - direction=request_direction, + direction=direction, ) ], ) - print("RFQ created with id:", result.rfq_id) + logger.info(f"✓ RFQ created: {rfq_result.rfq_id}") - def on_new_quote(quotes: List[BestQuoteChannelResultSchema]): - """ - Handle a new quote received for the RFQ. - """ + # Track best quote + best_quote = None + + def handle_quote(quotes: List[BestQuoteChannelResultSchema]): + nonlocal best_quote for quote in quotes: if quote.result and quote.result.best_quote: - print(f"New best quote received: {quote.result.best_quote}") + best_quote = quote.result.best_quote + total_price = sum(leg.price * leg.amount for leg in best_quote.legs) + logger.info(f"✓ Best quote received: {total_price}") + # Subscribe to quotes await client.private_channels.best_quotes_by_subaccount_id( subaccount_id=str(TAKER_SUBACCOUNT_ID), - callback=on_new_quote, + callback=handle_quote, ) - await asyncio.sleep(30) - print("Final quotes:") - + # Wait for quotes + await asyncio.sleep(10) -@click.group() -def rfq(): - """RFQ related commands.""" - pass + if not best_quote: + logger.error("✗ No quotes received") + return - -@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}" + # Execute quote + 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, ) + logger.info(f"✓ Quote executed at: {best_quote.quote_id}") + +if __name__ == "__main__": + # Example usage asyncio.run( - main( - side=side, - amount=amount, - instrument=instrument, - instrument_type=instrument_type, + create_and_execute_rfq( + instrument="ETH-PERP", + side="sell", + amount=1.0, ) ) - - 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/examples/rfqs/quote_rfq.py b/examples/rfqs/quote_rfq.py new file mode 100644 index 00000000..1a00cacf --- /dev/null +++ b/examples/rfqs/quote_rfq.py @@ -0,0 +1,106 @@ +""" +Example of how to poll RFQ (Request for Quote) status and handle transfers between subaccount and funding account. +""" + +import asyncio +from typing import List + +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.channel_models import QuoteResultSchema +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) + print(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 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, + ) + quotes = {} + + async def on_rfq(rfqs: List[RFQResultPublicSchema]): + for rfq in rfqs: + if rfq.status == Status.filled and rfq.rfq_id in quotes: + print(f"[blue]Quote {quotes[rfq.rfq_id].quote_id} accepted![/blue]") + del quotes[rfq.rfq_id] + elif rfq.status == Status.expired and rfq.rfq_id in quotes: + print(f"[yellow]Quote {quotes[rfq.rfq_id].quote_id} expired.[/yellow]") + del quotes[rfq.rfq_id] + open_rfqs = [r for r in rfqs if r.status == Status.open] + print(f"Received {len(rfqs)} RFQs ({len(open_rfqs)} open)") + if not open_rfqs: + return + + priced = await asyncio.gather(*(create_priced_legs(client, r) for r in open_rfqs)) + quotable = [(r, legs) for r, legs in zip(open_rfqs, priced) if legs] + + if not quotable: + return + + results = await asyncio.gather( + *(client.rfq.send_quote(rfq_id=r.rfq_id, legs=legs, direction=Direction.sell) for r, legs in quotable), + return_exceptions=True, + ) + for r, result in zip(quotable, results): + rfq, _ = r + if isinstance(result, DeriveJSONRPCError): + print(f"[red]Failed to send quote for RFQ {rfq.rfq_id}: {result}[/red]") + else: + quotes[rfq.rfq_id] = result + print(f"[green]Sent quote for RFQ {rfq.rfq_id}[/green]") + + async def on_quote(quotes_list: List[QuoteResultSchema]): + print(f"Received {len(quotes_list)} quotes") + for quote in quotes_list: + print(f" - Quote {quote.quote_id} for RFQ {quote.rfq_id} is {quote.status}") + + await client.connect() + + await client.private_channels.rfqs_by_wallet(wallet=TEST_WALLET, callback=on_rfq) + await client.private_channels.quotes_by_subaccount_id( + subaccount_id=str(SUBACCOUNT_ID), + callback=on_quote, + ) + + await asyncio.Event().wait() # Keep the connection alive + + +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..3389c408 100644 --- a/specs/websocket-channels.json +++ b/specs/websocket-channels.json @@ -1588,202 +1588,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 +2674,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", From 7d0d35ebf79bd29bb4da91fad7b76ad59001a587 Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 18:44:49 +0000 Subject: [PATCH 02/19] feat:added-rfq-demo --- examples/rfqs/create_rfq.py | 5 +- examples/rfqs/quote_rfq.py | 144 +++++++++++++++++++++--------------- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/examples/rfqs/create_rfq.py b/examples/rfqs/create_rfq.py index bc5cefe8..dfd2bf8f 100644 --- a/examples/rfqs/create_rfq.py +++ b/examples/rfqs/create_rfq.py @@ -83,7 +83,10 @@ def handle_quote(quotes: List[BestQuoteChannelResultSchema]): rfq_id=best_quote.rfq_id, quote_id=best_quote.quote_id, ) - logger.info(f"✓ Quote executed at: {best_quote.quote_id}") + 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__": diff --git a/examples/rfqs/quote_rfq.py b/examples/rfqs/quote_rfq.py index 1a00cacf..90649a5d 100644 --- a/examples/rfqs/quote_rfq.py +++ b/examples/rfqs/quote_rfq.py @@ -3,103 +3,127 @@ """ 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 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.channel_models import QuoteResultSchema -from derive_client.data_types.generated_models import Direction, LegPricedSchema, RFQResultPublicSchema, Status +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 -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, +class SimpleRfqQuoter: + logger: Logger + client: WebSocketClient + quotes: dict[str, PrivateSendQuoteResultSchema] = {} + + def __init__(self, client: WebSocketClient): + self.client = client + self.logger = client._logger + + async def create_priced_legs(self, client: WebSocketClient, rfq): + # 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: + ticker = await client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) + + base_price = ticker.index_price + + price = base_price * D("1.0") if unpriced_leg.direction == Direction.buy else base_price * D("1.0") + + 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)}" ) - priced_legs.append(priced_leg) - print(f" ✓ Priced legs for RFQ {rfq.rfq_id} at total price {sum(leg.price * leg.amount for leg in priced_legs)}") - return priced_legs + 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, - ) - quotes = {} - - async def on_rfq(rfqs: List[RFQResultPublicSchema]): + async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): for rfq in rfqs: - if rfq.status == Status.filled and rfq.rfq_id in quotes: - print(f"[blue]Quote {quotes[rfq.rfq_id].quote_id} accepted![/blue]") - del quotes[rfq.rfq_id] - elif rfq.status == Status.expired and rfq.rfq_id in quotes: - print(f"[yellow]Quote {quotes[rfq.rfq_id].quote_id} expired.[/yellow]") - del quotes[rfq.rfq_id] + if rfq.status in {Status.expired, Status.cancelled} and rfq.rfq_id in self.quotes: + del self.quotes[rfq.rfq_id] open_rfqs = [r for r in rfqs if r.status == Status.open] - print(f"Received {len(rfqs)} RFQs ({len(open_rfqs)} open)") if not open_rfqs: return - priced = await asyncio.gather(*(create_priced_legs(client, r) for r in open_rfqs)) + priced = await asyncio.gather(*(self.create_priced_legs(self.client, r) for r in open_rfqs)) quotable = [(r, legs) for r, legs in zip(open_rfqs, priced) if legs] if not quotable: return results = await asyncio.gather( - *(client.rfq.send_quote(rfq_id=r.rfq_id, legs=legs, direction=Direction.sell) for r, legs in quotable), + *(self.client.rfq.send_quote(rfq_id=r.rfq_id, legs=legs, direction=Direction.sell) for r, legs in quotable), return_exceptions=True, ) for r, result in zip(quotable, results): rfq, _ = r - if isinstance(result, DeriveJSONRPCError): - print(f"[red]Failed to send quote for RFQ {rfq.rfq_id}: {result}[/red]") + if isinstance(result, PrivateSendQuoteResultSchema): + self.quotes[rfq.rfq_id] = result else: - quotes[rfq.rfq_id] = result - print(f"[green]Sent quote for RFQ {rfq.rfq_id}[/green]") + self.logger.info(f" ❌ Failed to send quote for RFQ {rfq.rfq_id}: {result}[/red]") - async def on_quote(quotes_list: List[QuoteResultSchema]): - print(f"Received {len(quotes_list)} quotes") + async def on_quote(self, quotes_list: List[QuoteResultSchema]): for quote in quotes_list: - print(f" - Quote {quote.quote_id} for RFQ {quote.rfq_id} is {quote.status}") + 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. + 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): + await self.client.connect() + 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 asyncio.Event().wait() # Keep the connection alive - await client.connect() - await client.private_channels.rfqs_by_wallet(wallet=TEST_WALLET, callback=on_rfq) - await client.private_channels.quotes_by_subaccount_id( - subaccount_id=str(SUBACCOUNT_ID), - callback=on_quote, - ) +async def main(): + """ + Sample of polling for RFQs and printing their status. + """ - await asyncio.Event().wait() # Keep the connection alive + client = WebSocketClient( + session_key=SESSION_KEY_PRIVATE_KEY, + wallet=TEST_WALLET, + env=Environment.TEST, + subaccount_id=SUBACCOUNT_ID, + ) + rfq_quoter = SimpleRfqQuoter(client) + while True: + try: + await rfq_quoter.run() + except KeyboardInterrupt: + break if __name__ == "__main__": From 9ede2c5609cc504bd6c813f7f5f6e5ca5d8b37dd Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 18:45:38 +0000 Subject: [PATCH 03/19] feat:added-simple-rfq-quoter --- examples/rfqs/{quote_rfq.py => 01_simple_rfq_quoter.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/rfqs/{quote_rfq.py => 01_simple_rfq_quoter.py} (100%) diff --git a/examples/rfqs/quote_rfq.py b/examples/rfqs/01_simple_rfq_quoter.py similarity index 100% rename from examples/rfqs/quote_rfq.py rename to examples/rfqs/01_simple_rfq_quoter.py From b7b624a95fc7c613362704ffcb8b0b95f4feb76c Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 18:56:08 +0000 Subject: [PATCH 04/19] feat:added-simple-rfq-quoter --- examples/rfqs/01_simple_rfq_quoter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index 90649a5d..30bf4d71 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -84,7 +84,7 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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}[/red]") + self.logger.info(f" ❌ Failed to send quote for RFQ {rfq.rfq_id}: {result}") async def on_quote(self, quotes_list: List[QuoteResultSchema]): for quote in quotes_list: From fd6598105ee4b98a876aa491cc93ed3fd461d996 Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 18:57:45 +0000 Subject: [PATCH 05/19] chore:codegen --- derive_client/data_types/channel_models.py | 111 ++++++++------------- specs/websocket-channels.json | 37 +------ 2 files changed, 42 insertions(+), 106 deletions(-) diff --git a/derive_client/data_types/channel_models.py b/derive_client/data_types/channel_models.py index a70e0f6c..212468ee 100644 --- a/derive_client/data_types/channel_models.py +++ b/derive_client/data_types/channel_models.py @@ -23,6 +23,7 @@ PublicGetInstrumentParamsSchema, PublicGetOptionSettlementPricesParamsSchema, PublicMarginWatchResultSchema, + QuoteResultSchema, RFQResultPublicSchema, RPCErrorFormatSchema, Status, @@ -140,10 +141,6 @@ class SubaccountIdQuotesChannelSchema(SubaccountIdBalancesChannelSchema): pass -class LegPricedSchemaModel(LegPricedSchema): - pass - - class SubaccountIdTradesChannelSchema(SubaccountIdBalancesChannelSchema): pass @@ -250,50 +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 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[LegPricedSchemaModel] - 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_hash: Optional[str] = None - tx_status: Optional[TxStatus] = None - - class SubaccountIdTradesTxStatusNotificationParamsSchema(Struct): channel: str data: List[TradeResponseSchema] @@ -353,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): @@ -362,6 +329,11 @@ class SubaccountIdOrdersNotificationParamsSchema(Struct): data: List[OrderResponseSchema] +class SubaccountIdQuotesNotificationParamsSchema(Struct): + channel: str + data: List[QuoteResultSchema] + + class SubaccountIdTradesNotificationParamsSchema(SubaccountIdTradesTxStatusNotificationParamsSchema): pass @@ -431,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): @@ -447,9 +417,14 @@ class SubaccountIdOrdersPubSubSchema(Struct): notification: SubaccountIdOrdersNotificationSchema -class SubaccountIdQuotesNotificationParamsSchema(Struct): - channel: str - data: List[QuoteResultSchema] +class SubaccountIdQuotesNotificationSchema(Struct): + method: str + params: SubaccountIdQuotesNotificationParamsSchema + + +class SubaccountIdQuotesPubSubSchema(Struct): + channel_params: SubaccountIdQuotesChannelSchema + notification: SubaccountIdQuotesNotificationSchema class SubaccountIdTradesNotificationSchema(Struct): @@ -472,19 +447,10 @@ class WalletRfqsNotificationParamsSchema(Struct): data: List[RFQResultPublicSchema] -class SubaccountIdBestQuotesNotificationParamsSchema(Struct): - channel: str - data: List[BestQuoteChannelResultSchema] - - -class SubaccountIdQuotesNotificationSchema(Struct): - method: str - params: SubaccountIdQuotesNotificationParamsSchema - - -class SubaccountIdQuotesPubSubSchema(Struct): - channel_params: SubaccountIdQuotesChannelSchema - notification: SubaccountIdQuotesNotificationSchema +class BestQuoteChannelResultSchema(Struct): + rfq_id: str + error: Optional[RPCErrorFormatSchema] = None + result: Optional[RFQGetBestQuoteResultSchema] = None class TickerSlimInstrumentNameIntervalNotificationSchema(Struct): @@ -507,6 +473,11 @@ class WalletRfqsPubSubSchema(Struct): notification: WalletRfqsNotificationSchema +class SubaccountIdBestQuotesNotificationParamsSchema(Struct): + channel: str + data: List[BestQuoteChannelResultSchema] + + class SubaccountIdBestQuotesNotificationSchema(Struct): method: str params: SubaccountIdBestQuotesNotificationParamsSchema diff --git a/specs/websocket-channels.json b/specs/websocket-channels.json index 3389c408..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", From 28360cfd250dd858ddfdff638fc9cee0daa33bdf Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 19:01:24 +0000 Subject: [PATCH 06/19] feat:ensured-pricing-strategy-makes-a-modicum-off-send --- examples/rfqs/01_simple_rfq_quoter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index 30bf4d71..6351372e 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -1,5 +1,9 @@ """ -Example of how to poll RFQ (Request for Quote) status and handle transfers between subaccount and funding account. +Example of how to act as a simple RFQ quoter that prices incoming RFQs and sends quotes. +This example connects to the WebSocket API, listens for incoming RFQs on a specified wallet, +prices the legs using current market prices, and sends quotes back to the RFQs. +It also listens for quote updates to track the status of the quotes sent. +IT SHOULD NOT BE USED AS A TRADING STRATEGY!!! """ import asyncio @@ -46,7 +50,7 @@ async def create_priced_legs(self, client: WebSocketClient, rfq): base_price = ticker.index_price - price = base_price * D("1.0") if unpriced_leg.direction == Direction.buy else base_price * D("1.0") + price = base_price * D("1.001") if unpriced_leg.direction == Direction.buy else base_price * D("0.999") price = price.quantize(ticker.tick_size) priced_leg = LegPricedSchema( From 0033d8d2c0f5f99e0e9f970b52d0544afe35cda6 Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 20:00:26 +0000 Subject: [PATCH 07/19] feat:ensure-rffq-prices-by-market --- examples/rfqs/01_simple_rfq_quoter.py | 4 +-- examples/rfqs/create_rfq.py | 35 +++++++++++++-------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index 6351372e..d7c23498 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -48,9 +48,9 @@ async def create_priced_legs(self, client: WebSocketClient, rfq): for unpriced_leg in rfq.legs: ticker = await client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) - base_price = ticker.index_price + base_price = ticker.mark_price - price = base_price * D("1.001") if unpriced_leg.direction == Direction.buy else base_price * D("0.999") + price = base_price * D("0.999") if unpriced_leg.direction == Direction.buy else base_price * D("1.001") price = price.quantize(ticker.tick_size) priced_leg = LegPricedSchema( diff --git a/examples/rfqs/create_rfq.py b/examples/rfqs/create_rfq.py index dfd2bf8f..95b57098 100644 --- a/examples/rfqs/create_rfq.py +++ b/examples/rfqs/create_rfq.py @@ -10,15 +10,13 @@ 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 +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( - instrument: str, - side: str, - amount: float, + legs: List[LegUnpricedSchema], ): """ Create an RFQ, wait for quotes, and execute the best one. @@ -39,20 +37,11 @@ async def create_and_execute_rfq( logger = get_logger() # Send RFQ - direction = Direction.buy if side.lower() == "buy" else Direction.sell - rfq_result = await client.rfq.send_rfq( - legs=[ - LegUnpricedSchema( - amount=D(amount), - instrument_name=instrument, - direction=direction, - ) - ], - ) + rfq_result = await client.rfq.send_rfq(legs=legs) logger.info(f"✓ RFQ created: {rfq_result.rfq_id}") # Track best quote - best_quote = None + best_quote: QuoteResultPublicSchema | None = None def handle_quote(quotes: List[BestQuoteChannelResultSchema]): nonlocal best_quote @@ -91,10 +80,20 @@ def handle_quote(quotes: List[BestQuoteChannelResultSchema]): if __name__ == "__main__": # Example usage + legs = [ + LegUnpricedSchema( + instrument_name="ETH-20260125-3050-C", + amount=D("1.0"), + direction=Direction.buy, + ), + LegUnpricedSchema( + instrument_name="ETH-20260125-3050-P", + amount=D("1.0"), + direction=Direction.sell, + ), + ] asyncio.run( create_and_execute_rfq( - instrument="ETH-PERP", - side="sell", - amount=1.0, + legs=legs, ) ) From 5403d8148f20aacb781518206cc0cbf07fb029fd Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Fri, 23 Jan 2026 20:34:24 +0000 Subject: [PATCH 08/19] feat:silly-bug --- examples/rfqs/01_simple_rfq_quoter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index d7c23498..9ad107a3 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -41,12 +41,12 @@ def __init__(self, client: WebSocketClient): self.client = client self.logger = client._logger - async def create_priced_legs(self, client: WebSocketClient, rfq): + async def price_rfq(self, rfq): # 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: - ticker = await client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) + ticker = await self.client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) base_price = ticker.mark_price From ab69a9069250b3d147e21026004db071fb9c398c Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Sat, 24 Jan 2026 12:04:42 +0000 Subject: [PATCH 09/19] ffeat:silly-typo --- examples/rfqs/01_simple_rfq_quoter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index 9ad107a3..0324b228 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -73,7 +73,7 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): if not open_rfqs: return - priced = await asyncio.gather(*(self.create_priced_legs(self.client, r) for r in open_rfqs)) + 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: From 13409154eeb096bb61ef6bdccd691bafaa17902d Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Sat, 24 Jan 2026 12:05:55 +0000 Subject: [PATCH 10/19] feat:delta-hedged-quoter-added --- examples/rfqs/02_delta_hedged_quoter.py | 145 ++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 examples/rfqs/02_delta_hedged_quoter.py diff --git a/examples/rfqs/02_delta_hedged_quoter.py b/examples/rfqs/02_delta_hedged_quoter.py new file mode 100644 index 00000000..af1ead06 --- /dev/null +++ b/examples/rfqs/02_delta_hedged_quoter.py @@ -0,0 +1,145 @@ +""" +Example of how to act as a simple RFQ quoter that prices incoming RFQs and sends quotes. +This example connects to the WebSocket API, listens for incoming RFQs on a specified wallet, +prices the legs using current market prices, and sends quotes back to the RFQs. +It also listens for quote updates to track the status of the quotes sent. +IT SHOULD NOT BE USED AS A TRADING STRATEGY!!! +""" + +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 DeltaHedgerRfqQuoter: + logger: Logger + client: WebSocketClient + quotes: dict[str, PrivateSendQuoteResultSchema] = {} + + def __init__(self, client: WebSocketClient): + self.client = client + self.logger = client._logger + + async def create_priced_legs(self, client: WebSocketClient, rfq): + # 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: + ticker = await client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) + + base_price = ticker.mark_price + + price = base_price * D("0.999") if unpriced_leg.direction == Direction.buy else base_price * D("1.001") + + 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]): + 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] + open_rfqs = [r for r in rfqs if r.status == Status.open] + if not open_rfqs: + return + + priced = await asyncio.gather(*(self.create_priced_legs(self.client, r) for r in open_rfqs)) + quotable = [(r, legs) for r, legs in zip(open_rfqs, priced) if legs] + + if not quotable: + return + + 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, + ) + 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]): + 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. + 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 on_position_update(self, positions): + # Here we could implement delta hedging logic based on position updates. + pass + + async def on_balance_update(self, List[BalanceUpdateSchema]): + # Here we could implement logic based on balance updates. + + async def run(self): + await self.client.connect() + 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.balances_by_subaccount_id( + subaccount_id=str(SUBACCOUNT_ID), + callback=self.on_balance_update, + ) + await asyncio.Event().wait() # Keep the connection alive + + +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, + ) + rfq_quoter = DeltaHedgedRfqQuoter(client) + while True: + try: + await rfq_quoter.run() + except KeyboardInterrupt: + break + + +if __name__ == "__main__": + asyncio.run(main()) From ee1a4820c833076f179e6612825757169b6b1dec Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Sat, 24 Jan 2026 12:06:55 +0000 Subject: [PATCH 11/19] renamed-rfq --- examples/rfqs/{create_rfq.py => 00_create_rfq.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/rfqs/{create_rfq.py => 00_create_rfq.py} (100%) diff --git a/examples/rfqs/create_rfq.py b/examples/rfqs/00_create_rfq.py similarity index 100% rename from examples/rfqs/create_rfq.py rename to examples/rfqs/00_create_rfq.py From 4f417da096b7e37393b1aa5a74100b166a839d00 Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Sat, 24 Jan 2026 15:55:47 +0000 Subject: [PATCH 12/19] feat:first-draft-of-delta-hedger --- examples/rfqs/02_delta_hedged_quoter.py | 247 +++++++++++++++++++++--- 1 file changed, 219 insertions(+), 28 deletions(-) diff --git a/examples/rfqs/02_delta_hedged_quoter.py b/examples/rfqs/02_delta_hedged_quoter.py index af1ead06..dd39e004 100644 --- a/examples/rfqs/02_delta_hedged_quoter.py +++ b/examples/rfqs/02_delta_hedged_quoter.py @@ -1,13 +1,18 @@ """ -Example of how to act as a simple RFQ quoter that prices incoming RFQs and sends quotes. +Example of how to act as a RFQ quoter that prices incoming RFQs and sends quotes. +This is a more advanced example that sets the stage for implementing a delta-hedging strategy. This example connects to the WebSocket API, listens for incoming RFQs on a specified wallet, prices the legs using current market prices, and sends quotes back to the RFQs. -It also listens for quote updates to track the status of the quotes sent. +It performs basic delta-hedging on the accepted quotes by; +- Listening on to quote acceptances, calculating the implied delta from the quoted legs and + performing hedging trades to neutralize the delta exposure. +- It also periodically checks the position and balance updates to ensure the hedging is effective. IT SHOULD NOT BE USED AS A TRADING STRATEGY!!! """ import asyncio import warnings +from decimal import Decimal from logging import Logger from typing import List @@ -18,11 +23,17 @@ 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, + OrderType, + PositionResponseSchema, PrivateSendQuoteResultSchema, + PublicGetTickerResultSchema, RFQResultPublicSchema, Status, + TradeResponseSchema, + TxStatus, ) from derive_client.data_types.utils import D @@ -31,27 +42,87 @@ SLEEP_TIME = 1 SUBACCOUNT_ID = 31049 +UNDERLYING_TO_QUOTE = "ETH" -class DeltaHedgerRfqQuoter: - logger: Logger +# Hedging parameters +MAX_DELTA_TO_QUOTE = D("100.0") +MIN_DELTA_EXPOSURE = D("-0.1") +MAX_DELTA_EXPOSURE = D("0.1") +HEDGE_INTERVAL = 30 # seconds + + +class DeltaQuoterStrategy: + quote_tickers: dict[str, PublicGetTickerResultSchema] = {} client: WebSocketClient - quotes: dict[str, PrivateSendQuoteResultSchema] = {} + logger: Logger - def __init__(self, client: WebSocketClient): + def __init__(self, client: WebSocketClient, logger: Logger): self.client = client - self.logger = client._logger + self.logger = logger - async def create_priced_legs(self, client: WebSocketClient, rfq): - # 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}...") + async def should_quote(self, rfq: RFQResultPublicSchema) -> bool: + # 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: + 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 + + 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}") + + return all( + [ + is_for_target_underlying, + is_only_options, + abs(total_delta) <= MAX_DELTA_TO_QUOTE, + not is_error, + ] + ) + + async def calculate_delta_from_quote( + self, quote: QuoteResultSchema | RFQResultPublicSchema + ) -> tuple[Decimal, bool]: + """Calculates the hedge amount needed to neutralize delta exposure from the quote.""" + total_delta = D("0.0") + is_error = False + for leg in quote.legs: + if leg.instrument_name not in self.quote_tickers: + # fetch and cache ticker + ticker = await self.client.markets.get_ticker(instrument_name=leg.instrument_name) + self.quote_tickers[leg.instrument_name] = ticker + else: + ticker = self.quote_tickers[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 + if leg.direction == Direction.buy: + total_delta += leg_delta + else: + total_delta -= leg_delta + return total_delta, is_error + + async def price_legs(self, rfq: RFQResultPublicSchema) -> List[LegPricedSchema]: + # Implement logic to price the legs of the RFQ priced_legs = [] + expected_delta = D("0.0") # delta that selling the RFQ would add to the portfolio + shouldnt_price = False + expected_delta, is_error = await self.calculate_delta_from_quote(rfq) + if is_error: + return [] for unpriced_leg in rfq.legs: - ticker = await client.markets.get_ticker(instrument_name=unpriced_leg.instrument_name) - + ticker = self.quote_tickers[unpriced_leg.instrument_name] + expected_delta += unpriced_leg.amount * ticker.option_pricing.delta # type: ignore base_price = ticker.mark_price - price = base_price * D("0.999") if unpriced_leg.direction == Direction.buy else base_price * D("1.001") - price = price.quantize(ticker.tick_size) priced_leg = LegPricedSchema( price=price, @@ -60,6 +131,59 @@ async def create_priced_legs(self, client: WebSocketClient, rfq): instrument_name=unpriced_leg.instrument_name, ) priced_legs.append(priced_leg) + return priced_legs if not shouldnt_price else [] + + async def calculate_portfolio_delta(self) -> Decimal: + # Implement logic to calculate the current portfolio delta + positions: List[PositionResponseSchema] = await self.client.positions.list( + is_open=True, currency=UNDERLYING_TO_QUOTE + ) + 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) + ] + 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: + logger: Logger + client: WebSocketClient + quotes: dict[str, PrivateSendQuoteResultSchema] = {} + is_hedging: bool = False + + def __init__(self, client: WebSocketClient): + self.client = client + self.logger = client._logger + + async def create_quote(self, rfq): + # Price legs using current market prices NOTE! This is just an example and not a trading strategy!!! + delta_quoter_strategy = DeltaQuoterStrategy(self.client, self.logger) + if not await 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 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)}" ) @@ -73,7 +197,7 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): if not open_rfqs: return - priced = await asyncio.gather(*(self.create_priced_legs(self.client, r) for r in open_rfqs)) + 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: @@ -93,20 +217,74 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): async def on_quote(self, quotes_list: List[QuoteResultSchema]): 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. 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 on_position_update(self, positions): - # Here we could implement delta hedging logic based on position updates. - pass + async def execute_hedge(self, delta_to_hedge: Decimal): + self.is_hedging = True + instrument_name = f"{UNDERLYING_TO_QUOTE}-PERP" + ticker = await self.client.markets.get_ticker(instrument_name=instrument_name) + trade_direction = Direction.buy if delta_to_hedge < 0 else Direction.sell + price = ( + ticker.mark_price * D("1.01") if trade_direction == Direction.buy else ticker.mark_price * D("0.99") + ) # to make sure we fill + trade_amount = abs(delta_to_hedge) + if trade_amount < ticker.tick_size: + self.logger.info(f" - Hedge amount {trade_amount} is below min order size {ticker.tick_size}, skipping.") + self.is_hedging = False + return + self.logger.info(f" - Executing hedge for delta amount: {delta_to_hedge} in direction {trade_direction}") + 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, + ) + self.is_hedging = False - async def on_balance_update(self, List[BalanceUpdateSchema]): - # Here we could implement logic based on balance updates. + async def on_trade(self, trades: List[TradeResponseSchema], timeout_s=30): + """Handle trade updates if needed for more advanced hedging logic.""" + + trades_to_check = [] + settled_trades = [] + for trade in trades: + self.logger.info( + f" - {trade.direction} executed: market {trade.instrument_name} at price {trade.trade_price} " + + f"amount {trade.trade_amount} status: {trade.tx_status}" + ) + # Wait for pending trades a little while + if trade.tx_status in {TxStatus.pending, TxStatus.requested}: + trades_to_check.append(trade) + elif trade.tx_status == TxStatus.settled: + settled_trades.append(trade) + else: + self.logger.info(f" - Trade {trade.trade_id} has unexpected status {trade.tx_status}, skipping.") + + for trade in trades_to_check: + waited_s = 0 + all_settled = False + while not all_settled and waited_s < timeout_s: + await asyncio.sleep(1) + waited_s += 1 + trade_trades = await self.client.trades.list_private(order_id=trade.order_id) + settled = [] + for trade_part in trade_trades: + if trade_part.tx_status == TxStatus.settled: + settled.append(trade) + if len(settled) == len(trade_trades): + all_settled = True + settled_trades.append(trade) + break + if settled_trades: + delta_quoter_strategy = DeltaQuoterStrategy(self.client, self.logger) + delta_to_hedge = await delta_quoter_strategy.calculate_portfolio_delta() + await self.execute_hedge(delta_to_hedge) async def run(self): await self.client.connect() @@ -115,11 +293,24 @@ async def run(self): subaccount_id=str(SUBACCOUNT_ID), callback=self.on_quote, ) - await self.client.private_channels.balances_by_subaccount_id( + await self.client.private_channels.trades_by_subaccount_id( subaccount_id=str(SUBACCOUNT_ID), - callback=self.on_balance_update, + callback=self.on_trade, ) - await asyncio.Event().wait() # Keep the connection alive + await self.run_portfolio_hedging() + while True: + await asyncio.sleep(HEDGE_INTERVAL) + if not self.is_hedging: + await self.run_portfolio_hedging() + + async def run_portfolio_hedging(self): + # Implement periodic portfolio delta checking and hedging if necessary + delta_hedger_strategy = DeltaQuoterStrategy(self.client, self.logger) + current_delta = await delta_hedger_strategy.calculate_portfolio_delta() + self.logger.info(f" - Current portfolio delta: {current_delta}") + if current_delta < MIN_DELTA_EXPOSURE or current_delta > MAX_DELTA_EXPOSURE: + self.logger.info(" - Portfolio delta out of bounds, executing hedge.") + await self.execute_hedge(current_delta) async def main(): @@ -133,7 +324,7 @@ async def main(): env=Environment.TEST, subaccount_id=SUBACCOUNT_ID, ) - rfq_quoter = DeltaHedgedRfqQuoter(client) + rfq_quoter = DeltaHedgerRfqQuoter(client) while True: try: await rfq_quoter.run() From 28fc3b779fb99e46f0862e053a6ff3b89a67f4ad Mon Sep 17 00:00:00 2001 From: 8ball030 <8baller@station.codes> Date: Sat, 24 Jan 2026 18:48:28 +0000 Subject: [PATCH 13/19] feat:delta-hedging-rfq-added --- examples/rfqs/00_create_rfq.py | 29 ++- examples/rfqs/02_delta_hedged_quoter.py | 238 +++++++++++++++--------- 2 files changed, 171 insertions(+), 96 deletions(-) diff --git a/examples/rfqs/00_create_rfq.py b/examples/rfqs/00_create_rfq.py index 95b57098..f9bcf632 100644 --- a/examples/rfqs/00_create_rfq.py +++ b/examples/rfqs/00_create_rfq.py @@ -82,15 +82,30 @@ def handle_quote(quotes: List[BestQuoteChannelResultSchema]): # Example usage legs = [ LegUnpricedSchema( - instrument_name="ETH-20260125-3050-C", - amount=D("1.0"), - direction=Direction.buy, - ), - LegUnpricedSchema( - instrument_name="ETH-20260125-3050-P", - amount=D("1.0"), + instrument_name="ETH-20260327-4800-P", + amount=D("1"), direction=Direction.sell, ), + # LegUnpricedSchema( + # instrument_name="ETH-20260125-3050-P", + # amount=D("1.0"), + # direction=Direction.sell, + # ), + # LegUnpricedSchema( + # instrument_name="ETH-20260125-2900-P", + # amount=D("1.0"), + # direction=Direction.sell, + # ), + # LegUnpricedSchema( + # instrument_name="ETH-20260125-3000-P", + # amount=D("1.0"), + # direction=Direction.buy, + # ), + # LegUnpricedSchema( + # instrument_name="ETH-20260126-2900-P", + # amount=D("1.0"), + # direction=Direction.buy, + # ), ] asyncio.run( create_and_execute_rfq( diff --git a/examples/rfqs/02_delta_hedged_quoter.py b/examples/rfqs/02_delta_hedged_quoter.py index dd39e004..41df392d 100644 --- a/examples/rfqs/02_delta_hedged_quoter.py +++ b/examples/rfqs/02_delta_hedged_quoter.py @@ -12,6 +12,7 @@ import asyncio import warnings +from datetime import UTC, datetime, timedelta from decimal import Decimal from logging import Logger from typing import List @@ -26,6 +27,7 @@ AssetType, Direction, LegPricedSchema, + OrderResponseSchema, OrderType, PositionResponseSchema, PrivateSendQuoteResultSchema, @@ -33,26 +35,29 @@ RFQResultPublicSchema, Status, TradeResponseSchema, - TxStatus, + TxStatus4, ) from derive_client.data_types.utils import D warnings.filterwarnings("ignore", category=DeprecationWarning) -SLEEP_TIME = 1 SUBACCOUNT_ID = 31049 +# Quoting parameters UNDERLYING_TO_QUOTE = "ETH" +QUOTE_SPREAD_BPS = D("0") +FALLBACK_TO_MARK_PRICE_PREMIUM_BPS = D("1000") # 10% premium if no book price available # Hedging parameters MAX_DELTA_TO_QUOTE = D("100.0") MIN_DELTA_EXPOSURE = D("-0.1") MAX_DELTA_EXPOSURE = D("0.1") HEDGE_INTERVAL = 30 # seconds +HEDGE_ORDER_LABEL = "delta_hedge" +HEDGE_ORDER_TIMEOUT_S = 60 class DeltaQuoterStrategy: - quote_tickers: dict[str, PublicGetTickerResultSchema] = {} client: WebSocketClient logger: Logger @@ -91,12 +96,7 @@ async def calculate_delta_from_quote( total_delta = D("0.0") is_error = False for leg in quote.legs: - if leg.instrument_name not in self.quote_tickers: - # fetch and cache ticker - ticker = await self.client.markets.get_ticker(instrument_name=leg.instrument_name) - self.quote_tickers[leg.instrument_name] = ticker - else: - ticker = self.quote_tickers[leg.instrument_name] + 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." @@ -104,7 +104,9 @@ async def calculate_delta_from_quote( is_error = True break leg_delta = ticker.option_pricing.delta * leg.amount - if leg.direction == Direction.buy: + # we are the SELLER of the quote so we are in efffect 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 @@ -113,17 +115,31 @@ async def calculate_delta_from_quote( async def price_legs(self, rfq: RFQResultPublicSchema) -> List[LegPricedSchema]: # Implement logic to price the legs of the RFQ priced_legs = [] - expected_delta = D("0.0") # delta that selling the RFQ would add to the portfolio - shouldnt_price = False expected_delta, is_error = await self.calculate_delta_from_quote(rfq) - if is_error: + if is_error or abs(expected_delta) > MAX_DELTA_TO_QUOTE: return [] for unpriced_leg in rfq.legs: - ticker = self.quote_tickers[unpriced_leg.instrument_name] - expected_delta += unpriced_leg.amount * ticker.option_pricing.delta # type: ignore - base_price = ticker.mark_price - price = base_price * D("0.999") if unpriced_leg.direction == Direction.buy else base_price * D("1.001") - price = price.quantize(ticker.tick_size) + ticker: PublicGetTickerResultSchema = await self.client.markets.get_ticker( + instrument_name=unpriced_leg.instrument_name + ) + # we base on the book price here + if unpriced_leg.direction == Direction.buy: + if ticker.best_ask_price is None or ticker.best_ask_price == D("0"): + self.logger.info( + f" - fallback pricing used as no mark price for: {unpriced_leg.instrument_name}." + ) + base_price = ticker.mark_price * (D("1") + FALLBACK_TO_MARK_PRICE_PREMIUM_BPS / D("10000")) + else: + base_price = ticker.best_ask_price + else: + if ticker.best_bid_price is None or ticker.best_bid_price == D("0"): + self.logger.info( + f" - fallback pricing used as no mark price for: {unpriced_leg.instrument_name}." + ) + base_price = ticker.mark_price * (D("1") - FALLBACK_TO_MARK_PRICE_PREMIUM_BPS / D("10000")) + else: + base_price = ticker.best_bid_price + price = base_price.quantize(ticker.tick_size) priced_leg = LegPricedSchema( price=price, amount=unpriced_leg.amount, @@ -131,7 +147,16 @@ async def price_legs(self, rfq: RFQResultPublicSchema) -> List[LegPricedSchema]: instrument_name=unpriced_leg.instrument_name, ) priced_legs.append(priced_leg) - return priced_legs if not shouldnt_price else [] + return priced_legs + + +class PortfolioDeltaCalculator: + client: WebSocketClient + logger: Logger + + def __init__(self, client: WebSocketClient, logger: Logger): + self.client = client + self.logger = logger async def calculate_portfolio_delta(self) -> Decimal: # Implement logic to calculate the current portfolio delta @@ -170,20 +195,27 @@ async def calculate_portfolio_delta(self) -> Decimal: class DeltaHedgerRfqQuoter: logger: Logger client: WebSocketClient - quotes: dict[str, PrivateSendQuoteResultSchema] = {} - is_hedging: bool = False 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] = {} + # hedging state + self.hedging_queue = asyncio.Queue() + self.hedger_task: asyncio.Task[None] + self.hedge_order: OrderResponseSchema | None = None + self.hedge_lock = asyncio.Lock() + # state locks + self.quoting_lock = asyncio.Lock() async def create_quote(self, rfq): # Price legs using current market prices NOTE! This is just an example and not a trading strategy!!! - delta_quoter_strategy = DeltaQuoterStrategy(self.client, self.logger) - if not await delta_quoter_strategy.should_quote(rfq): + 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 delta_quoter_strategy.price_legs(rfq) + 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)}" ) @@ -191,8 +223,9 @@ async def create_quote(self, rfq): async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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] + 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] open_rfqs = [r for r in rfqs if r.status == Status.open] if not open_rfqs: return @@ -203,6 +236,7 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): if not quotable: return + # as we are the quoter, we always sell the quotes 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, @@ -210,81 +244,82 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): for r, result in zip(quotable, results): rfq, _ = r if isinstance(result, PrivateSendQuoteResultSchema): - self.quotes[rfq.rfq_id] = result + 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]): for quote in quotes_list: self.logger.info(f" - Quote {quote.quote_id} {quote.rfq_id}: {quote.status}") - 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 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): - self.is_hedging = True instrument_name = f"{UNDERLYING_TO_QUOTE}-PERP" + 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 ticker = await self.client.markets.get_ticker(instrument_name=instrument_name) - trade_direction = Direction.buy if delta_to_hedge < 0 else Direction.sell - price = ( - ticker.mark_price * D("1.01") if trade_direction == Direction.buy else ticker.mark_price * D("0.99") - ) # to make sure we fill + # if we need to hedge negative delta, we need to buy the underlying perp + # if we need to hedge positive delta, we need to sell the underlying perp + 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"): + price = ticker.mark_price trade_amount = abs(delta_to_hedge) - if trade_amount < ticker.tick_size: - self.logger.info(f" - Hedge amount {trade_amount} is below min order size {ticker.tick_size}, skipping.") - self.is_hedging = False + 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}") - await self.client.orders.create( + 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, + reject_timestamp=int((datetime.now(UTC) + timedelta(seconds=HEDGE_ORDER_TIMEOUT_S)).timestamp() * 1000), ) - self.is_hedging = False - async def on_trade(self, trades: List[TradeResponseSchema], timeout_s=30): + async def on_trade_settlement(self, trades: List[TradeResponseSchema]): """Handle trade updates if needed for more advanced hedging logic.""" - trades_to_check = [] - settled_trades = [] + rfq_trades = [] for trade in trades: self.logger.info( - f" - {trade.direction} executed: market {trade.instrument_name} at price {trade.trade_price} " - + f"amount {trade.trade_amount} status: {trade.tx_status}" + f" - {trade.instrument_name}-{trade.direction} {trade.trade_amount} at {trade.trade_price}" ) - # Wait for pending trades a little while - if trade.tx_status in {TxStatus.pending, TxStatus.requested}: - trades_to_check.append(trade) - elif trade.tx_status == TxStatus.settled: - settled_trades.append(trade) - else: - self.logger.info(f" - Trade {trade.trade_id} has unexpected status {trade.tx_status}, skipping.") - - for trade in trades_to_check: - waited_s = 0 - all_settled = False - while not all_settled and waited_s < timeout_s: - await asyncio.sleep(1) - waited_s += 1 - trade_trades = await self.client.trades.list_private(order_id=trade.order_id) - settled = [] - for trade_part in trade_trades: - if trade_part.tx_status == TxStatus.settled: - settled.append(trade) - if len(settled) == len(trade_trades): - all_settled = True - settled_trades.append(trade) - break - if settled_trades: - delta_quoter_strategy = DeltaQuoterStrategy(self.client, self.logger) - delta_to_hedge = await delta_quoter_strategy.calculate_portfolio_delta() - await self.execute_hedge(delta_to_hedge) + 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.") + await self.hedging_queue.put(await self.portfolio_delta_calculator.calculate_portfolio_delta()) + + async def on_order(self, orders: List[OrderResponseSchema]): + """Handle order updates if needed for more advanced hedging logic.""" + for order in orders: + self.logger.info(f" - Order {order.order_id} status update: {order.order_status}") + 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 + await self.hedging_queue.put(await self.portfolio_delta_calculator.calculate_portfolio_delta()) async def run(self): await self.client.connect() @@ -293,24 +328,49 @@ async def run(self): subaccount_id=str(SUBACCOUNT_ID), callback=self.on_quote, ) - await self.client.private_channels.trades_by_subaccount_id( + await self.client.private_channels.trades_tx_status_by_subaccount_id( + subaccount_id=SUBACCOUNT_ID, + callback=self.on_trade_settlement, + tx_status=TxStatus4.settled, + ) + await self.client.private_channels.orders_by_subaccount_id( subaccount_id=str(SUBACCOUNT_ID), - callback=self.on_trade, + callback=self.on_order, ) - await self.run_portfolio_hedging() - while True: - await asyncio.sleep(HEDGE_INTERVAL) - if not self.is_hedging: - await self.run_portfolio_hedging() + self.hedger_task = asyncio.create_task(self.portfolio_hedging_task()) + # we send a request to evaluate the current delta on startup + await self.hedging_queue.put(await self.portfolio_delta_calculator.calculate_portfolio_delta()) + await asyncio.Event().wait() - async def run_portfolio_hedging(self): + async def portfolio_hedging_task(self): # Implement periodic portfolio delta checking and hedging if necessary - delta_hedger_strategy = DeltaQuoterStrategy(self.client, self.logger) - current_delta = await delta_hedger_strategy.calculate_portfolio_delta() - self.logger.info(f" - Current portfolio delta: {current_delta}") - if current_delta < MIN_DELTA_EXPOSURE or current_delta > MAX_DELTA_EXPOSURE: - self.logger.info(" - Portfolio delta out of bounds, executing hedge.") - await self.execute_hedge(current_delta) + + last_check_time = datetime.now(UTC) + + while True: + portfolio_delta: Decimal | None = None + while True: + try: + portfolio_delta = self.hedging_queue.get_nowait() + except asyncio.QueueEmpty: + break + + 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: + await asyncio.sleep(1) + continue + + async with self.hedge_lock: + if portfolio_delta < MIN_DELTA_EXPOSURE or portfolio_delta > MAX_DELTA_EXPOSURE: + 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(): From d8d1a349f5e6da692a602ced4d1c7d5a8c856d8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:26:56 +0000 Subject: [PATCH 14/19] Initial plan From b61069159163a748624eacb949cfd217b4eb1332 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:28:28 +0000 Subject: [PATCH 15/19] fix: remove typos and commented-out code from RFQ examples Co-authored-by: 8ball030 <35799987+8ball030@users.noreply.github.com> --- examples/rfqs/00_create_rfq.py | 20 -------------------- examples/rfqs/01_simple_rfq_quoter.py | 2 +- examples/rfqs/02_delta_hedged_quoter.py | 4 ++-- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/examples/rfqs/00_create_rfq.py b/examples/rfqs/00_create_rfq.py index f9bcf632..8e69a2d1 100644 --- a/examples/rfqs/00_create_rfq.py +++ b/examples/rfqs/00_create_rfq.py @@ -86,26 +86,6 @@ def handle_quote(quotes: List[BestQuoteChannelResultSchema]): amount=D("1"), direction=Direction.sell, ), - # LegUnpricedSchema( - # instrument_name="ETH-20260125-3050-P", - # amount=D("1.0"), - # direction=Direction.sell, - # ), - # LegUnpricedSchema( - # instrument_name="ETH-20260125-2900-P", - # amount=D("1.0"), - # direction=Direction.sell, - # ), - # LegUnpricedSchema( - # instrument_name="ETH-20260125-3000-P", - # amount=D("1.0"), - # direction=Direction.buy, - # ), - # LegUnpricedSchema( - # instrument_name="ETH-20260126-2900-P", - # amount=D("1.0"), - # direction=Direction.buy, - # ), ] asyncio.run( create_and_execute_rfq( diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index 0324b228..b21360bd 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -99,7 +99,7 @@ async def on_quote(self, quotes_list: List[QuoteResultSchema]): # Here we could proceed to perform some type of hedging or other action based on the filled quote. 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!") + self.logger.info(f" ✗ Our quote {quote.quote_id} expired. Better luck next time!") async def run(self): await self.client.connect() diff --git a/examples/rfqs/02_delta_hedged_quoter.py b/examples/rfqs/02_delta_hedged_quoter.py index 41df392d..316c6e82 100644 --- a/examples/rfqs/02_delta_hedged_quoter.py +++ b/examples/rfqs/02_delta_hedged_quoter.py @@ -104,7 +104,7 @@ async def calculate_delta_from_quote( is_error = True break leg_delta = ticker.option_pricing.delta * leg.amount - # we are the SELLER of the quote so we are in efffect taking the opposite side of the leg direction + # 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 @@ -255,7 +255,7 @@ async def on_quote(self, quotes_list: List[QuoteResultSchema]): 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!") + 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!") From 22d1966f3a7359d9441c8d5a25865971129ed102 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:41:21 +0000 Subject: [PATCH 16/19] Initial plan From 8f99c471c44665c61086ac66d5bd284b12bfdeaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:50:46 +0000 Subject: [PATCH 17/19] docs: add comprehensive comments to RFQ example files Co-authored-by: 8ball030 <35799987+8ball030@users.noreply.github.com> --- examples/rfqs/00_create_rfq.py | 48 ++-- examples/rfqs/01_simple_rfq_quoter.py | 104 ++++++++- examples/rfqs/02_delta_hedged_quoter.py | 294 ++++++++++++++++++++---- 3 files changed, 384 insertions(+), 62 deletions(-) diff --git a/examples/rfqs/00_create_rfq.py b/examples/rfqs/00_create_rfq.py index 8e69a2d1..9aa24821 100644 --- a/examples/rfqs/00_create_rfq.py +++ b/examples/rfqs/00_create_rfq.py @@ -22,11 +22,17 @@ async def create_and_execute_rfq( Create an RFQ, wait for quotes, and execute the best one. Args: - instrument: Instrument name (e.g. "ETH-30JUN23-1500-C") - side: "buy" or "sell" - amount: Contract amount + 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 client + # 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, @@ -36,35 +42,46 @@ async def create_and_execute_rfq( await client.connect() logger = get_logger() - # Send RFQ + # 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 best quote + # 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 quotes + # 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 quotes + # 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 quote + # 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, @@ -72,6 +89,7 @@ def handle_quote(quotes: List[BestQuoteChannelResultSchema]): 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)}" @@ -79,12 +97,16 @@ def handle_quote(quotes: List[BestQuoteChannelResultSchema]): if __name__ == "__main__": - # Example usage + # 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", - amount=D("1"), - direction=Direction.sell, + 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( diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index b21360bd..23be0783 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -1,9 +1,19 @@ """ Example of how to act as a simple RFQ quoter that prices incoming RFQs and sends quotes. -This example connects to the WebSocket API, listens for incoming RFQs on a specified wallet, -prices the legs using current market prices, and sends quotes back to the RFQs. -It also listens for quote updates to track the status of the quotes sent. + +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 @@ -33,25 +43,56 @@ 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] = {} + 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, @@ -66,23 +107,43 @@ async def price_rfq(self, rfq): 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): @@ -91,29 +152,59 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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 + await asyncio.Event().wait() # Keep the connection alive indefinitely async def main(): """ - Sample of polling for RFQs and printing their status. + 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( @@ -123,6 +214,7 @@ async def main(): subaccount_id=SUBACCOUNT_ID, ) rfq_quoter = SimpleRfqQuoter(client) + # Run indefinitely with automatic reconnection on failure while True: try: await rfq_quoter.run() diff --git a/examples/rfqs/02_delta_hedged_quoter.py b/examples/rfqs/02_delta_hedged_quoter.py index 316c6e82..b5426b03 100644 --- a/examples/rfqs/02_delta_hedged_quoter.py +++ b/examples/rfqs/02_delta_hedged_quoter.py @@ -1,13 +1,27 @@ """ -Example of how to act as a RFQ quoter that prices incoming RFQs and sends quotes. -This is a more advanced example that sets the stage for implementing a delta-hedging strategy. -This example connects to the WebSocket API, listens for incoming RFQs on a specified wallet, -prices the legs using current market prices, and sends quotes back to the RFQs. -It performs basic delta-hedging on the accepted quotes by; -- Listening on to quote acceptances, calculating the implied delta from the quoted legs and - performing hedging trades to neutralize the delta exposure. -- It also periodically checks the position and balance updates to ensure the hedging is effective. -IT SHOULD NOT BE USED AS A TRADING STRATEGY!!! +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 @@ -44,20 +58,28 @@ SUBACCOUNT_ID = 31049 # Quoting parameters -UNDERLYING_TO_QUOTE = "ETH" -QUOTE_SPREAD_BPS = D("0") -FALLBACK_TO_MARK_PRICE_PREMIUM_BPS = D("1000") # 10% premium if no book price available +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 -MAX_DELTA_TO_QUOTE = D("100.0") -MIN_DELTA_EXPOSURE = D("-0.1") -MAX_DELTA_EXPOSURE = D("0.1") -HEDGE_INTERVAL = 30 # seconds -HEDGE_ORDER_LABEL = "delta_hedge" -HEDGE_ORDER_TIMEOUT_S = 60 +# 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 @@ -66,33 +88,62 @@ def __init__(self, client: WebSocketClient, logger: Logger): 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, - not is_error, + 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]: - """Calculates the hedge amount needed to neutralize delta exposure from the quote.""" + """ + 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: @@ -113,32 +164,53 @@ async def calculate_delta_from_quote( 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 on the book price here + # 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 mark price for: {unpriced_leg.instrument_name}." + 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 mark price for: {unpriced_leg.instrument_name}." + 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, @@ -151,6 +223,19 @@ async def price_legs(self, rfq: RFQResultPublicSchema) -> List[LegPricedSchema]: 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 @@ -159,10 +244,18 @@ def __init__(self, client: WebSocketClient, logger: Logger): 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 @@ -185,6 +278,7 @@ async def calculate_portfolio_delta(self) -> Decimal: 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 @@ -193,6 +287,21 @@ async def calculate_portfolio_delta(self) -> Decimal: 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 @@ -201,16 +310,22 @@ def __init__(self, client: WebSocketClient): 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] = {} - # hedging state - self.hedging_queue = asyncio.Queue() - self.hedger_task: asyncio.Task[None] - self.hedge_order: OrderResponseSchema | None = None - self.hedge_lock = asyncio.Lock() - # state locks + 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.") @@ -222,25 +337,35 @@ async def create_quote(self, rfq): 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 - # as we are the quoter, we always sell the quotes + # 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): @@ -250,6 +375,12 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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: @@ -261,26 +392,48 @@ async def on_quote(self, quotes_list: List[QuoteResultSchema]): 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) - # if we need to hedge negative delta, we need to buy the underlying perp - # if we need to hedge positive delta, we need to sell the underlying perp + + # 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, @@ -288,28 +441,43 @@ async def execute_hedge(self, delta_to_hedge: Decimal): direction=trade_direction, order_type=OrderType.limit, reduce_only=False, - label=HEDGE_ORDER_LABEL, + 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 updates if needed for more advanced hedging logic.""" + """ + 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 updates if needed for more advanced hedging logic.""" + """ + 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, @@ -318,11 +486,21 @@ async def on_order(self, orders: List[OrderResponseSchema]): self.logger.info( f" ✓ Hedge order {order.order_id} status {order.order_status}, re-evaluating total delta." ) - self.hedge_order = None + 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), @@ -331,23 +509,36 @@ async def run(self): await self.client.private_channels.trades_tx_status_by_subaccount_id( subaccount_id=SUBACCOUNT_ID, callback=self.on_trade_settlement, - tx_status=TxStatus4.settled, + 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()) - # we send a request to evaluate the current delta on startup + # Calculate initial portfolio delta on startup await self.hedging_queue.put(await self.portfolio_delta_calculator.calculate_portfolio_delta()) - await asyncio.Event().wait() + 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: @@ -355,17 +546,21 @@ async def portfolio_hedging_task(self): 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}." @@ -375,7 +570,19 @@ async def portfolio_hedging_task(self): async def main(): """ - Sample of polling for RFQs and printing their status. + 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( @@ -385,6 +592,7 @@ async def main(): subaccount_id=SUBACCOUNT_ID, ) rfq_quoter = DeltaHedgerRfqQuoter(client) + # Run indefinitely with automatic reconnection on failure while True: try: await rfq_quoter.run() From 5ce22afb87f51068c061f9603802c01752fd8a49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 05:14:25 +0000 Subject: [PATCH 18/19] Initial plan From 0756f155df992626c076751e93e3df8828f16b82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 05:16:28 +0000 Subject: [PATCH 19/19] chore: run make fmt lint to format examples Co-authored-by: 8ball030 <35799987+8ball030@users.noreply.github.com> --- examples/rfqs/00_create_rfq.py | 2 +- examples/rfqs/01_simple_rfq_quoter.py | 23 ++++----- examples/rfqs/02_delta_hedged_quoter.py | 65 +++++++++++++------------ 3 files changed, 47 insertions(+), 43 deletions(-) diff --git a/examples/rfqs/00_create_rfq.py b/examples/rfqs/00_create_rfq.py index 9aa24821..c520807e 100644 --- a/examples/rfqs/00_create_rfq.py +++ b/examples/rfqs/00_create_rfq.py @@ -24,7 +24,7 @@ async def create_and_execute_rfq( 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 diff --git a/examples/rfqs/01_simple_rfq_quoter.py b/examples/rfqs/01_simple_rfq_quoter.py index 23be0783..b56abe76 100644 --- a/examples/rfqs/01_simple_rfq_quoter.py +++ b/examples/rfqs/01_simple_rfq_quoter.py @@ -45,13 +45,14 @@ 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 @@ -63,18 +64,18 @@ def __init__(self, client: WebSocketClient): 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 """ @@ -109,11 +110,11 @@ async def price_rfq(self, rfq): 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 @@ -124,7 +125,7 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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: @@ -154,12 +155,12 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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 @@ -179,7 +180,7 @@ async def on_quote(self, quotes_list: List[QuoteResultSchema]): 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 @@ -199,7 +200,7 @@ async def run(self): 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 diff --git a/examples/rfqs/02_delta_hedged_quoter.py b/examples/rfqs/02_delta_hedged_quoter.py index b5426b03..67474188 100644 --- a/examples/rfqs/02_delta_hedged_quoter.py +++ b/examples/rfqs/02_delta_hedged_quoter.py @@ -74,12 +74,13 @@ 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 @@ -90,13 +91,13 @@ def __init__(self, client: WebSocketClient, 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 """ @@ -131,14 +132,14 @@ async def calculate_delta_from_quote( ) -> 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 @@ -166,14 +167,14 @@ async def calculate_delta_from_quote( 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 """ @@ -183,7 +184,7 @@ async def price_legs(self, rfq: RFQResultPublicSchema) -> List[LegPricedSchema]: 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 @@ -225,17 +226,18 @@ async def price_legs(self, rfq: RFQResultPublicSchema) -> List[LegPricedSchema]: 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 @@ -246,7 +248,7 @@ def __init__(self, client: WebSocketClient, 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) """ @@ -289,19 +291,20 @@ async def calculate_portfolio_delta(self) -> Decimal: 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 @@ -322,7 +325,7 @@ def __init__(self, client: WebSocketClient): 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 """ @@ -339,7 +342,7 @@ async def create_quote(self, rfq): 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 @@ -347,7 +350,7 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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: @@ -377,7 +380,7 @@ async def on_rfq(self, rfqs: List[RFQResultPublicSchema]): 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. """ @@ -394,13 +397,13 @@ async def on_quote(self, quotes_list: List[QuoteResultSchema]): 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) """ @@ -411,10 +414,10 @@ async def execute_hedge(self, delta_to_hedge: Decimal): 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) @@ -423,7 +426,7 @@ async def execute_hedge(self, delta_to_hedge: Decimal): 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: @@ -431,7 +434,7 @@ async def execute_hedge(self, delta_to_hedge: Decimal): 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( @@ -448,7 +451,7 @@ async def execute_hedge(self, delta_to_hedge: Decimal): 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. """ @@ -461,7 +464,7 @@ async def on_trade_settlement(self, trades: List[TradeResponseSchema]): # 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 @@ -470,7 +473,7 @@ async def on_trade_settlement(self, trades: List[TradeResponseSchema]): 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 @@ -493,7 +496,7 @@ async def on_order(self, orders: List[OrderResponseSchema]): 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 @@ -524,12 +527,12 @@ async def run(self): 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. """ @@ -571,13 +574,13 @@ async def portfolio_hedging_task(self): 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