Skip to content
Merged
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
154 changes: 58 additions & 96 deletions derive_client/data_types/channel_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
PublicGetInstrumentParamsSchema,
PublicGetOptionSettlementPricesParamsSchema,
PublicMarginWatchResultSchema,
QuoteResultSchema,
RFQResultPublicSchema,
RPCErrorFormatSchema,
Status,
TickerSlimSchema,
Expand Down Expand Up @@ -139,32 +141,6 @@ class SubaccountIdQuotesChannelSchema(SubaccountIdBalancesChannelSchema):
pass


class QuoteResultSchema(Struct):
cancel_reason: CancelReason
creation_timestamp: int
direction: Direction
fee: Decimal
fill_pct: Decimal
is_transfer: bool
label: str
last_update_timestamp: int
legs: List[LegPricedSchema]
legs_hash: str
liquidity_role: LiquidityRole
max_fee: Decimal
mmp: bool
nonce: int
quote_id: str
rfq_id: str
signature: str
signature_expiry_sec: int
signer: str
status: Status
subaccount_id: int
tx_status: TxStatus
tx_hash: Optional[str] = None


class SubaccountIdTradesChannelSchema(SubaccountIdBalancesChannelSchema):
pass

Expand Down Expand Up @@ -241,6 +217,10 @@ class WalletRfqsChannelSchema(PrivateGetAllPortfoliosParamsSchema):
pass


class LegUnpricedSchema1(LegUnpricedSchema):
pass


class AuctionResultSchema(Struct):
state: State
subaccount_id: int
Expand All @@ -267,29 +247,6 @@ class SubaccountIdBalancesNotificationParamsSchema(Struct):
data: List[BalanceUpdateSchema]


class QuoteResultPublicSchema(Struct):
cancel_reason: CancelReason
creation_timestamp: int
direction: Direction
fill_pct: Decimal
last_update_timestamp: int
legs: List[LegPricedSchema]
legs_hash: str
liquidity_role: LiquidityRole
quote_id: str
rfq_id: str
status: Status
subaccount_id: int
tx_status: TxStatus
wallet: str
tx_hash: Optional[str] = None


class SubaccountIdQuotesNotificationParamsSchema(Struct):
channel: str
data: List[QuoteResultSchema]


class SubaccountIdTradesTxStatusNotificationParamsSchema(Struct):
channel: str
data: List[TradeResponseSchema]
Expand All @@ -314,24 +271,6 @@ class TradesInstrumentTypeCurrencyTxStatusNotificationParamsSchema(Struct):
data: List[TradeSettledPublicResponseSchema]


class RFQResultPublicSchema(Struct):
cancel_reason: CancelReason
creation_timestamp: int
filled_direction: Direction
filled_pct: Decimal
last_update_timestamp: int
legs: List[LegUnpricedSchema]
partial_fill_step: Decimal
rfq_id: str
status: Status
subaccount_id: int
valid_until: int
wallet: str
fill_rate: Optional[Decimal] = None
recent_fill_rate: Optional[Decimal] = None
total_cost: Optional[Decimal] = None


class AuctionsWatchNotificationParamsSchema(Struct):
channel: str
data: List[AuctionResultSchema]
Expand Down Expand Up @@ -367,23 +306,32 @@ 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):
channel: str
data: List[OrderResponseSchema]


class SubaccountIdQuotesNotificationSchema(Struct):
method: str
params: SubaccountIdQuotesNotificationParamsSchema


class SubaccountIdQuotesPubSubSchema(Struct):
channel_params: SubaccountIdQuotesChannelSchema
notification: SubaccountIdQuotesNotificationSchema
class SubaccountIdQuotesNotificationParamsSchema(Struct):
channel: str
data: List[QuoteResultSchema]


class SubaccountIdTradesNotificationParamsSchema(SubaccountIdTradesTxStatusNotificationParamsSchema):
Expand Down Expand Up @@ -425,11 +373,6 @@ class TradesInstrumentTypeCurrencyTxStatusPubSubSchema(Struct):
notification: TradesInstrumentTypeCurrencyTxStatusNotificationSchema


class WalletRfqsNotificationParamsSchema(Struct):
channel: str
data: List[RFQResultPublicSchema]


class AuctionsWatchNotificationSchema(Struct):
method: str
params: AuctionsWatchNotificationParamsSchema
Expand Down Expand Up @@ -460,10 +403,8 @@ class SpotFeedCurrencyPubSubSchema(Struct):
notification: SpotFeedCurrencyNotificationSchema


class BestQuoteChannelResultSchema(Struct):
rfq_id: str
error: Optional[RPCErrorFormatSchema] = None
result: Optional[RFQGetBestQuoteResultSchema] = None
class RFQGetBestQuoteResultSchema(PrivateRfqGetBestQuoteResultSchema):
pass


class SubaccountIdOrdersNotificationSchema(Struct):
Expand All @@ -476,6 +417,16 @@ class SubaccountIdOrdersPubSubSchema(Struct):
notification: SubaccountIdOrdersNotificationSchema


class SubaccountIdQuotesNotificationSchema(Struct):
method: str
params: SubaccountIdQuotesNotificationParamsSchema


class SubaccountIdQuotesPubSubSchema(Struct):
channel_params: SubaccountIdQuotesChannelSchema
notification: SubaccountIdQuotesNotificationSchema


class SubaccountIdTradesNotificationSchema(Struct):
method: str
params: SubaccountIdTradesNotificationParamsSchema
Expand All @@ -491,6 +442,27 @@ class TickerSlimInstrumentNameIntervalNotificationParamsSchema(Struct):
data: TickerSlimInstrumentNameIntervalPublisherDataSchema


class WalletRfqsNotificationParamsSchema(Struct):
channel: str
data: List[RFQResultPublicSchema]


class BestQuoteChannelResultSchema(Struct):
rfq_id: str
error: Optional[RPCErrorFormatSchema] = None
result: Optional[RFQGetBestQuoteResultSchema] = None


class TickerSlimInstrumentNameIntervalNotificationSchema(Struct):
method: str
params: TickerSlimInstrumentNameIntervalNotificationParamsSchema


class TickerSlimInstrumentNameIntervalPubSubSchema(Struct):
channel_params: TickerSlimInstrumentNameIntervalChannelSchema
notification: TickerSlimInstrumentNameIntervalNotificationSchema


class WalletRfqsNotificationSchema(Struct):
method: str
params: WalletRfqsNotificationParamsSchema
Expand All @@ -506,16 +478,6 @@ class SubaccountIdBestQuotesNotificationParamsSchema(Struct):
data: List[BestQuoteChannelResultSchema]


class TickerSlimInstrumentNameIntervalNotificationSchema(Struct):
method: str
params: TickerSlimInstrumentNameIntervalNotificationParamsSchema


class TickerSlimInstrumentNameIntervalPubSubSchema(Struct):
channel_params: TickerSlimInstrumentNameIntervalChannelSchema
notification: TickerSlimInstrumentNameIntervalNotificationSchema


class SubaccountIdBestQuotesNotificationSchema(Struct):
method: str
params: SubaccountIdBestQuotesNotificationParamsSchema
Expand Down
116 changes: 116 additions & 0 deletions examples/rfqs/00_create_rfq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
Simple demonstration of creating and executing an RFQ.
"""

import asyncio
from typing import List

from config import OWNER_TEST_WALLET, SESSION_KEY_PRIVATE_KEY, TAKER_SUBACCOUNT_ID

from derive_client import WebSocketClient
from derive_client.data_types import Environment
from derive_client.data_types.channel_models import BestQuoteChannelResultSchema
from derive_client.data_types.generated_models import Direction, LegUnpricedSchema, QuoteResultPublicSchema
from derive_client.data_types.utils import D
from derive_client.utils.logger import get_logger


async def create_and_execute_rfq(
legs: List[LegUnpricedSchema],
):
"""
Create an RFQ, wait for quotes, and execute the best one.

Args:
legs: List of unpriced legs representing the instruments and amounts to trade.
Each leg specifies the instrument name, amount, and direction (buy/sell).

Flow:
1. Initialize WebSocket client and connect to the exchange
2. Send RFQ (Request for Quote) with the specified legs
3. Subscribe to the best quotes channel to receive quote updates
4. Wait for market makers to respond with their quotes
5. Execute the best received quote if available
"""
# Initialize the WebSocket client with authentication credentials and connect to the test environment
client = WebSocketClient(
session_key=SESSION_KEY_PRIVATE_KEY,
wallet=OWNER_TEST_WALLET,
env=Environment.TEST,
subaccount_id=TAKER_SUBACCOUNT_ID,
)
await client.connect()
logger = get_logger()

# Send the RFQ to the exchange - this broadcasts the request to all market makers
# who can then respond with their quotes
rfq_result = await client.rfq.send_rfq(legs=legs)
logger.info(f"✓ RFQ created: {rfq_result.rfq_id}")

# Track the best quote received from market makers
# This will be updated as better quotes arrive
best_quote: QuoteResultPublicSchema | None = None

def handle_quote(quotes: List[BestQuoteChannelResultSchema]):
"""
Callback function that processes incoming quote updates.
Each time a better quote arrives, we update our tracked best quote.
"""
nonlocal best_quote
for quote in quotes:
if quote.result and quote.result.best_quote:
best_quote = quote.result.best_quote
# Calculate the total price across all legs for logging
total_price = sum(leg.price * leg.amount for leg in best_quote.legs)
logger.info(f"✓ Best quote received: {total_price}")

# Subscribe to the best quotes channel for this subaccount
# This allows us to receive real-time updates as market makers send quotes
await client.private_channels.best_quotes_by_subaccount_id(
subaccount_id=str(TAKER_SUBACCOUNT_ID),
callback=handle_quote,
)

# Wait for market makers to respond with their quotes
# In a production system, you might want to wait until the RFQ expires or use a different timing strategy
await asyncio.sleep(10)

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

# Execute the best quote we received
# Note: We must take the opposite direction of the quote
# If the market maker is buying (quote.direction == buy), we must sell to them
execute_direction = Direction.sell if best_quote.direction == Direction.buy else Direction.buy
await client.rfq.execute_quote(
direction=execute_direction,
legs=best_quote.legs,
rfq_id=best_quote.rfq_id,
quote_id=best_quote.quote_id,
)
# Log the successful execution with the final price
logger.info(
f"✓ Quote {best_quote.quote_id} executed at total price: "
+ f"{sum(leg.price * leg.amount for leg in best_quote.legs)}"
)


if __name__ == "__main__":
# Example usage: Create an RFQ to sell 1 ETH put option
# This will:
# 1. Send the RFQ to market makers
# 2. Wait for quotes to come in
# 3. Execute the best quote automatically
legs = [
LegUnpricedSchema(
instrument_name="ETH-20260327-4800-P", # ETH put option expiring March 27, 2026 with strike 4800
amount=D("1"), # Trade 1 contract
direction=Direction.sell, # We want to sell this option
),
]
asyncio.run(
create_and_execute_rfq(
legs=legs,
)
)
Loading