Skip to content
Open
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
15 changes: 7 additions & 8 deletions .github/workflows/common_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ jobs:
installer-parallel: true

- name: Install Packages
run: |
poetry install -v
run: poetry install -v

- name: Format
run: poetry run make fmt

- name: Lint
run: |
poetry run make lint
run: poetry run make lint

- name: Tests
run: |
poetry run make tests
run: poetry run make tests

- name: Doc Tests
run: |
make test-docs
run: poetry run make test-docs
1 change: 1 addition & 0 deletions derive_client/clients/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def __init__(
self.logger = logger or get_logger()
self.web3_client = Web3(Web3.HTTPProvider(self.config.rpc_endpoint))
self.signer = self.web3_client.eth.account.from_key(private_key)
print(self.signer.address)
self.wallet = wallet
self._verify_wallet(wallet)
self.subaccount_ids = self.fetch_subaccounts().get("subaccount_ids", [])
Expand Down
151 changes: 24 additions & 127 deletions derive_client/clients/ws_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Direction,
OrderResponseSchema,
PrivateGetOrdersResultSchema,
PrivateGetPositionsResultSchema,
PrivateOrderParamsSchema,
PublicGetTickerResultSchema,
TradeResponseSchema,
Expand Down Expand Up @@ -51,107 +52,6 @@ def from_json(cls, data):
)


@dataclass
class Position:
"""
{'instrument_type': 'perp', 'instrument_name': 'ETH-PERP', 'amount': '0', 'average_price': '0', 'realized_pnl': '0',
'unrealized_pnl': '0', 'total_fees': '0', 'average_price_excl_fees': '0', 'realized_pnl_excl_fees': '0', 'unrealized_pnl_excl_fees': '0',
'net_settlements': '0', 'cumulative_funding': '0', 'pending_funding': '0', 'mark_price': '4153.1395224770267304847948253154754638671875',
'index_price': '4156.522924638571', 'delta': '1', 'gamma': '0', 'vega': '0', 'theta': '0', 'mark_value': '0', 'maintenance_margin': '0',
'initial_margin': '0', 'open_orders_margin': '-81.7268423838896751476568169891834259033203125', 'leverage': None, 'liquidation_price': None,
'creation_timestamp': 0, 'amount_step': '0'}
"""

instrument_type: str | None = None
instrument_name: str | None = None
amount: float | None = None
average_price: float | None = None
realized_pnl: float | None = None
unrealized_pnl: float | None = None
total_fees: float | None = None
average_price_excl_fees: float | None = None
realized_pnl_excl_fees: float | None = None
unrealized_pnl_excl_fees: float | None = None
net_settlements: float | None = None
cumulative_funding: float | None = None
pending_funding: float | None = None
mark_price: float | None = None
index_price: float | None = None
delta: float | None = None
gamma: float | None = None
vega: float | None = None
theta: float | None = None
mark_value: float | None = None
maintenance_margin: float | None = None
initial_margin: float | None = None
open_orders_margin: float | None = None
leverage: float | None = None
liquidation_price: float | None = None
creation_timestamp: int | None = None
amount_step: float | None = None

@classmethod
def from_json(cls, data):
return cls(
instrument_type=data.get("instrument_type"),
instrument_name=data.get("instrument_name"),
amount=float(data.get("amount", 0)) if data.get("amount") is not None else None,
average_price=float(data.get("average_price", 0)) if data.get("average_price") is not None else None,
realized_pnl=float(data.get("realized_pnl", 0)) if data.get("realized_pnl") is not None else None,
unrealized_pnl=float(data.get("unrealized_pnl", 0)) if data.get("unrealized_pnl") is not None else None,
total_fees=float(data.get("total_fees", 0)) if data.get("total_fees") is not None else None,
average_price_excl_fees=float(data.get("average_price_excl_fees", 0))
if data.get("average_price_excl_fees") is not None
else None,
realized_pnl_excl_fees=float(data.get("realized_pnl_excl_fees", 0))
if data.get("realized_pnl_excl_fees") is not None
else None,
unrealized_pnl_excl_fees=float(data.get("unrealized_pnl_excl_fees", 0))
if data.get("unrealized_pnl_excl_fees") is not None
else None,
net_settlements=float(data.get("net_settlements", 0)) if data.get("net_settlements") is not None else None,
cumulative_funding=float(data.get("cumulative_funding", 0))
if data.get("cumulative_funding") is not None
else None,
pending_funding=float(data.get("pending_funding", 0)) if data.get("pending_funding") is not None else None,
mark_price=float(data.get("mark_price", 0)) if data.get("mark_price") is not None else None,
index_price=float(data.get("index_price", 0)) if data.get("index_price") is not None else None,
delta=float(data.get("delta", 0)) if data.get("delta") is not None else None,
gamma=float(data.get("gamma", 0)) if data.get("gamma") is not None else None,
vega=float(data.get("vega", 0)) if data.get("vega") is not None else None,
theta=float(data.get("theta", 0)) if data.get("theta") is not None else None,
mark_value=float(data.get("mark_value", 0)) if data.get("mark_value") is not None else None,
maintenance_margin=float(data.get("maintenance_margin", 0))
if data.get("maintenance_margin") is not None
else None,
initial_margin=float(data.get("initial_margin", 0)) if data.get("initial_margin") is not None else None,
open_orders_margin=float(data.get("open_orders_margin", 0))
if data.get("open_orders_margin") is not None
else None,
leverage=float(data.get("leverage")) if data.get("leverage") is not None else None,
liquidation_price=float(data.get("liquidation_price"))
if data.get("liquidation_price") is not None
else None,
creation_timestamp=int(data.get("creation_timestamp", 0))
if data.get("creation_timestamp") is not None
else None,
amount_step=float(data.get("amount_step", 0)) if data.get("amount_step") is not None else None,
)


@dataclass
class Positions:
positions: list[Position]
subaccount_id: str

@classmethod
def from_json(cls, data):
return cls(
positions=[Position.from_json(pos) for pos in data],
subaccount_id=data["subaccount_id"],
)


class Depth(StrEnum):
DEPTH_1 = "1"
DEPTH_10 = "10"
Expand Down Expand Up @@ -319,13 +219,31 @@ def subscribe_ticker(self, instrument_name, interval: Interval = Interval.ONE_HU
"""
msg = f"ticker.{instrument_name}.{interval}"
self.ws.send(json.dumps({"method": "subscribe", "params": {"channels": [msg]}, "id": str(utc_now_ms())}))
self.subsriptions[msg] = self._parse_ticker_stream
self.subsriptions[msg] = self._parse_ticker

def _parse_ticker_stream(self, json_message):
def get_positions(self):
"""
Get positions
"""
id = str(uuid.uuid4())
payload = {"subaccount_id": self.subaccount_id}
self.ws.send(json.dumps({"method": "private/get_positions", "params": payload, "id": id}))
self.requests_in_flight[id] = self._parse_positions_response

def get_orders(self):
"""
Get orders
"""
id = str(uuid.uuid4())
payload = {"subaccount_id": self.subaccount_id}
self.ws.send(json.dumps({"method": "private/get_open_orders", "params": payload, "id": id}))
self.requests_in_flight[id] = self._parse_orders_message

def _parse_ticker(self, json_message):
"""
Parse a ticker message.
"""
return PublicGetTickerResultSchema(**json_message["params"]["data"])
return msgspec.convert(json_message["params"]["data"]['instrument_ticker'], PublicGetTickerResultSchema)

def _parse_orderbook_message(self, json_message):
"""
Expand All @@ -337,7 +255,7 @@ def _parse_trades_message(self, json_message):
"""
Parse a trades message.
"""
return TradeResponseSchema.from_json(json_message["params"]["data"])
return msgspec.convert(json_message["params"]["data"], TradeResponseSchema)

def _parse_order_message(self, json_message):
"""
Expand Down Expand Up @@ -365,32 +283,11 @@ def _parse_orders_message(self, json_message):
"""
return msgspec.convert(json_message['result'], PrivateGetOrdersResultSchema)

def get_positions(self):
"""
Get positions
"""
id = str(uuid.uuid4())
payload = {"subaccount_id": self.subaccount_id}
self.ws.send(json.dumps({"method": "private/get_positions", "params": payload, "id": id}))
self.requests_in_flight[id] = self._parse_positions_response

def get_orders(self):
"""
Get orders
"""
id = str(uuid.uuid4())
payload = {"subaccount_id": self.subaccount_id}
self.ws.send(json.dumps({"method": "private/get_open_orders", "params": payload, "id": id}))
self.requests_in_flight[id] = self._parse_orders_message

def _parse_positions_response(self, json_message):
"""
Parse a positions response message.
"""
return Positions(
[Position.from_json(pos) for pos in json_message["result"]["positions"]],
subaccount_id=json_message["result"]["subaccount_id"],
)
return msgspec.convert(json_message['result'], PrivateGetPositionsResultSchema)

def parse_message(self, raw_message):
"""
Expand Down
100 changes: 75 additions & 25 deletions examples/websockets/websocket_quoter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
"""

import os
import traceback

from dotenv import load_dotenv
from websockets import ConnectionClosedError

from derive_client.clients.ws_client import (
Orderbook,
OrderResponseSchema,
Position,
Positions,
PrivateGetOrdersResultSchema,
TradeResponseSchema,
WsClient,
)
from derive_client.data.generated.models import Direction, OrderStatus
from derive_client.data.generated.models import (
Direction,
OrderStatus,
PositionResponseSchema,
PrivateGetPositionsResponseSchema,
PrivateGetPositionsResultSchema,
)
from derive_client.data_types import Environment
from derive_client.data_types.enums import OrderSide, OrderType
from derive_client.data_types.enums import InstrumentType, OrderSide, OrderType

MARKET_1 = "ETH-PERP"
MAX_POSTION_SIZE = 0.5
Expand All @@ -30,8 +35,8 @@
class WebsocketQuoterStrategy:
def __init__(self, ws_client: WsClient):
self.ws_client = ws_client
self.current_positions: Positions | None = None
self.current_position: Position | None = None
self.current_positions: PrivateGetPositionsResponseSchema | None = None
self.current_position: PositionResponseSchema | None | bool = False
self.orders = {
Direction.buy: {},
Direction.sell: {},
Expand All @@ -45,9 +50,11 @@ def on_orderbook_update(self, orderbook: Orderbook):
if not orderbook.bids or not orderbook.asks:
return

if not self.current_position:
if self.current_position is None:
return

print(orderbook)

bid_price = orderbook.bids[0][0] * BUY_OFFSET
ask_price = orderbook.asks[0][0] * SELL_OFFSET

Expand All @@ -68,6 +75,9 @@ def on_orderbook_update(self, orderbook: Orderbook):
):
self.create_order(Direction.sell, ask_price, QUOTE_SIZE)

def update_order_price(self, old_order_nonce: str, new_price: float):
pass

def create_order(self, side: OrderSide, price: float, amount: float) -> OrderResponseSchema:
order = self.ws_client.create_order(
instrument_name=MARKET_1,
Expand All @@ -80,16 +90,50 @@ def create_order(self, side: OrderSide, price: float, amount: float) -> OrderRes
print(f"{side.value} order placed: {order.nonce} at {price} for {amount}")
return order

def on_position_update(self, positions: Positions):
def on_position_update(self, positions: PrivateGetPositionsResultSchema):
self.current_positions = positions
if not positions.positions:
self.current_position = Position(instrument_name=MARKET_1, amount=0)
self.current_position = self.get_empty_position()
print("No current position")

else:
_matches = [p for p in positions.positions if p.instrument_name == MARKET_1]
self.current_position = _matches[0] if _matches else Position(instrument_name=MARKET_1, amount=0)

pos = self.current_position
print(f"Current position: {pos.instrument_name} {pos.amount} @ {pos.average_price}")
if _matches:
self.current_position = _matches[0]
pos = self.current_position
print(f"Current position: {pos.instrument_name} {pos.amount} @ {pos.average_price}")
else:
self.current_position = self.get_empty_position()
print("No current position")

def get_empty_position(self) -> PositionResponseSchema:
return PositionResponseSchema(
amount=0,
amount_step=0,
average_price=0,
average_price_excl_fees=0,
creation_timestamp=0,
cumulative_funding=0,
delta=0,
gamma=0,
index_price=0,
initial_margin=0,
instrument_name=MARKET_1,
instrument_type=InstrumentType.PERP,
maintenance_margin=0,
mark_price=0,
mark_value=0,
net_settlements=0,
open_orders_margin=0,
pending_funding=0,
realized_pnl=0,
realized_pnl_excl_fees=0,
theta=0,
total_fees=0,
unrealized_pnl=0,
unrealized_pnl_excl_fees=0,
vega=0,
)

def on_order(self, order: OrderResponseSchema):
print(f"Order update: {order.nonce} {order.order_status} {order.direction} {order.limit_price} {order.amount}")
Expand Down Expand Up @@ -132,23 +176,28 @@ def run_loop(self):
print("Connection closed, exiting...")
self.setup_session()
parsed_message = self.ws_client.parse_message(raw_message)
if isinstance(parsed_message, TradeResponseSchema):
self.on_trade(parsed_message)
elif isinstance(parsed_message, Positions):
self.on_position_update(parsed_message)
elif isinstance(parsed_message, PrivateGetOrdersResultSchema):
self.on_orders_update(parsed_message)
elif isinstance(parsed_message, OrderResponseSchema):
self.on_order(parsed_message)
elif isinstance(parsed_message, Orderbook):
self.on_orderbook_update(parsed_message)
else:
print(f"Received unhandled message: {parsed_message}")
try:
if isinstance(parsed_message, TradeResponseSchema):
self.on_trade(parsed_message)
elif isinstance(parsed_message, PrivateGetPositionsResultSchema):
print("Position update received")
self.on_position_update(parsed_message)
elif isinstance(parsed_message, PrivateGetOrdersResultSchema):
self.on_orders_update(parsed_message)
elif isinstance(parsed_message, OrderResponseSchema):
self.on_order(parsed_message)
elif isinstance(parsed_message, Orderbook):
self.on_orderbook_update(parsed_message)
else:
print(f"Received unhandled message: {parsed_message}")
except Exception:
print(f"Error processing message {parsed_message}: {traceback.format_exc()}")

def setup_session(self):
self.ws_client.connect_ws()
self.ws_client.login_client()
# get state data
print("Fetching initial state...")
self.ws_client.get_orders()
self.ws_client.get_positions()
# subscribe to updates
Expand Down Expand Up @@ -178,6 +227,7 @@ def create_client_from_env() -> WsClient:

if __name__ == "__main__":
ws_client = create_client_from_env()
print(ws_client.signer.address)
quoter = WebsocketQuoterStrategy(ws_client)
try:
quoter.run_loop()
Expand Down
Loading