Skip to content

Commit

Permalink
v1.8.0 (#79)
Browse files Browse the repository at this point in the history
* Release v1.8.0

* Update requirements.txt

* Update requirements.txt
  • Loading branch information
davidMkCb authored Nov 13, 2024
1 parent 81b0e1c commit 2250dbd
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 16 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [1.8.0] - 2024-NOV-12

### Added
- Custom response type object for WebSocket channels

## [1.7.0] - 2024-OCT-16

### Added
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,35 @@ The functions described above handle the asynchronous nature of WebSocket connec

We similarly provide async channel specific methods for subscribing and unsubscribing such as `ticker_async`, `ticker_unsubscribe_async`, etc.

### WebSocket Response Types
For your convenience, we have provided a custom, built-in WebSocket response type object to help interact with our WebSocket feeds more easily.

Assume we simply want the price feed for BTC-USD and ETH-USD.
Like we did in previous steps, we subscribe to the `ticker` channel and include 'BTC-USD' and 'ETH-USD' in the `product_ids` list.
As the data comes through, it is passed into the `on_message` function. From there, we use it to build the `WebsocketResponse` object.

Using said object, we can now extract only the desired parts. In our case, we retrieve and print only the `product_id` and `price` fields, resulting in a cleaner feed.
```python
def on_message(msg):
ws_object = WebsocketResponse(json.loads(msg))
if ws_object.channel == "ticker" :
for event in ws_object.events:
for ticker in event.tickers:
print(ticker.product_id + ": " + ticker.price)

client.open()
client.subscribe(product_ids=["BTC-USD", "ETH-USD"], channels=["ticker"])
time.sleep(10)
client.unsubscribe(product_ids=["BTC-USD", "ETH-USD"], channels=["ticker"])
client.close()
```
#### Avoiding errors
In the example, note how we first checked `if ws_object.channel == "ticker"`.
Since each channel's event field has a unique structure and set of fields, it's important to ensure that the fields we access are actually present in the object.
For example, if we were to subscribe to the `user` channel and try to access a field that does not exist in it, such as the `tickers` field, we would be met with an error.

Therefore, we urge users to reference our [documentation](https://docs.cdp.coinbase.com/advanced-trade/docs/ws-channels), which outlines the JSON object that each channel will return.

___
## Debugging the Clients
You can enable debug logging for the REST and WebSocket clients by setting the `verbose` variable to `True` when initializing the clients. This will log useful information throughout the lifecycle of the REST request or WebSocket connection, and is highly recommended for debugging purposes.
Expand Down
2 changes: 1 addition & 1 deletion coinbase/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.7.0"
__version__ = "1.8.0"
6 changes: 2 additions & 4 deletions coinbase/rest/types/base_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@

class BaseResponse:
def __init__(self, **kwargs):
for field in list(kwargs.keys()):
attr_name = field.replace("-", "_")

for field, formattedField in common_fields.items():
if field in kwargs:
setattr(self, attr_name, kwargs.pop(field))
setattr(self, formattedField, kwargs.pop(field))

for key in list(kwargs.keys()):
setattr(self, key, kwargs.pop(key))
Expand Down
13 changes: 7 additions & 6 deletions coinbase/rest/types/common_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,17 +382,18 @@ def __init__(self, **kwargs):
**kwargs.pop("limit_limit_fok")
)
if "stop_limit_stop_limit_gtc" in kwargs:
self.stop_limit_stop_limit_gtc: Optional[
StopLimitStopLimitGtc
] = StopLimitStopLimitGtc(**kwargs.pop("stop_limit_stop_limit_gtc"))
self.stop_limit_stop_limit_gtc: Optional[StopLimitStopLimitGtc] = (
StopLimitStopLimitGtc(**kwargs.pop("stop_limit_stop_limit_gtc"))
)
if "stop_limit_stop_limit_gtd" in kwargs:
self.stop_limit_stop_limit_gtd: Optional[
StopLimitStopLimitGtd
] = StopLimitStopLimitGtd(**kwargs.pop("stop_limit_stop_limit_gtd"))
self.stop_limit_stop_limit_gtd: Optional[StopLimitStopLimitGtd] = (
StopLimitStopLimitGtd(**kwargs.pop("stop_limit_stop_limit_gtd"))
)
if "trigger_bracket_gtc" in kwargs:
self.trigger_bracket_gtc: Optional[TriggerBracketGtc] = TriggerBracketGtc(
**kwargs.pop("trigger_bracket_gtc")
)

if "trigger_bracket_gtd" in kwargs:
self.trigger_bracket_gtd: Optional[TriggerBracketGtd] = TriggerBracketGtd(
**kwargs.pop("trigger_bracket_gtd")
Expand Down
6 changes: 3 additions & 3 deletions coinbase/rest/types/futures_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ def __init__(self, response: dict):
"is_intraday_margin_killswitch_enabled"
)
if "is_intraday_margin_enrollment_killswitch_enabled" in response:
self.is_intraday_margin_enrollment_killswitch_enabled: Optional[
bool
] = response.pop("is_intraday_margin_enrollment_killswitch_enabled")
self.is_intraday_margin_enrollment_killswitch_enabled: Optional[bool] = (
response.pop("is_intraday_margin_enrollment_killswitch_enabled")
)
super().__init__(**response)


Expand Down
1 change: 1 addition & 0 deletions coinbase/websocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from coinbase.constants import API_ENV_KEY, API_SECRET_ENV_KEY, WS_USER_BASE_URL

from .types.websocket_response import WebsocketResponse
from .websocket_base import WSBase, WSClientConnectionClosedException, WSClientException


Expand Down
13 changes: 13 additions & 0 deletions coinbase/websocket/types/base_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Any


class BaseResponse:
def __init__(self, **data):
for key in list(data.keys()):
setattr(self, key, data.pop(key))

def __getitem__(self, key: str) -> Any:
return self.__dict__.get(key)

def __repr__(self):
return str(self.__dict__)
226 changes: 226 additions & 0 deletions coinbase/websocket/types/misc_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
from typing import List, Optional

from coinbase.websocket.types.base_response import BaseResponse


class WSHeartBeats(BaseResponse):
def __init__(self, **kwargs):
self.current_time: Optional[str] = kwargs.pop("current_time", None)
self.heartbeat_counter: Optional[str] = kwargs.pop("heartbeat_counter", None)
super().__init__(**kwargs)


class WSCandle(BaseResponse):
def __init__(self, **kwargs):
self.start: str = kwargs.pop("start", None)
self.high: str = kwargs.pop("high", None)
self.low: str = kwargs.pop("low", None)
self.open: str = kwargs.pop("open", None)
self.close: str = kwargs.pop("close", None)
self.volume: str = kwargs.pop("volume", None)
self.product_id: str = kwargs.pop("product_id", None)
super().__init__(**kwargs)


class WSHistoricalMarketTrade(BaseResponse):
def __init__(self, **kwargs):
self.product_id: str = kwargs.pop("product_id", None)
self.trade_id: str = kwargs.pop("trade_id", None)
self.price: str = kwargs.pop("price", None)
self.size: str = kwargs.pop("size", None)
self.time: str = kwargs.pop("time", None)
self.side: str = kwargs.pop("side", None)
super().__init__(**kwargs)


class WSProduct(BaseResponse):
def __init__(self, **kwargs):
self.product_type: str = kwargs.pop("product_type", None)
self.id: str = kwargs.pop("id", None)
self.base_currency: str = kwargs.pop("base_currency", None)
self.quote_currency: str = kwargs.pop("quote_currency", None)
self.base_increment: str = kwargs.pop("base_increment", None)
self.quote_increment: str = kwargs.pop("quote_increment", None)
self.display_name: str = kwargs.pop("display_name", None)
self.status: str = kwargs.pop("status", None)
self.status_message: str = kwargs.pop("status_message", None)
self.min_market_funds: str = kwargs.pop("min_market_funds", None)
super().__init__(**kwargs)


class WSTicker(BaseResponse):
def __init__(self, **kwargs):
self.type: str = kwargs.pop("type", None)
self.product_id: str = kwargs.pop("product_id", None)
self.price: str = kwargs.pop("price", None)
self.volume_24_h: str = kwargs.pop("volume_24_h", None)
self.low_24_h: str = kwargs.pop("low_24_h", None)
self.high_24_h: str = kwargs.pop("high_24_h", None)
self.low_52_w: str = kwargs.pop("low_52_w", None)
self.high_52_w: str = kwargs.pop("high_52_w", None)
self.price_percent_chg_24_h: str = kwargs.pop("price_percent_chg_24_h", None)
self.best_bid: str = kwargs.pop("best_bid", None)
self.best_ask: str = kwargs.pop("best_ask", None)
self.best_bid_quantity: str = kwargs.pop("best_bid_quantity", None)
self.best_ask_quantity: str = kwargs.pop("best_ask_quantity", None)
super().__init__(**kwargs)


class L2Update(BaseResponse):
def __init__(self, **kwargs):
self.side: str = kwargs.pop("side", None)
self.event_time: str = kwargs.pop("event_time", None)
self.price_level: str = kwargs.pop("price_level", None)
self.new_quantity: str = kwargs.pop("new_quantity", None)
super().__init__(**kwargs)


class UserOrders(BaseResponse):
def __init__(self, **kwargs):
self.avg_price: Optional[str] = kwargs.pop("avg_price", None)
self.cancel_reason: Optional[str] = kwargs.pop("cancel_reason", None)
self.client_order_id: Optional[str] = kwargs.pop("client_order_id", None)
self.completion_percentage: Optional[str] = kwargs.pop(
"completion_percentage", None
)
self.contract_expiry_type: Optional[str] = kwargs.pop(
"contract_expiry_type", None
)
self.cumulative_quantity: Optional[str] = kwargs.pop(
"cumulative_quantity", None
)
self.filled_value: Optional[str] = kwargs.pop("filled_value", None)
self.leaves_quantity: Optional[str] = kwargs.pop("leaves_quantity", None)
self.limit_price: Optional[str] = kwargs.pop("limit_price", None)
self.number_of_fills: Optional[str] = kwargs.pop("number_of_fills", None)
self.order_id: Optional[str] = kwargs.pop("order_id", None)
self.order_side: Optional[str] = kwargs.pop("order_side", None)
self.order_type: Optional[str] = kwargs.pop("order_type", None)
self.outstanding_hold_amount: Optional[str] = kwargs.pop(
"outstanding_hold_amount", None
)
self.post_only: Optional[str] = kwargs.pop("post_only", None)
self.product_id: Optional[str] = kwargs.pop("product_id", None)
self.product_type: Optional[str] = kwargs.pop("product_type", None)
self.reject_reason: Optional[str] = kwargs.pop("reject_reason", None)
self.retail_portfolio_id: Optional[str] = kwargs.pop(
"retail_portfolio_id", None
)
self.risk_managed_by: Optional[str] = kwargs.pop("risk_managed_by", None)
self.status: Optional[str] = kwargs.pop("status", None)
self.stop_price: Optional[str] = kwargs.pop("stop_price", None)
self.time_in_force: Optional[str] = kwargs.pop("time_in_force", None)
self.total_fees: Optional[str] = kwargs.pop("total_fees", None)
self.total_value_after_fees: Optional[str] = kwargs.pop(
"total_value_after_fees", None
)
self.trigger_status: Optional[str] = kwargs.pop("trigger_status", None)
self.creation_time: Optional[str] = kwargs.pop("creation_time", None)
self.end_time: Optional[str] = kwargs.pop("end_time", None)
self.start_time: Optional[str] = kwargs.pop("start_time", None)
super().__init__(**kwargs)


class UserPositions(BaseResponse):
def __init__(self, **kwargs):
self.perpetual_futures_positions: Optional[List[UserFuturesPositions]] = (
[
UserFuturesPositions(**position)
for position in kwargs.pop("perpetual_futures_positions", [])
]
if kwargs.get("perpetual_futures_positions") is not None
else []
)
self.expiring_futures_positions: Optional[List[UserExpFuturesPositions]] = (
[
UserExpFuturesPositions(**position)
for position in kwargs.pop("expiring_futures_positions", [])
]
if kwargs.get("expiring_futures_positions") is not None
else []
)
super().__init__(**kwargs)


class UserFuturesPositions(BaseResponse):
def __init__(self, **kwargs):
self.product_id: Optional[str] = kwargs.pop("product_id", None)
self.portfolio_uuid: Optional[str] = kwargs.pop("portfolio_uuid", None)
self.vwap: Optional[str] = kwargs.pop("vwap", None)
self.entry_vwap: Optional[str] = kwargs.pop("entry_vwap", None)
self.position_side: Optional[str] = kwargs.pop("position_side", None)
self.margin_type: Optional[str] = kwargs.pop("margin_type", None)
self.net_size: Optional[str] = kwargs.pop("net_size", None)
self.buy_order_size: Optional[str] = kwargs.pop("buy_order_size", None)
self.sell_order_size: Optional[str] = kwargs.pop("sell_order_size", None)
self.leverage: Optional[str] = kwargs.pop("leverage", None)
self.mark_price: Optional[str] = kwargs.pop("mark_price", None)
self.liquidation_price: Optional[str] = kwargs.pop("liquidation_price", None)
self.im_notional: Optional[str] = kwargs.pop("im_notional", None)
self.mm_notional: Optional[str] = kwargs.pop("mm_notional", None)
self.position_notional: Optional[str] = kwargs.pop("position_notional", None)
self.unrealized_pnl: Optional[str] = kwargs.pop("unrealized_pnl", None)
self.aggregated_pnl: Optional[str] = kwargs.pop("aggregated_pnl", None)
super().__init__(**kwargs)


class UserExpFuturesPositions(BaseResponse):
def __init__(self, **kwargs):
self.product_id: Optional[str] = kwargs.pop("product_id", None)
self.side: Optional[str] = kwargs.pop("side", None)
self.number_of_contracts: Optional[str] = kwargs.pop(
"number_of_contracts", None
)
self.realized_pnl: Optional[str] = kwargs.pop("realized_pnl", None)
self.unrealized_pnl: Optional[str] = kwargs.pop("unrealized_pnl", None)
self.entry_price: Optional[str] = kwargs.pop("entry_price", None)
super().__init__(**kwargs)


class WSFCMBalanceSummary(BaseResponse):
def __init__(self, **kwargs):
self.futures_buying_power: str = kwargs.pop("futures_buying_power", None)
self.total_usd_balance: str = kwargs.pop("total_usd_balance", None)
self.cbi_usd_balance: str = kwargs.pop("cbi_usd_balance", None)
self.cfm_usd_balance: str = kwargs.pop("cfm_usd_balance", None)
self.total_open_orders_hold_amount: str = kwargs.pop(
"total_open_orders_hold_amount", None
)
self.unrealized_pnl: str = kwargs.pop("unrealized_pnl", None)
self.daily_realized_pnl: str = kwargs.pop("daily_realized_pnl", None)
self.initial_margin: str = kwargs.pop("initial_margin", None)
self.available_margin: str = kwargs.pop("available_margin", None)
self.liquidation_threshold: str = kwargs.pop("liquidation_threshold", None)
self.liquidation_buffer_amount: str = kwargs.pop(
"liquidation_buffer_amount", None
)
self.liquidation_buffer_percentage: str = kwargs.pop(
"liquidation_buffer_percentage", None
)
self.intraday_margin_window_measure: Optional[FCMMarginWindowMeasure] = (
FCMMarginWindowMeasure(**kwargs.pop("intraday_margin_window_measure"))
if kwargs.get("intraday_margin_window_measure")
else None
)
self.overnight_margin_window_measure: Optional[FCMMarginWindowMeasure] = (
FCMMarginWindowMeasure(**kwargs.pop("overnight_margin_window_measure"))
if kwargs.get("overnight_margin_window_measure")
else None
)
super().__init__(**kwargs)


class FCMMarginWindowMeasure(BaseResponse):
def __init__(self, **kwargs):
self.margin_window_type: Optional[str] = kwargs.pop("margin_window_type", None)
self.margin_level: Optional[str] = kwargs.pop("margin_level", None)
self.initial_margin: Optional[str] = kwargs.pop("initial_margin", None)
self.maintenance_margin: Optional[str] = kwargs.pop("maintenance_margin", None)
self.liquidation_buffer_percentage: Optional[str] = kwargs.pop(
"liquidation_buffer_percentage", None
)
self.total_hold: Optional[str] = kwargs.pop("total_hold", None)
self.futures_buying_power: Optional[str] = kwargs.pop(
"futures_buying_power", None
)
super().__init__(**kwargs)
Loading

0 comments on commit 2250dbd

Please sign in to comment.