Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions derive_client/data_types/channel_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ class BalanceUpdateSchema(Struct):


class SubaccountIdBestQuotesChannelSchema(SubaccountIdBalancesChannelSchema):
"""
WebSocket channel schema for subscribing to best quote updates for a subaccount.

This channel allows takers to receive real-time updates on the best available
quotes for their RFQs. The "best" quote is determined by price competitiveness.
"""
pass


Expand All @@ -138,6 +144,12 @@ class SubaccountIdOrdersChannelSchema(SubaccountIdBalancesChannelSchema):


class SubaccountIdQuotesChannelSchema(SubaccountIdBalancesChannelSchema):
"""
WebSocket channel schema for subscribing to quote updates for a subaccount.

This channel is used by market makers to receive updates on quotes they've submitted,
including status changes (filled, expired, cancelled) and fill percentages.
"""
pass


Expand Down Expand Up @@ -214,6 +226,12 @@ class TradeSettledPublicResponseSchema(Struct):


class WalletRfqsChannelSchema(PrivateGetAllPortfoliosParamsSchema):
"""
WebSocket channel schema for subscribing to RFQ updates for a specific wallet.

Market makers use this channel to receive incoming RFQs directed to their wallet.
When a taker creates an RFQ, it's broadcast to wallets that can provide quotes.
"""
pass


Expand Down Expand Up @@ -307,6 +325,30 @@ class SubaccountIdBalancesPubSubSchema(Struct):


class QuoteResultPublicSchema(Struct):
"""
Public schema for RFQ quote results.

This represents a quote that a market maker has submitted for an RFQ.
It contains the basic public information about the quote without sensitive
details like signatures or fee calculations.

Attributes:
quote_id: Unique identifier for this quote
rfq_id: Reference to the RFQ this quote is responding to
direction: The direction of the quote (buy or sell from market maker's perspective)
legs: List of priced legs with specific prices for each instrument
status: Current status (open, filled, expired, cancelled)
creation_timestamp: When the quote was created (milliseconds since epoch)
last_update_timestamp: When the quote was last updated (milliseconds since epoch)
wallet: The market maker's wallet address
subaccount_id: The market maker's subaccount ID
fill_pct: Percentage of the quote that has been filled (0-100)
legs_hash: Hash of the legs for verification
liquidity_role: Role in the trade (maker or taker)
cancel_reason: Reason for cancellation if applicable
tx_status: Blockchain transaction status
tx_hash: Transaction hash if executed on-chain
"""
cancel_reason: CancelReason
creation_timestamp: int
direction: Direction
Expand Down Expand Up @@ -404,6 +446,12 @@ class SpotFeedCurrencyPubSubSchema(Struct):


class RFQGetBestQuoteResultSchema(PrivateRfqGetBestQuoteResultSchema):
"""
Result schema for getting the best quote for an RFQ.

This wraps the API response that contains the most competitive quote
currently available for a given RFQ.
"""
pass


Expand Down Expand Up @@ -443,11 +491,28 @@ class TickerSlimInstrumentNameIntervalNotificationParamsSchema(Struct):


class WalletRfqsNotificationParamsSchema(Struct):
"""
Parameters for RFQ notifications sent to a wallet.

Contains the list of RFQ updates that the subscribed wallet should be aware of.
"""
channel: str
data: List[RFQResultPublicSchema]


class BestQuoteChannelResultSchema(Struct):
"""
Result schema for best quote channel updates.

Each message on the best quotes channel contains either:
- A successful result with the best quote details
- An error if the best quote couldn't be determined

Attributes:
rfq_id: The RFQ this best quote update relates to
result: The best quote result (if successful)
error: Error details (if failed)
"""
rfq_id: str
error: Optional[RPCErrorFormatSchema] = None
result: Optional[RFQGetBestQuoteResultSchema] = None
Expand All @@ -474,15 +539,28 @@ class WalletRfqsPubSubSchema(Struct):


class SubaccountIdBestQuotesNotificationParamsSchema(Struct):
"""
Parameters for best quote notifications for a subaccount.

Contains updates on the best quotes available for RFQs created by this subaccount.
Takers subscribe to this to track which quotes are most competitive.
"""
channel: str
data: List[BestQuoteChannelResultSchema]


class SubaccountIdBestQuotesNotificationSchema(Struct):
"""Notification wrapper for best quote updates."""
method: str
params: SubaccountIdBestQuotesNotificationParamsSchema


class SubaccountIdBestQuotesPubSubSchema(Struct):
"""
Complete pub/sub schema for best quotes channel.

This is the top-level schema for the best quotes WebSocket channel,
combining channel subscription parameters with notification structure.
"""
channel_params: SubaccountIdBestQuotesChannelSchema
notification: SubaccountIdBestQuotesNotificationSchema
67 changes: 47 additions & 20 deletions examples/rfqs/00_create_rfq.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,78 @@ async def create_and_execute_rfq(
"""
Create an RFQ, wait for quotes, and execute the best one.

This function demonstrates the complete RFQ (Request for Quote) lifecycle from the taker's perspective:
1. Connects to the WebSocket API
2. Sends an RFQ with unpriced legs (instruments and amounts without prices)
3. Subscribes to quote updates and tracks the best available quote
4. Executes the best quote after waiting for market makers to respond

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 instrument_name, amount, and direction (buy/sell)
"""
# Initialize client
# Initialize the WebSocket client with authentication credentials
# The client will be used to communicate with the Derive API
client = WebSocketClient(
session_key=SESSION_KEY_PRIVATE_KEY,
wallet=OWNER_TEST_WALLET,
env=Environment.TEST,
subaccount_id=TAKER_SUBACCOUNT_ID,
session_key=SESSION_KEY_PRIVATE_KEY, # Private key for signing requests
wallet=OWNER_TEST_WALLET, # Ethereum wallet address
env=Environment.TEST, # Use test environment
subaccount_id=TAKER_SUBACCOUNT_ID, # Subaccount to execute trades under
)
await client.connect()
logger = get_logger()

# Send RFQ
# Send the RFQ to the network
# This broadcasts the request to all market makers subscribed to the wallet
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 via the WebSocket channel
best_quote: QuoteResultPublicSchema | None = None

def handle_quote(quotes: List[BestQuoteChannelResultSchema]):
"""
Callback function that handles incoming quote updates.

This is called whenever a new quote is received or an existing quote is updated.
It extracts the best quote from the update and calculates its total price.
"""
nonlocal best_quote
for quote in quotes:
if quote.result and quote.result.best_quote:
# Update our tracking variable with the latest best quote
best_quote = quote.result.best_quote
# Calculate total price by summing price * amount for all legs
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 establishes a WebSocket subscription that will call handle_quote
# whenever market makers submit quotes for our RFQ
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 quotes
# In a production system, you might want to wait for a specific number of quotes
# or use a more sophisticated decision mechanism
await asyncio.sleep(10)

if not best_quote:
logger.error("✗ No quotes received")
return

# Execute quote
# Execute the best quote we received
# Note: We take the opposite direction from the quote's direction
# If the quote is buying from us (Direction.buy), we sell to them (Direction.sell)
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,
legs=best_quote.legs, # Use the priced legs from the quote
rfq_id=best_quote.rfq_id, # Reference to our original RFQ
quote_id=best_quote.quote_id, # Specific quote we're accepting
)
logger.info(
f"✓ Quote {best_quote.quote_id} executed at total price: "
Expand All @@ -79,13 +101,18 @@ def handle_quote(quotes: List[BestQuoteChannelResultSchema]):


if __name__ == "__main__":
# Example usage
# Example usage: Create a simple RFQ for a single ETH put option
# In a real scenario, you might include multiple legs for complex strategies
# like spreads, straddles, or butterflies
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, strike 4800
amount=D("1"), # Trade 1 contract
direction=Direction.sell, # We want to sell this option
),
# Additional legs commented out - these show how to create multi-leg strategies:
# - Multiple puts at different strikes (e.g., put spreads)
# - Combination of buys and sells (e.g., iron condors)
# LegUnpricedSchema(
# instrument_name="ETH-20260125-3050-P",
# amount=D("1.0"),
Expand Down
Loading