Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
333a5ea
tests: test_poll_quotes
Karrenbelt Sep 12, 2025
eba41d2
feat: poll_quotes
Karrenbelt Sep 12, 2025
3eeb4ee
tests: test_execute_quote
Karrenbelt Sep 12, 2025
7b45161
feat: execute_quote
Karrenbelt Sep 12, 2025
e91bd7a
fix: set max_fee to zero for RFQ
Karrenbelt Sep 13, 2025
8d2621f
feat: rfq_trading_flow example scaffold
Karrenbelt Sep 15, 2025
974011e
chore: Leg model
Karrenbelt Sep 15, 2025
abc8a45
feat: rfq_max_fee draft
Karrenbelt Sep 15, 2025
10597dd
feat: _is_box_spread
Karrenbelt Sep 15, 2025
7c9fa25
feat: _classify_leg
Karrenbelt Sep 15, 2025
95d0504
feat: examples/rfq_trading_flow draft
Karrenbelt Sep 15, 2025
684807d
chore: make fmt lint
Karrenbelt Sep 18, 2025
813a2d3
feat:rfq-stuff
Oct 1, 2025
4133dd4
feat:websocket-examples
Oct 1, 2025
1919584
Merge pull request #94 from 8ball030/fix-ci
8ball030 Oct 1, 2025
7e9934d
fixes:ci
Oct 1, 2025
55deff6
Merge pull request #97 from 8ball030/fix-ci
8ball030 Oct 1, 2025
05a02bb
chore:use-web3.py-webosckets
Oct 1, 2025
c3cf833
chore:linters
Oct 1, 2025
a990866
feat:improving-examples
Oct 1, 2025
4388b41
Merge branch 'feat/websocket-client' into syncing
8ball030 Oct 1, 2025
0336ea8
Merge pull request #100 from 8ball030/syncing
8ball030 Oct 1, 2025
f02a613
chore:re-locked
Oct 1, 2025
bf39095
feat:added-key-gen-script
Oct 1, 2025
8c95d96
feat:codegen-improvements
Oct 1, 2025
111de05
feat:codegen-improvements
Oct 1, 2025
0c82d50
Merge branch 'feat/rfq-implementations' into feat/websocket-client
8ball030 Oct 1, 2025
852890b
Merge pull request #102 from 8ball030/feat/websocket-client
8ball030 Oct 1, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
python-versions:
- 3.10
- 3.11
os:
- ubuntu-24.04
runs-on: ubuntu-24.04
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ tests:
poetry run pytest tests -vv --reruns 3 --reruns-delay 3

fmt:
poetry run ruff format tests derive_client examples
poetry run ruff check tests derive_client examples --fix
poetry run ruff format tests derive_client examples scripts
poetry run ruff check tests derive_client examples scripts --fix

lint:
poetry run ruff check tests derive_client examples
poetry run ruff check tests derive_client examples scripts

all: fmt lint tests

Expand Down
2 changes: 1 addition & 1 deletion derive_client/_bridge/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ async def _check_bridge_funds(self, token_data, connector: Address, amount: int)
if token_data.isNewBridge:
deposit_hook = await controller.functions.hook__().call()
expected_hook = token_data.LyraTSAShareHandlerDepositHook
if not deposit_hook == token_data.LyraTSAShareHandlerDepositHook:
if deposit_hook != token_data.LyraTSAShareHandlerDepositHook:
msg = f"Controller deposit hook {deposit_hook} does not match expected address {expected_hook}"
raise ValueError(msg)
deposit_contract = _load_deposit_contract(w3=self.derive_w3, token_data=token_data)
Expand Down
5 changes: 1 addition & 4 deletions derive_client/_bridge/w3.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,7 @@ async def estimate_fees(w3, blocks: int = 20) -> FeeEstimates:
for percentile in percentiles:
rewards = percentile_rewards[percentile]
non_zero_rewards = list(filter(lambda x: x, rewards))
if non_zero_rewards:
estimated_priority_fee = int(statistics.median(non_zero_rewards))
else:
estimated_priority_fee = MIN_PRIORITY_FEE
estimated_priority_fee = int(statistics.median(non_zero_rewards)) if non_zero_rewards else MIN_PRIORITY_FEE

buffered_base_fee = int(latest_base_fee * GAS_FEE_BUFFER)
estimated_max_fee = buffered_base_fee + estimated_priority_fee
Expand Down
16 changes: 16 additions & 0 deletions derive_client/analyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ def print_positions(self, underlying_currency: str, columns: Optional[List[str]]
if columns:
df = df[[c for c in columns if c not in DELTA_COLUMNS] + DELTA_COLUMNS]
print(df)

def calculate_greeks_of_option(
self,
underlying_price: float,
strike_price: float,
interest_rate: float,
days_to_expiration: int,
volatility: float,
) -> dict:
"""
Calculate the greeks of each option position using the Black-Scholes model.
# BS([underlyingPrice, strikePrice, interestRate, daysToExpiration], volatility=x, callPrice=y, putPrice=z)

# eg:
# c = mibian.BS([1.4565, 1.45, 1, 30], volatility=20)
"""
144 changes: 83 additions & 61 deletions derive_client/clients/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
from decimal import Decimal
from logging import Logger, LoggerAdapter
from time import sleep
from typing import Any

import eth_abi
import requests
from derive_action_signing.module_data import (
DepositModuleData,
MakerTransferPositionModuleData,
MakerTransferPositionsModuleData,
RecipientTransferERC20ModuleData,
RFQExecuteModuleData,
RFQQuoteDetails,
RFQQuoteModuleData,
SenderTransferERC20ModuleData,
Expand Down Expand Up @@ -61,7 +62,7 @@
)
from derive_client.endpoints import RestAPI
from derive_client.exceptions import DeriveJSONRPCException
from derive_client.utils import get_logger, wait_until
from derive_client.utils import get_logger, rfq_max_fee, wait_until


def _is_final_tx(res: DeriveTxResult) -> bool:
Expand Down Expand Up @@ -197,9 +198,9 @@ def _internal_map_instrument(self, instrument_type, currency):

def create_order(
self,
price,
amount: int,
instrument_name: str,
price: float = None,
reduce_only=False,
instrument_type: InstrumentType = InstrumentType.PERP,
side: OrderSide = OrderSide.BUY,
Expand Down Expand Up @@ -228,21 +229,23 @@ def create_order(
amount_step = instrument["amount_step"]
rounded_amount = Decimal(str(amount)).quantize(Decimal(str(amount_step)))

price_step = instrument["tick_size"]
rounded_price = Decimal(str(price)).quantize(Decimal(str(price_step)))
if price is not None:
price_step = instrument["tick_size"]
rounded_price = Decimal(str(price)).quantize(Decimal(str(price_step)))

module_data = {
"asset_address": instrument["base_asset_address"],
"sub_id": int(instrument["base_asset_sub_id"]),
"limit_price": Decimal(str(rounded_price)),
"limit_price": Decimal(str(rounded_price)) if price is not None else Decimal(0),
"amount": Decimal(str(rounded_amount)),
"max_fee": Decimal(1000),
"recipient_id": int(self.subaccount_id),
"is_bid": side == OrderSide.BUY,
}

signed_action = self._generate_signed_action(
module_address=self.config.contracts.TRADE_MODULE, module_data=module_data
module_address=self.config.contracts.TRADE_MODULE,
module_data=module_data,
)

order = {
Expand Down Expand Up @@ -286,49 +289,6 @@ def submit_order(self, order):
url = self.endpoints.private.order
return self._send_request(url, json=order)["order"]

def _sign_quote(self, quote):
"""
Sign the quote
"""
rfq_module_data = self._encode_quote_data(quote)
return self._sign_quote_data(quote, rfq_module_data)

def _encode_quote_data(self, quote, underlying_currency: UnderlyingCurrency = UnderlyingCurrency.ETH):
"""
Convert the quote to encoded data.
"""
instruments = self.fetch_instruments(instrument_type=InstrumentType.OPTION, currency=underlying_currency)
ledgs_to_subids = {i["instrument_name"]: i["base_asset_sub_id"] for i in instruments}
dir_sign = 1 if quote["direction"] == "buy" else -1
quote["price"] = "10"

def encode_leg(leg):
sub_id = ledgs_to_subids[leg["instrument_name"]]
leg_sign = 1 if leg["direction"] == "buy" else -1
signed_amount = self.web3_client.to_wei(leg["amount"], "ether") * leg_sign * dir_sign
return [
self.config.contracts[f"{underlying_currency.name}_OPTION"],
sub_id,
self.web3_client.to_wei(quote["price"], "ether"),
signed_amount,
]

self.logger.info(f"Quote: {quote}")
encoded_legs = [encode_leg(leg) for leg in quote["legs"]]
rfq_data = [self.web3_client.to_wei(quote["max_fee"], "ether"), encoded_legs]

encoded_data = eth_abi.encode(
# ['uint256(address,uint256,uint256,int256)[]'],
[
"uint256",
"address",
"uint256",
"int256",
],
[rfq_data],
)
return self.web3_client.keccak(encoded_data)

def fetch_ticker(self, instrument_name):
"""
Fetch the ticker for a given instrument name.
Expand Down Expand Up @@ -407,11 +367,10 @@ def cancel_all(self):
return self._send_request(url, json=payload)

def _check_output_for_rate_limit(self, message):
if error := message.get("error"):
if "Rate limit exceeded" in error["message"]:
sleep((int(error["data"].split(" ")[-2]) / 1000))
self.logger.info("Rate limit exceeded, sleeping and retrying request")
return True
if (error := message.get("error")) and "Rate limit exceeded" in error["message"]:
sleep((int(error["data"].split(" ")[-2]) / 1000))
self.logger.info("Rate limit exceeded, sleeping and retrying request")
return True
return False

def get_positions(self):
Expand Down Expand Up @@ -587,9 +546,13 @@ def set_mmp_config(
def send_rfq(self, rfq):
"""Send an RFQ."""
url = self.endpoints.private.send_rfq
return self._send_request(url, rfq)
payload = {
**rfq,
"subaccount_id": self.subaccount_id,
}
return self._send_request(url, payload)

def poll_rfqs(self):
def poll_rfqs(self, rfq_status: RfqStatus | None = None):
"""
Poll RFQs.
type RfqResponse = {
Expand All @@ -606,8 +569,9 @@ def poll_rfqs(self):
url = self.endpoints.private.poll_rfqs
params = {
"subaccount_id": self.subaccount_id,
"status": RfqStatus.OPEN.value,
}
if rfq_status:
params["status"] = rfq_status.value
return self._send_request(
url,
json=params,
Expand All @@ -623,10 +587,14 @@ def create_quote(
rfq_id,
legs,
direction,
max_fee=None,
):
"""Create a quote object."""
_, nonce, expiration = self.get_nonce_and_signature_expiry()

if max_fee is None:
max_fee = rfq_max_fee(client=self, legs=legs, is_taker=True)

rfq_legs: list[RFQQuoteDetails] = []
for leg in legs:
ticker = self.fetch_ticker(instrument_name=leg["instrument_name"])
Expand All @@ -635,7 +603,7 @@ def create_quote(
direction=leg["direction"],
asset_address=ticker["base_asset_address"],
sub_id=int(ticker["base_asset_sub_id"]),
price=leg["price"],
price=Decimal(leg["price"]),
amount=Decimal(leg["amount"]),
)
rfq_legs.append(rfq_quote_details)
Expand All @@ -649,7 +617,7 @@ def create_quote(
module_address=self.config.contracts.RFQ_MODULE,
module_data=RFQQuoteModuleData(
global_direction=direction,
max_fee=Decimal("123"),
max_fee=Decimal(max_fee),
legs=rfq_legs,
),
DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR,
Expand All @@ -667,6 +635,59 @@ def create_quote(

return self.send_quote(quote=payload)

def poll_quotes(self, **kwargs):
url = self.endpoints.private.poll_quotes
payload = {
"subaccount_id": self.subaccount_id,
**kwargs,
}
return self._send_request(url, json=payload)

def execute_quote(self, quote):

_, nonce, expiration = self.get_nonce_and_signature_expiry()

quote_legs: list[RFQQuoteDetails] = []
for leg in quote["legs"]:
ticker = self.fetch_ticker(instrument_name=leg["instrument_name"])
rfq_quote_details = RFQQuoteDetails(
instrument_name=ticker["instrument_name"],
direction=leg["direction"],
asset_address=ticker["base_asset_address"],
sub_id=int(ticker["base_asset_sub_id"]),
price=Decimal(leg["price"]),
amount=Decimal(leg["amount"]),
)
quote_legs.append(rfq_quote_details)

direction = "buy" if quote["direction"] == "sell" else "sell"
module_data = RFQExecuteModuleData(
global_direction=direction,
max_fee=Decimal("0"),
legs=quote_legs,
)

action = SignedAction(
subaccount_id=self.subaccount_id,
owner=self.wallet,
signer=self.signer.address,
signature_expiry_sec=MAX_INT_32,
nonce=nonce,
module_address=self.config.contracts.RFQ_MODULE,
module_data=request,
DOMAIN_SEPARATOR=self.config.DOMAIN_SEPARATOR,
ACTION_TYPEHASH=self.config.ACTION_TYPEHASH,
)
action.sign(self.signer.key)
payload = {
**action.to_json(),
"label": "",
"rfq_id": rfq_id,
"quote_id": quote_id,
}
url = self.endpoints.private.execute_quote
return self._send_request(url, json=payload)

def cancel_rfq(self, rfq_id: str):
"""Cancel an RFQ."""
url = self.endpoints.private.cancel_rfq
Expand Down Expand Up @@ -701,10 +722,11 @@ def poll_quotes(self, rfq_id: str = None, quote_id: str = None, status: RfqStatu
payload["quote_id"] = quote_id
if status:
payload["status"] = status.value

return self._send_request(url, json=payload)

def _send_request(self, url, json=None, params=None, headers=None):
headers = self._create_signature_headers() if not headers else headers
headers = headers if headers else self._create_signature_headers()
response = requests.post(url, json=json, headers=headers, params=params)
response.raise_for_status()
json_data = response.json()
Expand Down
Loading
Loading