derive-client 0.2.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- derive_client-0.2.12/PKG-INFO +117 -0
- derive_client-0.2.12/README.md +94 -0
- derive_client-0.2.12/derive_client/__init__.py +6 -0
- derive_client-0.2.12/derive_client/analyser.py +50 -0
- derive_client-0.2.12/derive_client/async_client.py +343 -0
- derive_client-0.2.12/derive_client/base_client.py +741 -0
- derive_client-0.2.12/derive_client/cli.py +556 -0
- derive_client-0.2.12/derive_client/constants.py +50 -0
- derive_client-0.2.12/derive_client/create_subaccount_module.py +37 -0
- derive_client-0.2.12/derive_client/derive.py +28 -0
- derive_client-0.2.12/derive_client/enums.py +100 -0
- derive_client-0.2.12/derive_client/http_client.py +9 -0
- derive_client-0.2.12/derive_client/utils.py +32 -0
- derive_client-0.2.12/derive_client/ws_client.py +14 -0
- derive_client-0.2.12/pyproject.toml +77 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: derive-client
|
|
3
|
+
Version: 0.2.12
|
|
4
|
+
Summary:
|
|
5
|
+
Author: 8baller
|
|
6
|
+
Author-email: 8baller@station.codes
|
|
7
|
+
Requires-Python: >=3.9,<3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Requires-Dist: derive-action-signing (>=0.0.9,<0.0.10)
|
|
13
|
+
Requires-Dist: eth-account (>=0.13)
|
|
14
|
+
Requires-Dist: pandas (>=1,<=3)
|
|
15
|
+
Requires-Dist: python-dotenv (>=0.14.0,<0.18.0)
|
|
16
|
+
Requires-Dist: requests (>=2,<3)
|
|
17
|
+
Requires-Dist: rich-click (>=1.7.1,<2.0.0)
|
|
18
|
+
Requires-Dist: setuptools (>=68.2.2,<80)
|
|
19
|
+
Requires-Dist: web3 (>=6,<8)
|
|
20
|
+
Requires-Dist: websocket-client (>=0.32.0,<1)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Derive.xyz Python Client.
|
|
24
|
+
|
|
25
|
+
This repo provides a unified interface for the Derive Exchange.
|
|
26
|
+
|
|
27
|
+
Please checkout the [examples](./examples) directory for usage.
|
|
28
|
+
|
|
29
|
+
Here is a quick demonstration of the cli functionality.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Preparing Keys for the Client
|
|
35
|
+
|
|
36
|
+
To use the client, you will need to generate an API key from the Derive Exchange.
|
|
37
|
+
|
|
38
|
+
The process involves linking your local signer to the account you want to use programmatically.
|
|
39
|
+
|
|
40
|
+
Here are the steps:
|
|
41
|
+
|
|
42
|
+
0. Generate a local signer using your preferred method. For example, you can use the Open Aea Ledger Ethereum Cli.
|
|
43
|
+
```bash
|
|
44
|
+
aea generate-key ethereum
|
|
45
|
+
```
|
|
46
|
+
This will generate a new private key in the `ethereum_private_key.txt` file.
|
|
47
|
+
|
|
48
|
+
1. Go to the [Derive Exchange](https://derive.xyz) and create an account.
|
|
49
|
+
2. Go to the API section and create a new [API key](https://.derive.xyz/api-keys/developers).
|
|
50
|
+
3. Register a new Session key with the Public Address of the account your signer generated in step 0.
|
|
51
|
+
|
|
52
|
+
Once you have the API key, you can use it to interact with the Derive Exchange.
|
|
53
|
+
|
|
54
|
+
You need;
|
|
55
|
+
|
|
56
|
+
`DERIVE_WALLET` - The programtic wallet generated upon account creation. It can be found in the Developer section of the Derive Exchange.
|
|
57
|
+
`SIGNER_PRIVATE_KEY` - The private key generated in step 0.
|
|
58
|
+
`SUBACCOOUNT_ID` - The subaccount id you want to use for the API key.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
derive_client = DeriveClient(
|
|
62
|
+
private_key=TEST_PRIVATE_KEY,
|
|
63
|
+
env=Environment.TEST, # or Environment.PROD
|
|
64
|
+
wallet=TEST_WALLET,
|
|
65
|
+
subaccount_id = 123456
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install derive-client
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Dev
|
|
79
|
+
|
|
80
|
+
### Formatting
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
make fmt
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Linting
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
make lint
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Tests
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
make tests
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For convience, all commands can be run with:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
make all
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Releasing
|
|
105
|
+
|
|
106
|
+
We can use `tbump` to automatically bump our versions in preparation of a release.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
export new_version=0.1.5
|
|
110
|
+
tbump $new_version
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The release workflow will then detect that a branch with a `v` prefix exists and create a release from it.
|
|
114
|
+
|
|
115
|
+
Additionally, the package will be published to PyPI.
|
|
116
|
+
|
|
117
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Derive.xyz Python Client.
|
|
2
|
+
|
|
3
|
+
This repo provides a unified interface for the Derive Exchange.
|
|
4
|
+
|
|
5
|
+
Please checkout the [examples](./examples) directory for usage.
|
|
6
|
+
|
|
7
|
+
Here is a quick demonstration of the cli functionality.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Preparing Keys for the Client
|
|
13
|
+
|
|
14
|
+
To use the client, you will need to generate an API key from the Derive Exchange.
|
|
15
|
+
|
|
16
|
+
The process involves linking your local signer to the account you want to use programmatically.
|
|
17
|
+
|
|
18
|
+
Here are the steps:
|
|
19
|
+
|
|
20
|
+
0. Generate a local signer using your preferred method. For example, you can use the Open Aea Ledger Ethereum Cli.
|
|
21
|
+
```bash
|
|
22
|
+
aea generate-key ethereum
|
|
23
|
+
```
|
|
24
|
+
This will generate a new private key in the `ethereum_private_key.txt` file.
|
|
25
|
+
|
|
26
|
+
1. Go to the [Derive Exchange](https://derive.xyz) and create an account.
|
|
27
|
+
2. Go to the API section and create a new [API key](https://.derive.xyz/api-keys/developers).
|
|
28
|
+
3. Register a new Session key with the Public Address of the account your signer generated in step 0.
|
|
29
|
+
|
|
30
|
+
Once you have the API key, you can use it to interact with the Derive Exchange.
|
|
31
|
+
|
|
32
|
+
You need;
|
|
33
|
+
|
|
34
|
+
`DERIVE_WALLET` - The programtic wallet generated upon account creation. It can be found in the Developer section of the Derive Exchange.
|
|
35
|
+
`SIGNER_PRIVATE_KEY` - The private key generated in step 0.
|
|
36
|
+
`SUBACCOOUNT_ID` - The subaccount id you want to use for the API key.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
derive_client = DeriveClient(
|
|
40
|
+
private_key=TEST_PRIVATE_KEY,
|
|
41
|
+
env=Environment.TEST, # or Environment.PROD
|
|
42
|
+
wallet=TEST_WALLET,
|
|
43
|
+
subaccount_id = 123456
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install derive-client
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Dev
|
|
57
|
+
|
|
58
|
+
### Formatting
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
make fmt
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Linting
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
make lint
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Tests
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
make tests
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For convience, all commands can be run with:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
make all
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Releasing
|
|
83
|
+
|
|
84
|
+
We can use `tbump` to automatically bump our versions in preparation of a release.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export new_version=0.1.5
|
|
88
|
+
tbump $new_version
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The release workflow will then detect that a branch with a `v` prefix exists and create a release from it.
|
|
92
|
+
|
|
93
|
+
Additionally, the package will be published to PyPI.
|
|
94
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class based analyser for portfolios.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
pd.set_option('display.precision', 2)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DELTA_COLUMNS = ['delta', 'gamma', 'vega', 'theta']
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PortfolioAnalyser:
|
|
16
|
+
raw_data: List[dict]
|
|
17
|
+
df: pd.DataFrame
|
|
18
|
+
|
|
19
|
+
def __init__(self, raw_data: List[dict]):
|
|
20
|
+
self.raw_data = raw_data
|
|
21
|
+
self.positions = pd.DataFrame.from_records(raw_data['positions'])
|
|
22
|
+
self.positions["amount"] = pd.to_numeric(self.positions["amount"])
|
|
23
|
+
for col in DELTA_COLUMNS:
|
|
24
|
+
self.positions[col] = pd.to_numeric(self.positions[col])
|
|
25
|
+
adjusted_greek = self.positions[col] * self.positions.amount
|
|
26
|
+
self.positions[col] = adjusted_greek
|
|
27
|
+
|
|
28
|
+
self.positions = self.positions.apply(pd.to_numeric, errors='ignore')
|
|
29
|
+
|
|
30
|
+
def get_positions(self, underlying_currency: str) -> pd.DataFrame:
|
|
31
|
+
df = self.positions
|
|
32
|
+
df = df[df['instrument_name'].str.contains(underlying_currency.upper())]
|
|
33
|
+
return df
|
|
34
|
+
|
|
35
|
+
def get_open_positions(self, underlying_currency: str) -> pd.DataFrame:
|
|
36
|
+
df = self.get_positions(underlying_currency)
|
|
37
|
+
return df[df['amount'] != 0]
|
|
38
|
+
|
|
39
|
+
def get_total_greeks(self, underlying_currency: str) -> pd.DataFrame:
|
|
40
|
+
df = self.get_open_positions(underlying_currency)
|
|
41
|
+
return df[DELTA_COLUMNS].sum()
|
|
42
|
+
|
|
43
|
+
def get_subaccount_value(self) -> float:
|
|
44
|
+
return float(self.raw_data['subaccount_value'])
|
|
45
|
+
|
|
46
|
+
def print_positions(self, underlying_currency: str, columns: Optional[List[str]] = None):
|
|
47
|
+
df = self.get_open_positions(underlying_currency)
|
|
48
|
+
if columns:
|
|
49
|
+
df = df[[c for c in columns if c not in DELTA_COLUMNS] + DELTA_COLUMNS]
|
|
50
|
+
print(df)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async client for Derive
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from decimal import ROUND_DOWN, Decimal
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
from derive_action_signing.utils import sign_ws_login, utc_now_ms
|
|
13
|
+
from web3 import Web3
|
|
14
|
+
|
|
15
|
+
from derive_client.base_client import ApiException
|
|
16
|
+
from derive_client.constants import CONTRACTS, DEFAULT_REFERER, TEST_PRIVATE_KEY
|
|
17
|
+
from derive_client.enums import Environment, InstrumentType, OrderSide, OrderType, TimeInForce, UnderlyingCurrency
|
|
18
|
+
from derive_client.utils import get_logger
|
|
19
|
+
from derive_client.ws_client import WsClient as BaseClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeriveAsyncClient(BaseClient):
|
|
23
|
+
"""
|
|
24
|
+
We use the async client to make async requests to the derive API
|
|
25
|
+
We us the ws client to make async requests to the derive ws API
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
current_subscriptions = {}
|
|
29
|
+
|
|
30
|
+
listener = None
|
|
31
|
+
subscribing = False
|
|
32
|
+
_ws = None
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
private_key: str = TEST_PRIVATE_KEY,
|
|
37
|
+
env: Environment = Environment.TEST,
|
|
38
|
+
logger=None,
|
|
39
|
+
verbose=False,
|
|
40
|
+
subaccount_id=None,
|
|
41
|
+
wallet=None,
|
|
42
|
+
):
|
|
43
|
+
self.verbose = verbose
|
|
44
|
+
self.env = env
|
|
45
|
+
self.contracts = CONTRACTS[env]
|
|
46
|
+
self.logger = logger or get_logger()
|
|
47
|
+
self.web3_client = Web3()
|
|
48
|
+
self.signer = self.web3_client.eth.account.from_key(private_key)
|
|
49
|
+
self.wallet = self.signer.address if not wallet else wallet
|
|
50
|
+
print(f"Signing address: {self.signer.address}")
|
|
51
|
+
if wallet:
|
|
52
|
+
print(f"Using wallet: {wallet}")
|
|
53
|
+
self.subaccount_id = subaccount_id
|
|
54
|
+
print(f"Using subaccount id: {self.subaccount_id}")
|
|
55
|
+
self.message_queues = {}
|
|
56
|
+
self.connecting = False
|
|
57
|
+
# we make sure to get the event loop
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
async def ws(self):
|
|
61
|
+
if self._ws is None:
|
|
62
|
+
self._ws = await self.connect_ws()
|
|
63
|
+
if not self._ws.connected:
|
|
64
|
+
self._ws = await self.connect_ws()
|
|
65
|
+
return self._ws
|
|
66
|
+
|
|
67
|
+
def get_subscription_id(self, instrument_name: str, group: str = "1", depth: str = "100"):
|
|
68
|
+
return f"orderbook.{instrument_name}.{group}.{depth}"
|
|
69
|
+
|
|
70
|
+
async def subscribe(self, instrument_name: str, group: str = "1", depth: str = "100"):
|
|
71
|
+
"""
|
|
72
|
+
Subscribe to the order book for a symbol
|
|
73
|
+
"""
|
|
74
|
+
# if self.listener is None or self.listener.done():
|
|
75
|
+
asyncio.create_task(self.listen_for_messages())
|
|
76
|
+
channel = self.get_subscription_id(instrument_name, group, depth)
|
|
77
|
+
if channel not in self.message_queues:
|
|
78
|
+
self.message_queues[channel] = asyncio.Queue()
|
|
79
|
+
msg = {"method": "subscribe", "params": {"channels": [channel]}}
|
|
80
|
+
await self._ws.send_json(msg)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
while instrument_name not in self.current_subscriptions:
|
|
84
|
+
await asyncio.sleep(0.01)
|
|
85
|
+
return self.current_subscriptions[instrument_name]
|
|
86
|
+
|
|
87
|
+
async def connect_ws(self):
|
|
88
|
+
self.connecting = True
|
|
89
|
+
self.session = aiohttp.ClientSession()
|
|
90
|
+
ws = await self.session.ws_connect(self.contracts['WS_ADDRESS'])
|
|
91
|
+
self._ws = ws
|
|
92
|
+
self.connecting = False
|
|
93
|
+
return ws
|
|
94
|
+
|
|
95
|
+
async def listen_for_messages(
|
|
96
|
+
self,
|
|
97
|
+
):
|
|
98
|
+
while True:
|
|
99
|
+
try:
|
|
100
|
+
msg = await self.ws.receive_json()
|
|
101
|
+
except TypeError:
|
|
102
|
+
continue
|
|
103
|
+
if "error" in msg:
|
|
104
|
+
print(msg)
|
|
105
|
+
raise Exception(msg["error"])
|
|
106
|
+
if "result" in msg:
|
|
107
|
+
result = msg["result"]
|
|
108
|
+
if "status" in result:
|
|
109
|
+
# print(f"Succesfully subscribed to {result['status']}")
|
|
110
|
+
for channel, value in result['status'].items():
|
|
111
|
+
# print(f"Channel {channel} has value {value}")
|
|
112
|
+
if "error" in value:
|
|
113
|
+
raise Exception(f"Subscription error for channel: {channel} error: {value['error']}")
|
|
114
|
+
continue
|
|
115
|
+
# default to putting the message in the queue
|
|
116
|
+
subscription = msg['params']['channel']
|
|
117
|
+
data = msg['params']['data']
|
|
118
|
+
self.handle_message(subscription, data)
|
|
119
|
+
|
|
120
|
+
async def login_client(
|
|
121
|
+
self,
|
|
122
|
+
retries=3,
|
|
123
|
+
):
|
|
124
|
+
login_request = {
|
|
125
|
+
'method': 'public/login',
|
|
126
|
+
'params': sign_ws_login(
|
|
127
|
+
web3_client=self.web3_client,
|
|
128
|
+
smart_contract_wallet=self.wallet,
|
|
129
|
+
session_key_or_wallet_private_key=self.signer._private_key,
|
|
130
|
+
),
|
|
131
|
+
'id': str(utc_now_ms()),
|
|
132
|
+
}
|
|
133
|
+
await self._ws.send_json(login_request)
|
|
134
|
+
# we need to wait for the response
|
|
135
|
+
async for msg in self._ws:
|
|
136
|
+
message = json.loads(msg.data)
|
|
137
|
+
if message['id'] == login_request['id']:
|
|
138
|
+
if "result" not in message:
|
|
139
|
+
if self._check_output_for_rate_limit(message):
|
|
140
|
+
return await self.login_client()
|
|
141
|
+
raise ApiException(message['error'])
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
def handle_message(self, subscription, data):
|
|
145
|
+
bids = data['bids']
|
|
146
|
+
asks = data['asks']
|
|
147
|
+
|
|
148
|
+
bids = list(map(lambda x: (float(x[0]), float(x[1])), bids))
|
|
149
|
+
asks = list(map(lambda x: (float(x[0]), float(x[1])), asks))
|
|
150
|
+
|
|
151
|
+
instrument_name = subscription.split(".")[1]
|
|
152
|
+
|
|
153
|
+
if subscription in self.current_subscriptions:
|
|
154
|
+
old_params = self.current_subscriptions[subscription]
|
|
155
|
+
_asks, _bids = old_params["asks"], old_params["bids"]
|
|
156
|
+
if not asks:
|
|
157
|
+
asks = _asks
|
|
158
|
+
if not bids:
|
|
159
|
+
bids = _bids
|
|
160
|
+
timestamp = data['timestamp']
|
|
161
|
+
datetime_str = datetime.fromtimestamp(timestamp / 1000)
|
|
162
|
+
nonce = data['publish_id']
|
|
163
|
+
self.current_subscriptions[instrument_name] = {
|
|
164
|
+
"asks": asks,
|
|
165
|
+
"bids": bids,
|
|
166
|
+
"timestamp": timestamp,
|
|
167
|
+
"datetime": datetime_str.isoformat(),
|
|
168
|
+
"nonce": nonce,
|
|
169
|
+
"symbol": instrument_name,
|
|
170
|
+
}
|
|
171
|
+
return self.current_subscriptions[instrument_name]
|
|
172
|
+
|
|
173
|
+
async def watch_order_book(self, instrument_name: str, group: str = "1", depth: str = "100"):
|
|
174
|
+
"""
|
|
175
|
+
Watch the order book for a symbol
|
|
176
|
+
orderbook.{instrument_name}.{group}.{depth}
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
if not self.ws and not self.connecting:
|
|
180
|
+
await self.connect_ws()
|
|
181
|
+
await self.login_client()
|
|
182
|
+
|
|
183
|
+
subscription = self.get_subscription_id(instrument_name, group, depth)
|
|
184
|
+
|
|
185
|
+
if subscription not in self.message_queues:
|
|
186
|
+
while any([self.subscribing, self.ws is None, self.connecting]):
|
|
187
|
+
await asyncio.sleep(1)
|
|
188
|
+
await self.subscribe(instrument_name, group, depth)
|
|
189
|
+
|
|
190
|
+
while instrument_name not in self.current_subscriptions and not self.connecting:
|
|
191
|
+
await asyncio.sleep(0.01)
|
|
192
|
+
|
|
193
|
+
return self.current_subscriptions[instrument_name]
|
|
194
|
+
|
|
195
|
+
async def fetch_instruments(
|
|
196
|
+
self,
|
|
197
|
+
expired=False,
|
|
198
|
+
instrument_type: InstrumentType = InstrumentType.PERP,
|
|
199
|
+
currency: UnderlyingCurrency = UnderlyingCurrency.BTC,
|
|
200
|
+
):
|
|
201
|
+
return super().fetch_instruments(expired, instrument_type, currency)
|
|
202
|
+
|
|
203
|
+
async def close(self):
|
|
204
|
+
"""
|
|
205
|
+
Close the connection
|
|
206
|
+
"""
|
|
207
|
+
self.ws.close()
|
|
208
|
+
|
|
209
|
+
async def fetch_tickers(
|
|
210
|
+
self,
|
|
211
|
+
instrument_type: InstrumentType = InstrumentType.OPTION,
|
|
212
|
+
currency: UnderlyingCurrency = UnderlyingCurrency.BTC,
|
|
213
|
+
):
|
|
214
|
+
if not self._ws:
|
|
215
|
+
await self.connect_ws()
|
|
216
|
+
instruments = await self.fetch_instruments(instrument_type=instrument_type, currency=currency)
|
|
217
|
+
instrument_names = [i['instrument_name'] for i in instruments]
|
|
218
|
+
id_base = str(int(time.time()))
|
|
219
|
+
ids_to_instrument_names = {
|
|
220
|
+
f'{id_base}_{enumerate}': instrument_name for enumerate, instrument_name in enumerate(instrument_names)
|
|
221
|
+
}
|
|
222
|
+
for id, instrument_name in ids_to_instrument_names.items():
|
|
223
|
+
payload = {"instrument_name": instrument_name}
|
|
224
|
+
await self._ws.send_json({'method': 'public/get_ticker', 'params': payload, 'id': id})
|
|
225
|
+
await asyncio.sleep(0.1) # otherwise we get rate limited...
|
|
226
|
+
results = {}
|
|
227
|
+
while ids_to_instrument_names:
|
|
228
|
+
message = await self._ws.receive()
|
|
229
|
+
if message is None:
|
|
230
|
+
continue
|
|
231
|
+
if 'error' in message:
|
|
232
|
+
raise Exception(f"Error fetching ticker {message}")
|
|
233
|
+
if message.type == aiohttp.WSMsgType.CLOSED:
|
|
234
|
+
# we try to reconnect
|
|
235
|
+
print(f"Erorr fetching ticker {message}...")
|
|
236
|
+
self._ws = await self.connect_ws()
|
|
237
|
+
return await self.fetch_tickers(instrument_type, currency)
|
|
238
|
+
message = json.loads(message.data)
|
|
239
|
+
if message['id'] in ids_to_instrument_names:
|
|
240
|
+
try:
|
|
241
|
+
results[message['result']['instrument_name']] = message['result']
|
|
242
|
+
except KeyError:
|
|
243
|
+
print(f"Error fetching ticker {message}")
|
|
244
|
+
del ids_to_instrument_names[message['id']]
|
|
245
|
+
return results
|
|
246
|
+
|
|
247
|
+
async def get_collaterals(self):
|
|
248
|
+
return super().get_collaterals()
|
|
249
|
+
|
|
250
|
+
async def get_positions(self, currency: UnderlyingCurrency = UnderlyingCurrency.BTC):
|
|
251
|
+
return super().get_positions()
|
|
252
|
+
|
|
253
|
+
async def get_open_orders(self, status, currency: UnderlyingCurrency = UnderlyingCurrency.BTC):
|
|
254
|
+
return super().fetch_orders(
|
|
255
|
+
status=status,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
async def fetch_ticker(self, instrument_name: str):
|
|
259
|
+
"""
|
|
260
|
+
Fetch the ticker for a symbol
|
|
261
|
+
"""
|
|
262
|
+
return super().fetch_ticker(instrument_name)
|
|
263
|
+
|
|
264
|
+
async def create_order(
|
|
265
|
+
self,
|
|
266
|
+
price,
|
|
267
|
+
amount,
|
|
268
|
+
instrument_name: str,
|
|
269
|
+
reduce_only=False,
|
|
270
|
+
side: OrderSide = OrderSide.BUY,
|
|
271
|
+
order_type: OrderType = OrderType.LIMIT,
|
|
272
|
+
time_in_force: TimeInForce = TimeInForce.GTC,
|
|
273
|
+
instrument_type: InstrumentType = InstrumentType.PERP,
|
|
274
|
+
underlying_currency: UnderlyingCurrency = UnderlyingCurrency.USDC,
|
|
275
|
+
):
|
|
276
|
+
"""
|
|
277
|
+
Create the order.
|
|
278
|
+
"""
|
|
279
|
+
if not self._ws:
|
|
280
|
+
await self.connect_ws()
|
|
281
|
+
await self.login_client()
|
|
282
|
+
if side.name.upper() not in OrderSide.__members__:
|
|
283
|
+
raise Exception(f"Invalid side {side}")
|
|
284
|
+
instruments = await self._internal_map_instrument(instrument_type, underlying_currency)
|
|
285
|
+
instrument = instruments[instrument_name]
|
|
286
|
+
|
|
287
|
+
rounded_price = Decimal(price).quantize(Decimal(instrument['tick_size']), rounding=ROUND_DOWN)
|
|
288
|
+
rounded_amount = Decimal(amount).quantize(Decimal(instrument['amount_step']), rounding=ROUND_DOWN)
|
|
289
|
+
|
|
290
|
+
module_data = {
|
|
291
|
+
"asset_address": instrument['base_asset_address'],
|
|
292
|
+
"sub_id": int(instrument['base_asset_sub_id']),
|
|
293
|
+
"limit_price": rounded_price,
|
|
294
|
+
"amount": rounded_amount,
|
|
295
|
+
"max_fee": Decimal(1000),
|
|
296
|
+
"recipient_id": int(self.subaccount_id),
|
|
297
|
+
"is_bid": side == OrderSide.BUY,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
signed_action = self._generate_signed_action(
|
|
301
|
+
module_address=self.contracts['TRADE_MODULE_ADDRESS'], module_data=module_data
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
order = {
|
|
305
|
+
"instrument_name": instrument_name,
|
|
306
|
+
"direction": side.name.lower(),
|
|
307
|
+
"order_type": order_type.name.lower(),
|
|
308
|
+
"mmp": False,
|
|
309
|
+
"time_in_force": time_in_force.value,
|
|
310
|
+
"referral_code": DEFAULT_REFERER if not self.referral_code else self.referral_code,
|
|
311
|
+
**signed_action.to_json(),
|
|
312
|
+
}
|
|
313
|
+
try:
|
|
314
|
+
response = await self.submit_order(order)
|
|
315
|
+
except aiohttp.ClientConnectionResetError:
|
|
316
|
+
await self.connect_ws()
|
|
317
|
+
await self.login_client()
|
|
318
|
+
response = await self.submit_order(order)
|
|
319
|
+
return response
|
|
320
|
+
|
|
321
|
+
async def _internal_map_instrument(self, instrument_type, currency):
|
|
322
|
+
"""
|
|
323
|
+
Map the instrument.
|
|
324
|
+
"""
|
|
325
|
+
instruments = await self.fetch_instruments(instrument_type=instrument_type, currency=currency)
|
|
326
|
+
return {i['instrument_name']: i for i in instruments}
|
|
327
|
+
|
|
328
|
+
async def submit_order(self, order):
|
|
329
|
+
id = str(utc_now_ms())
|
|
330
|
+
await self._ws.send_json({'method': 'private/order', 'params': order, 'id': id})
|
|
331
|
+
while True:
|
|
332
|
+
async for msg in self._ws:
|
|
333
|
+
message = json.loads(msg.data)
|
|
334
|
+
if message['id'] == id:
|
|
335
|
+
try:
|
|
336
|
+
if "result" not in message:
|
|
337
|
+
if self._check_output_for_rate_limit(message):
|
|
338
|
+
return await self.submit_order(order)
|
|
339
|
+
raise ApiException(message['error'])
|
|
340
|
+
return message['result']['order']
|
|
341
|
+
except KeyError as error:
|
|
342
|
+
print(message)
|
|
343
|
+
raise Exception(f"Unable to submit order {message}") from error
|