ostium-python-sdk 0.1.3__py3-none-any.whl
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.
- ostium_python_sdk/__init__.py +4 -0
- ostium_python_sdk/abi.py +4071 -0
- ostium_python_sdk/balance.py +54 -0
- ostium_python_sdk/config.py +33 -0
- ostium_python_sdk/constants.py +11 -0
- ostium_python_sdk/formulae.py +352 -0
- ostium_python_sdk/formulae_wrapper.py +294 -0
- ostium_python_sdk/ostium.py +320 -0
- ostium_python_sdk/price.py +37 -0
- ostium_python_sdk/sdk.py +33 -0
- ostium_python_sdk/subgraph.py +187 -0
- ostium_python_sdk/utils.py +604 -0
- ostium_python_sdk-0.1.3.dist-info/METADATA +93 -0
- ostium_python_sdk-0.1.3.dist-info/RECORD +16 -0
- ostium_python_sdk-0.1.3.dist-info/WHEEL +5 -0
- ostium_python_sdk-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from formulae import (PRECISION_18, PRECISION_2, PRECISION_6, GetCurrentRolloverFee,
|
|
3
|
+
GetTradeFundingFee, GetTradeLiquidationPrice, GetTradeRolloverFee,
|
|
4
|
+
GetFundingRate, GetPriceImpact, CurrentTradeProfitRaw,
|
|
5
|
+
CurrentTotalProfitRaw, CurrentTotalProfitP)
|
|
6
|
+
from typing import Dict, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_liq_price(trade_details, pair_info, block_number):
|
|
10
|
+
current_funding_fee = GetTradeFundingFee(trade_details['funding'], pair_info['accFundingLong'] if trade_details['isBuy']
|
|
11
|
+
else pair_info['accFundingShort'], trade_details['collateral'], trade_details['leverage'])
|
|
12
|
+
|
|
13
|
+
current_rollover_fee = GetCurrentRolloverFee(
|
|
14
|
+
pair_info['accRollover'], pair_info['lastRolloverBlock'], pair_info['rolloverFeePerBlock'], block_number)
|
|
15
|
+
|
|
16
|
+
trade_rollover_fee = GetTradeRolloverFee(
|
|
17
|
+
trade_details['rollover'], current_rollover_fee, trade_details['collateral'], trade_details['leverage'])
|
|
18
|
+
|
|
19
|
+
liq_price = GetTradeLiquidationPrice(trade_details['openPrice'], trade_details['isBuy'], Decimal(trade_details['collateral']) / PRECISION_6, Decimal(
|
|
20
|
+
trade_details['leverage']) / PRECISION_2, Decimal(trade_rollover_fee)/PRECISION_6, Decimal(current_funding_fee) / PRECISION_6)
|
|
21
|
+
|
|
22
|
+
return liq_price / PRECISION_18
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_funding_fee_long_short(pair_info, block_number):
|
|
26
|
+
funding_rate_raw = GetFundingRate(
|
|
27
|
+
pair_info['accFundingLong'],
|
|
28
|
+
pair_info['accFundingShort'],
|
|
29
|
+
pair_info['lastFundingRate'],
|
|
30
|
+
pair_info['lastFundingVelocity'],
|
|
31
|
+
pair_info['maxFundingFeePerBlock'],
|
|
32
|
+
pair_info['lastFundingBlock'],
|
|
33
|
+
str(block_number),
|
|
34
|
+
pair_info['longOI'],
|
|
35
|
+
pair_info['shortOI']
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Convert latest funding rate to decimal
|
|
39
|
+
latest_rate = Decimal(
|
|
40
|
+
funding_rate_raw['latestFundingRate']) / PRECISION_18 # Fixed key name
|
|
41
|
+
|
|
42
|
+
# Convert OI values to decimal
|
|
43
|
+
long_oi = Decimal(pair_info['longOI']) / PRECISION_18
|
|
44
|
+
short_oi = Decimal(pair_info['shortOI']) / PRECISION_18
|
|
45
|
+
|
|
46
|
+
if funding_rate_raw['longsPay']:
|
|
47
|
+
# If longs pay, they get negative rate
|
|
48
|
+
long_rate = -latest_rate
|
|
49
|
+
# Shorts receive proportional to OI ratio
|
|
50
|
+
short_rate = latest_rate * long_oi / \
|
|
51
|
+
short_oi if short_oi > 0 else Decimal('0')
|
|
52
|
+
else:
|
|
53
|
+
# If shorts pay, they get negative rate
|
|
54
|
+
short_rate = -latest_rate
|
|
55
|
+
# Longs receive proportional to OI ratio
|
|
56
|
+
long_rate = latest_rate * short_oi / \
|
|
57
|
+
long_oi if long_oi > 0 else Decimal('0')
|
|
58
|
+
|
|
59
|
+
return float(long_rate), float(short_rate)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_trade_pnl(trade_details, pair_info, price_data, block_number):
|
|
63
|
+
"""
|
|
64
|
+
Calculate PNL and related metrics for a trade.
|
|
65
|
+
"""
|
|
66
|
+
if not trade_details or not price_data or not block_number or not pair_info:
|
|
67
|
+
return {
|
|
68
|
+
'pnl': 0,
|
|
69
|
+
'pnl_raw': '0',
|
|
70
|
+
'pnl_percent': 0,
|
|
71
|
+
'rollover': 0,
|
|
72
|
+
'funding': 0,
|
|
73
|
+
'total_profit': 0,
|
|
74
|
+
'net_pnl': 0,
|
|
75
|
+
'net_value': 0,
|
|
76
|
+
'liquidation_price': 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Calculate current rollover fee
|
|
80
|
+
current_rollover_raw = GetCurrentRolloverFee(
|
|
81
|
+
pair_info['accRollover'],
|
|
82
|
+
pair_info['lastRolloverBlock'],
|
|
83
|
+
pair_info['rolloverFeePerBlock'],
|
|
84
|
+
str(block_number)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Calculate rollover for this trade
|
|
88
|
+
rollover_raw = GetTradeRolloverFee(
|
|
89
|
+
trade_details['rollover'],
|
|
90
|
+
current_rollover_raw,
|
|
91
|
+
trade_details['collateral'],
|
|
92
|
+
trade_details['leverage']
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Get funding rate
|
|
96
|
+
funding_rate_raw = GetFundingRate(
|
|
97
|
+
pair_info['accFundingLong'],
|
|
98
|
+
pair_info['accFundingShort'],
|
|
99
|
+
pair_info['lastFundingRate'],
|
|
100
|
+
pair_info['lastFundingVelocity'],
|
|
101
|
+
pair_info['maxFundingFeePerBlock'],
|
|
102
|
+
pair_info['lastFundingBlock'],
|
|
103
|
+
str(block_number),
|
|
104
|
+
pair_info['longOI'],
|
|
105
|
+
pair_info['shortOI']
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Calculate funding fee
|
|
109
|
+
funding_raw = GetTradeFundingFee(
|
|
110
|
+
trade_details['funding'],
|
|
111
|
+
funding_rate_raw['accFundingLong'] if trade_details['isBuy'] else funding_rate_raw['accFundingShort'],
|
|
112
|
+
trade_details['collateral'],
|
|
113
|
+
trade_details['leverage']
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Calculate liquidation price
|
|
117
|
+
liquidation_price = GetTradeLiquidationPrice(
|
|
118
|
+
trade_details['openPrice'],
|
|
119
|
+
trade_details['isBuy'],
|
|
120
|
+
trade_details['collateral'],
|
|
121
|
+
trade_details['leverage'],
|
|
122
|
+
str(rollover_raw),
|
|
123
|
+
str(funding_raw)
|
|
124
|
+
)
|
|
125
|
+
liquidation_price = Decimal(liquidation_price) / PRECISION_18
|
|
126
|
+
|
|
127
|
+
# Calculate price impact
|
|
128
|
+
price_impact_raw = GetPriceImpact(
|
|
129
|
+
str(int(Decimal(str(price_data['mid'])) * PRECISION_18)),
|
|
130
|
+
str(int(Decimal(str(price_data['bid'])) * PRECISION_18)),
|
|
131
|
+
str(int(Decimal(str(price_data['ask'])) * PRECISION_18)),
|
|
132
|
+
pair_info['spreadP'],
|
|
133
|
+
False,
|
|
134
|
+
trade_details['isBuy'],
|
|
135
|
+
True,
|
|
136
|
+
str(Decimal(trade_details['collateral']) *
|
|
137
|
+
Decimal(trade_details['leverage']) / PRECISION_2),
|
|
138
|
+
pair_info['tradeSizeRef']
|
|
139
|
+
)
|
|
140
|
+
price_after_impact = price_impact_raw['priceAfterImpact']
|
|
141
|
+
|
|
142
|
+
# Calculate PNL
|
|
143
|
+
pnl_raw = CurrentTradeProfitRaw(
|
|
144
|
+
trade_details['openPrice'],
|
|
145
|
+
price_after_impact,
|
|
146
|
+
trade_details['isBuy'],
|
|
147
|
+
trade_details['leverage'],
|
|
148
|
+
trade_details['collateral']
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Calculate total profit
|
|
152
|
+
total_profit_raw = CurrentTotalProfitRaw(
|
|
153
|
+
trade_details['openPrice'],
|
|
154
|
+
price_after_impact,
|
|
155
|
+
trade_details['isBuy'],
|
|
156
|
+
trade_details['leverage'],
|
|
157
|
+
trade_details['collateral'],
|
|
158
|
+
str(rollover_raw),
|
|
159
|
+
str(funding_raw)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Calculate PNL percentage
|
|
163
|
+
pnl_percent_raw = CurrentTotalProfitP(
|
|
164
|
+
str(total_profit_raw), trade_details['collateral'])
|
|
165
|
+
|
|
166
|
+
# Convert values to proper decimals
|
|
167
|
+
pnl = Decimal(pnl_raw) / PRECISION_6
|
|
168
|
+
pnl_percent = Decimal(pnl_percent_raw) / PRECISION_6
|
|
169
|
+
net_pnl = Decimal(total_profit_raw) / PRECISION_6
|
|
170
|
+
total_profit = Decimal(total_profit_raw) / PRECISION_6
|
|
171
|
+
funding = Decimal(funding_raw) / PRECISION_6
|
|
172
|
+
rollover = Decimal(rollover_raw) / PRECISION_6
|
|
173
|
+
net_value = net_pnl + (Decimal(trade_details['collateral']) / PRECISION_6)
|
|
174
|
+
price_impact = Decimal(price_after_impact) / PRECISION_18
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
'pnl': float(pnl),
|
|
178
|
+
'pnl_raw': str(pnl_raw),
|
|
179
|
+
'pnl_percent': float(pnl_percent),
|
|
180
|
+
'rollover': float(rollover),
|
|
181
|
+
'funding': float(funding),
|
|
182
|
+
'funding_raw': str(funding_raw),
|
|
183
|
+
'rollover_raw': str(rollover_raw),
|
|
184
|
+
'total_profit': float(total_profit),
|
|
185
|
+
'net_pnl': float(net_pnl),
|
|
186
|
+
'net_value': float(net_value),
|
|
187
|
+
'liquidation_price': float(liquidation_price),
|
|
188
|
+
'price_impact': float(price_impact)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def ceil_div(a: int, b: int) -> int:
|
|
193
|
+
"""Implements ceiling division for integers"""
|
|
194
|
+
return -((-a) // b)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def GetFundingRate(
|
|
198
|
+
acc_per_oi_long: str,
|
|
199
|
+
acc_per_oi_short: str,
|
|
200
|
+
last_funding_rate: str,
|
|
201
|
+
last_velocity: str,
|
|
202
|
+
max_funding_fee_per_block: str,
|
|
203
|
+
last_update_block: str,
|
|
204
|
+
latest_block: str,
|
|
205
|
+
oi_long: str,
|
|
206
|
+
oi_short: str,
|
|
207
|
+
) -> Dict[str, Union[str, bool]]:
|
|
208
|
+
# Convert string inputs to integers (similar to BigNumber.from)
|
|
209
|
+
acc_per_oi_long_bn = int(acc_per_oi_long)
|
|
210
|
+
acc_per_oi_short_bn = int(acc_per_oi_short)
|
|
211
|
+
last_funding_rate_bn = int(last_funding_rate)
|
|
212
|
+
last_velocity_bn = int(last_velocity)
|
|
213
|
+
max_funding_fee_per_block_bn = int(max_funding_fee_per_block)
|
|
214
|
+
last_update_block_bn = int(last_update_block)
|
|
215
|
+
latest_block_bn = int(latest_block)
|
|
216
|
+
oi_long_bn = int(oi_long)
|
|
217
|
+
oi_short_bn = int(oi_short)
|
|
218
|
+
|
|
219
|
+
value_long = acc_per_oi_long_bn
|
|
220
|
+
value_short = acc_per_oi_short_bn
|
|
221
|
+
fr = 0
|
|
222
|
+
|
|
223
|
+
abs_last_funding_rate = abs(last_funding_rate_bn)
|
|
224
|
+
abs_last_velocity = abs(last_velocity_bn)
|
|
225
|
+
|
|
226
|
+
num_blocks = latest_block_bn - last_update_block_bn
|
|
227
|
+
new_funding_rate = last_funding_rate_bn + (last_velocity_bn * num_blocks)
|
|
228
|
+
abs_new_funding_rate = abs(new_funding_rate)
|
|
229
|
+
|
|
230
|
+
num_blocks_to_charge = num_blocks
|
|
231
|
+
|
|
232
|
+
accumulated_funding_rate_change = 0
|
|
233
|
+
longs_pay = False
|
|
234
|
+
funding_rate_to_use = 0
|
|
235
|
+
|
|
236
|
+
if abs_new_funding_rate > max_funding_fee_per_block_bn:
|
|
237
|
+
num_blocks_to_limit = (
|
|
238
|
+
max_funding_fee_per_block_bn - abs_last_funding_rate) // abs_last_velocity
|
|
239
|
+
|
|
240
|
+
if (new_funding_rate * last_funding_rate_bn) < 0:
|
|
241
|
+
num_blocks_to_charge = num_blocks_to_charge + \
|
|
242
|
+
(2 * last_funding_rate_bn) // last_velocity_bn
|
|
243
|
+
|
|
244
|
+
accumulated_funding_rate_change = (
|
|
245
|
+
abs_last_funding_rate +
|
|
246
|
+
(ceil_div(num_blocks_to_limit, 2) * abs_last_velocity)
|
|
247
|
+
) * num_blocks_to_limit + (
|
|
248
|
+
(num_blocks_to_charge - num_blocks_to_limit) *
|
|
249
|
+
max_funding_fee_per_block_bn
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if new_funding_rate > 0:
|
|
253
|
+
longs_pay = True
|
|
254
|
+
fr = max_funding_fee_per_block_bn
|
|
255
|
+
else:
|
|
256
|
+
longs_pay = False
|
|
257
|
+
fr = -max_funding_fee_per_block_bn
|
|
258
|
+
else:
|
|
259
|
+
funding_rate_to_use = abs_new_funding_rate if abs_new_funding_rate > abs_last_funding_rate else abs_last_funding_rate
|
|
260
|
+
|
|
261
|
+
if (last_funding_rate_bn * new_funding_rate) < 0:
|
|
262
|
+
num_blocks_to_charge = num_blocks_to_charge - \
|
|
263
|
+
(2 * funding_rate_to_use) // abs_last_velocity
|
|
264
|
+
|
|
265
|
+
longs_pay = (new_funding_rate + last_funding_rate_bn) >= 0
|
|
266
|
+
|
|
267
|
+
accumulated_funding_rate_change = (
|
|
268
|
+
funding_rate_to_use +
|
|
269
|
+
(ceil_div(num_blocks_to_charge, 2) * abs_last_velocity)
|
|
270
|
+
) * num_blocks_to_charge
|
|
271
|
+
|
|
272
|
+
fr = new_funding_rate
|
|
273
|
+
|
|
274
|
+
if longs_pay:
|
|
275
|
+
value_long = value_long + \
|
|
276
|
+
(accumulated_funding_rate_change if oi_long_bn > 0 else 0)
|
|
277
|
+
|
|
278
|
+
if oi_short_bn != 0:
|
|
279
|
+
value_short = value_short - \
|
|
280
|
+
(accumulated_funding_rate_change * oi_long_bn) // oi_short_bn
|
|
281
|
+
else:
|
|
282
|
+
value_short = value_short + \
|
|
283
|
+
(accumulated_funding_rate_change if oi_short_bn > 0 else 0)
|
|
284
|
+
|
|
285
|
+
if oi_long_bn != 0:
|
|
286
|
+
value_long = value_long - \
|
|
287
|
+
(accumulated_funding_rate_change * oi_short_bn) // oi_long_bn
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
'accFundingLong': str(value_long),
|
|
291
|
+
'accFundingShort': str(value_short),
|
|
292
|
+
'latestFundingRate': str(fr),
|
|
293
|
+
'longsPay': longs_pay,
|
|
294
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import traceback
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from .abi import usdc_abi, ostium_trading_abi, ostium_trading_storage_abi
|
|
6
|
+
from web3 import Web3
|
|
7
|
+
from .utils import convert_to_scaled_integer, fromErrorCodeToMessage, get_tp_sl_prices, to_base_units
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenOrderType(Enum):
|
|
11
|
+
MARKET = 0
|
|
12
|
+
LIMIT = 1
|
|
13
|
+
STOP = 2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Ostium:
|
|
17
|
+
def __init__(self, w3: Web3, usdc_address: str, ostium_trading_storage_address: str, ostium_trading_address: str) -> None:
|
|
18
|
+
self.web3 = w3
|
|
19
|
+
|
|
20
|
+
self.usdc_address = usdc_address
|
|
21
|
+
self.ostium_trading_storage_address = ostium_trading_storage_address
|
|
22
|
+
self.ostium_trading_address = ostium_trading_address
|
|
23
|
+
|
|
24
|
+
# Create contract instance
|
|
25
|
+
self.usdc_contract = self.web3.eth.contract(
|
|
26
|
+
address=self.usdc_address, abi=usdc_abi)
|
|
27
|
+
self.ostium_trading_storage_contract = self.web3.eth.contract(
|
|
28
|
+
address=self.ostium_trading_storage_address, abi=ostium_trading_storage_abi)
|
|
29
|
+
self.ostium_trading_contract = self.web3.eth.contract(
|
|
30
|
+
address=self.ostium_trading_address, abi=ostium_trading_abi)
|
|
31
|
+
|
|
32
|
+
def get_block_number(self):
|
|
33
|
+
return self.web3.eth.get_block('latest')['number']
|
|
34
|
+
|
|
35
|
+
def get_nonce(self, address):
|
|
36
|
+
return self.web3.eth.get_transaction_count(address)
|
|
37
|
+
|
|
38
|
+
def perform_trade(self, trade_params, pvt_key, at_price):
|
|
39
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
40
|
+
|
|
41
|
+
amount = to_base_units(trade_params['collateral'], decimals=6)
|
|
42
|
+
self.__approve(account, amount)
|
|
43
|
+
try:
|
|
44
|
+
# Log input trade parameters for verification
|
|
45
|
+
print("Final trade parameters being sent:", trade_params)
|
|
46
|
+
|
|
47
|
+
tp_price, sl_price = get_tp_sl_prices(trade_params)
|
|
48
|
+
# Define trade parameters and execute trade
|
|
49
|
+
trade = {
|
|
50
|
+
'collateral': convert_to_scaled_integer(trade_params['collateral'], precision=5, scale=6),
|
|
51
|
+
# Example open price, adjust as needed
|
|
52
|
+
'openPrice': convert_to_scaled_integer(at_price),
|
|
53
|
+
'tp': convert_to_scaled_integer(tp_price),
|
|
54
|
+
'sl': convert_to_scaled_integer(sl_price),
|
|
55
|
+
'trader': account.address,
|
|
56
|
+
'leverage': to_base_units(trade_params['leverage'], decimals=2),
|
|
57
|
+
'pairIndex': int(trade_params['asset_type']),
|
|
58
|
+
'index': 0,
|
|
59
|
+
'buy': trade_params['direction']
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
order_type = OpenOrderType.MARKET.value # assume market order
|
|
63
|
+
|
|
64
|
+
if 'order_type' in trade_params:
|
|
65
|
+
if trade_params['order_type'] == 'LIMIT':
|
|
66
|
+
order_type = OpenOrderType.LIMIT.value
|
|
67
|
+
elif trade_params['order_type'] == 'STOP':
|
|
68
|
+
order_type = OpenOrderType.STOP.value
|
|
69
|
+
elif trade_params['order_type'] == 'MARKET':
|
|
70
|
+
pass
|
|
71
|
+
else:
|
|
72
|
+
raise Exception('Invalid order type')
|
|
73
|
+
|
|
74
|
+
# Log the structured trade object to be sent
|
|
75
|
+
# print("Structured trade object:", trade)
|
|
76
|
+
|
|
77
|
+
trade_tx = self.ostium_trading_contract.functions.openTrade(
|
|
78
|
+
trade, order_type, 9000).build_transaction({'from': account.address})
|
|
79
|
+
trade_tx['nonce'] = self.get_nonce(account.address)
|
|
80
|
+
|
|
81
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
82
|
+
trade_tx, private_key=account.key)
|
|
83
|
+
trade_tx_hash = self.web3.eth.send_raw_transaction(
|
|
84
|
+
signed_tx.raw_transaction)
|
|
85
|
+
# print('Trade TX Hash:', trade_tx_hash.hex())
|
|
86
|
+
|
|
87
|
+
# Wait for the trade transaction to be mined
|
|
88
|
+
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
89
|
+
trade_tx_hash)
|
|
90
|
+
print('Trade Receipt:', trade_receipt)
|
|
91
|
+
return trade_receipt
|
|
92
|
+
except Exception as e:
|
|
93
|
+
reason_string, suggestion = fromErrorCodeToMessage(e)
|
|
94
|
+
print(
|
|
95
|
+
f"An error ({str(e)}) occurred during the trading process - parsed as {reason_string}")
|
|
96
|
+
# traceback.print_exc() # This prints the full traceback
|
|
97
|
+
raise Exception(
|
|
98
|
+
f'{reason_string}\n\n{suggestion}' if suggestion != None else reason_string)
|
|
99
|
+
|
|
100
|
+
# Cancel Limit and Stop orders
|
|
101
|
+
|
|
102
|
+
def cancel_limit_order(self, pairID, index, pvt_key):
|
|
103
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
104
|
+
|
|
105
|
+
trade_tx = self.ostium_trading_contract.functions.cancelOpenLimitOrder(
|
|
106
|
+
int(pairID), int(index)).build_transaction({'from': account.address})
|
|
107
|
+
trade_tx['nonce'] = self.get_nonce(account.address)
|
|
108
|
+
|
|
109
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
110
|
+
trade_tx, private_key=account.key)
|
|
111
|
+
trade_tx_hash = self. web3.eth.send_raw_transaction(
|
|
112
|
+
signed_tx.raw_transaction)
|
|
113
|
+
print('Cancel Limit Order TX Hash:', trade_tx_hash.hex())
|
|
114
|
+
|
|
115
|
+
# Wait for the trade transaction to be mined
|
|
116
|
+
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
117
|
+
trade_tx_hash)
|
|
118
|
+
print('Cancel Limit Order Receipt:', trade_receipt)
|
|
119
|
+
return trade_receipt
|
|
120
|
+
|
|
121
|
+
def close_trade(self, pairID, index, pvt_key):
|
|
122
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
123
|
+
|
|
124
|
+
trade_tx = self.ostium_trading_contract.functions.closeTradeMarket(
|
|
125
|
+
int(pairID), int(index)).build_transaction({'from': account.address})
|
|
126
|
+
trade_tx['nonce'] = self.get_nonce(account.address)
|
|
127
|
+
|
|
128
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
129
|
+
trade_tx, private_key=account.key)
|
|
130
|
+
trade_tx_hash = self. web3.eth.send_raw_transaction(
|
|
131
|
+
signed_tx.raw_transaction)
|
|
132
|
+
print('Trade TX Hash:', trade_tx_hash.hex())
|
|
133
|
+
|
|
134
|
+
# Wait for the trade transaction to be mined
|
|
135
|
+
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
136
|
+
trade_tx_hash)
|
|
137
|
+
print('Trade Receipt:', trade_receipt)
|
|
138
|
+
return trade_receipt
|
|
139
|
+
|
|
140
|
+
def add_collateral(self, pairID, index, collateral, pvt_key):
|
|
141
|
+
try:
|
|
142
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
143
|
+
amount = to_base_units(collateral, decimals=6)
|
|
144
|
+
self.__approve(account, amount)
|
|
145
|
+
|
|
146
|
+
add_collateral_tx = self.ostium_trading_contract.functions.topUpCollateral(
|
|
147
|
+
int(pairID), int(index), amount).build_transaction({'from': account.address})
|
|
148
|
+
add_collateral_tx['nonce'] = self.get_nonce(account.address)
|
|
149
|
+
|
|
150
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
151
|
+
add_collateral_tx, private_key=account.key)
|
|
152
|
+
add_collateral_tx_hash = self.web3.eth.send_raw_transaction(
|
|
153
|
+
signed_tx.raw_transaction)
|
|
154
|
+
print('Add Collateral TX Hash:', add_collateral_tx_hash.hex())
|
|
155
|
+
# Wait for the trade transaction to be mined
|
|
156
|
+
add_collateral_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
157
|
+
add_collateral_tx_hash)
|
|
158
|
+
print('Add Collateral Receipt:', add_collateral_receipt)
|
|
159
|
+
return add_collateral_receipt
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print("An error occurred during the add collateral process:")
|
|
163
|
+
traceback.print_exc() # This prints the full traceback
|
|
164
|
+
raise e # Optionally re-raise the exception if you want it to propagate
|
|
165
|
+
|
|
166
|
+
def update_tp(self, pairID, index, tp, pvt_key):
|
|
167
|
+
try:
|
|
168
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
169
|
+
|
|
170
|
+
tp_value = to_base_units(tp, decimals=18)
|
|
171
|
+
|
|
172
|
+
update_tp_tx = self.ostium_trading_contract.functions.updateTp(
|
|
173
|
+
int(pairID), int(index), tp_value).build_transaction({'from': account.address})
|
|
174
|
+
update_tp_tx['nonce'] = self.get_nonce(account.address)
|
|
175
|
+
|
|
176
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
177
|
+
update_tp_tx, private_key=account.key)
|
|
178
|
+
update_tp_tx_hash = self.web3.eth.send_raw_transaction(
|
|
179
|
+
signed_tx.raw_transaction)
|
|
180
|
+
print('Update TP TX Hash:', update_tp_tx_hash.hex())
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print("An error occurred during the update tp process:")
|
|
183
|
+
traceback.print_exc() # This prints the full traceback
|
|
184
|
+
raise e # Optionally re-raise the exception if you want it to propagate
|
|
185
|
+
|
|
186
|
+
def update_sl(self, pairID, index, sl, pvt_key):
|
|
187
|
+
try:
|
|
188
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
189
|
+
sl_value = to_base_units(sl, decimals=18)
|
|
190
|
+
|
|
191
|
+
update_sl_tx = self.ostium_trading_contract.functions.updateSl(
|
|
192
|
+
int(pairID), int(index), sl_value).build_transaction({'from': account.address})
|
|
193
|
+
update_sl_tx['nonce'] = self.get_nonce(account.address)
|
|
194
|
+
|
|
195
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
196
|
+
update_sl_tx, private_key=account.key)
|
|
197
|
+
update_sl_tx_hash = self.web3.eth.send_raw_transaction(
|
|
198
|
+
signed_tx.raw_transaction)
|
|
199
|
+
print('Update SL TX Hash:', update_sl_tx_hash.hex())
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(f"An error occurred during the update sl process: {e}")
|
|
202
|
+
reason_string, suggestion = fromErrorCodeToMessage(str(e))
|
|
203
|
+
print(
|
|
204
|
+
f"An error occurred during the update sl process: {reason_string}")
|
|
205
|
+
# traceback.print_exc() # This prints the full traceback
|
|
206
|
+
# Optionally re-raise the exception if you want it to propagate
|
|
207
|
+
raise Exception(
|
|
208
|
+
f'{reason_string}\n\n{suggestion}' if suggestion != None else reason_string)
|
|
209
|
+
|
|
210
|
+
def __approve(self, account, collateral):
|
|
211
|
+
# Approve the transaction
|
|
212
|
+
# Approval tx should only be done if sufficient amount is not already approved.
|
|
213
|
+
|
|
214
|
+
allowance = self.usdc_contract.functions.allowance(
|
|
215
|
+
account.address, self.ostium_trading_storage_address).call()
|
|
216
|
+
|
|
217
|
+
if allowance < collateral:
|
|
218
|
+
approve_tx = self.usdc_contract.functions.approve(
|
|
219
|
+
self.ostium_trading_storage_address,
|
|
220
|
+
self.web3.to_wei(1000000, 'mwei')
|
|
221
|
+
).build_transaction({'from': account.address})
|
|
222
|
+
|
|
223
|
+
approve_tx['nonce'] = self.get_nonce(account.address)
|
|
224
|
+
|
|
225
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
226
|
+
approve_tx, private_key=account.key)
|
|
227
|
+
approve_tx_hash = self.web3.eth.send_raw_transaction(
|
|
228
|
+
signed_tx.raw_transaction)
|
|
229
|
+
print('Approval TX Hash:', approve_tx_hash.hex())
|
|
230
|
+
|
|
231
|
+
# Ensure the approval transaction is mined
|
|
232
|
+
approve_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
233
|
+
approve_tx_hash)
|
|
234
|
+
print('Approval Receipt:', approve_receipt)
|
|
235
|
+
|
|
236
|
+
def withdraw(self, amount, receiving_address, pvt_key):
|
|
237
|
+
try:
|
|
238
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
239
|
+
amount_in_base_units = to_base_units(amount, decimals=6)
|
|
240
|
+
|
|
241
|
+
if not self.web3.is_address(receiving_address):
|
|
242
|
+
raise ValueError("Invalid Arbitrum address format")
|
|
243
|
+
|
|
244
|
+
# Use USDCs' transfer function to send funds to the receiving address
|
|
245
|
+
transfer_tx = self.usdc_contract.functions.transfer(
|
|
246
|
+
receiving_address,
|
|
247
|
+
amount_in_base_units
|
|
248
|
+
).build_transaction({'from': account.address})
|
|
249
|
+
|
|
250
|
+
transfer_tx['nonce'] = self.get_nonce(account.address)
|
|
251
|
+
|
|
252
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
253
|
+
transfer_tx, private_key=account.key)
|
|
254
|
+
transfer_tx_hash = self.web3.eth.send_raw_transaction(
|
|
255
|
+
signed_tx.raw_transaction)
|
|
256
|
+
print('Transfer TX Hash:', transfer_tx_hash.hex())
|
|
257
|
+
|
|
258
|
+
transfer_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
259
|
+
transfer_tx_hash)
|
|
260
|
+
print('Transfer Receipt:', transfer_receipt)
|
|
261
|
+
return transfer_receipt
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
reason_string, suggestion = fromErrorCodeToMessage(str(e))
|
|
265
|
+
print(
|
|
266
|
+
f"An error occurred during the transfer process: {reason_string}")
|
|
267
|
+
raise Exception(
|
|
268
|
+
f'{reason_string}\n\n{suggestion}' if suggestion != None else reason_string)
|
|
269
|
+
|
|
270
|
+
def update_limit_order(self, pair_id, index, pvt_key, price=None, tp=None, sl=None):
|
|
271
|
+
try:
|
|
272
|
+
print('update_limit_order called with pair_id', pair_id, 'index',
|
|
273
|
+
index, 'pvt_key', pvt_key, 'price', price, 'tp', tp, 'sl', sl)
|
|
274
|
+
account = self.web3.eth.account.from_key(pvt_key)
|
|
275
|
+
|
|
276
|
+
# Get existing order details (tbd why read from storage)
|
|
277
|
+
existing_order = self.ostium_trading_storage_contract.functions.getOpenLimitOrder(
|
|
278
|
+
account.address,
|
|
279
|
+
int(pair_id),
|
|
280
|
+
int(index)
|
|
281
|
+
).call()
|
|
282
|
+
|
|
283
|
+
print('existing_order', existing_order)
|
|
284
|
+
# Use existing values if new values are not provided
|
|
285
|
+
price_value = convert_to_scaled_integer(
|
|
286
|
+
price) if price is not None else existing_order[1] # openPrice
|
|
287
|
+
tp_value = convert_to_scaled_integer(
|
|
288
|
+
tp) if tp is not None else existing_order[2] # tp
|
|
289
|
+
sl_value = convert_to_scaled_integer(
|
|
290
|
+
sl) if sl is not None else existing_order[3] # sl
|
|
291
|
+
|
|
292
|
+
print('calling updateOpenLimitOrder with price_value',
|
|
293
|
+
price_value, 'tp_value', tp_value, 'sl_value', sl_value)
|
|
294
|
+
trade_tx = self.ostium_trading_contract.functions.updateOpenLimitOrder(
|
|
295
|
+
int(pair_id),
|
|
296
|
+
int(index),
|
|
297
|
+
price_value,
|
|
298
|
+
tp_value,
|
|
299
|
+
sl_value
|
|
300
|
+
).build_transaction({'from': account.address})
|
|
301
|
+
|
|
302
|
+
trade_tx['nonce'] = self.get_nonce(account.address)
|
|
303
|
+
|
|
304
|
+
signed_tx = self.web3.eth.account.sign_transaction(
|
|
305
|
+
trade_tx, private_key=account.key)
|
|
306
|
+
trade_tx_hash = self.web3.eth.send_raw_transaction(
|
|
307
|
+
signed_tx.raw_transaction)
|
|
308
|
+
print('Update Limit Order TX Hash:', trade_tx_hash.hex())
|
|
309
|
+
|
|
310
|
+
trade_receipt = self.web3.eth.wait_for_transaction_receipt(
|
|
311
|
+
trade_tx_hash)
|
|
312
|
+
print('Update Limit Order Receipt:', trade_receipt)
|
|
313
|
+
return trade_receipt
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
reason_string, suggestion = fromErrorCodeToMessage(str(e))
|
|
317
|
+
print(
|
|
318
|
+
f"An error occurred during the update limit order process: {reason_string}")
|
|
319
|
+
raise Exception(
|
|
320
|
+
f'{reason_string}\n\n{suggestion}' if suggestion != None else reason_string)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
import json
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Price:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.base_url = "https://listener.ostium.io"
|
|
9
|
+
|
|
10
|
+
async def get_latest_prices(self):
|
|
11
|
+
"""
|
|
12
|
+
Fetches the latest prices from the Ostium price listener service.
|
|
13
|
+
Returns a dictionary of price data.
|
|
14
|
+
"""
|
|
15
|
+
async with aiohttp.ClientSession() as session:
|
|
16
|
+
async with session.get(f"{self.base_url}/PricePublish/latest-prices") as response:
|
|
17
|
+
if response.status == 200:
|
|
18
|
+
return await response.json()
|
|
19
|
+
else:
|
|
20
|
+
raise Exception(
|
|
21
|
+
f"Failed to fetch prices: {response.status}")
|
|
22
|
+
|
|
23
|
+
async def get_price(self, from_asset: str, to_asset: str) -> Tuple[float, bool]:
|
|
24
|
+
"""
|
|
25
|
+
Get the latest price and market status for a specific asset pair.
|
|
26
|
+
Args:
|
|
27
|
+
from_asset: The base asset symbol (e.g., "BTC")
|
|
28
|
+
to_asset: The quote asset symbol (e.g., "USD")
|
|
29
|
+
Returns:
|
|
30
|
+
Tuple[float, bool]: The latest price and market open status for the asset pair
|
|
31
|
+
"""
|
|
32
|
+
prices = await self.get_latest_prices()
|
|
33
|
+
for price_data in prices:
|
|
34
|
+
if (price_data.get('from') == from_asset and
|
|
35
|
+
price_data.get('to') == to_asset):
|
|
36
|
+
return float(price_data.get('mid', 0)), price_data.get('isMarketOpen', False)
|
|
37
|
+
raise ValueError(f"No price found for pair: {from_asset}/{to_asset}")
|
ostium_python_sdk/sdk.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from web3 import Web3
|
|
3
|
+
from .config import NetworkConfig
|
|
4
|
+
from .ostium import Ostium
|
|
5
|
+
# from .formulae import Formulae
|
|
6
|
+
from .subgraph import SubgraphClient
|
|
7
|
+
from .price import Price
|
|
8
|
+
from .balance import Balance
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OstiumSDK:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
network_config: NetworkConfig,
|
|
15
|
+
custom_rpc_url: Optional[str] = None,
|
|
16
|
+
custom_graph_url: Optional[str] = None
|
|
17
|
+
):
|
|
18
|
+
self.config = network_config
|
|
19
|
+
self.w3 = Web3(Web3.HTTPProvider(
|
|
20
|
+
custom_rpc_url or network_config.rpc_url
|
|
21
|
+
))
|
|
22
|
+
self.ostium = Ostium(
|
|
23
|
+
self.w3,
|
|
24
|
+
network_config.contracts["usdc"],
|
|
25
|
+
network_config.contracts["trading"],
|
|
26
|
+
network_config.contracts["tradingStorage"]
|
|
27
|
+
)
|
|
28
|
+
# self.formulae = Formulae()
|
|
29
|
+
self.subgraph = SubgraphClient(
|
|
30
|
+
custom_graph_url or network_config.graph_url
|
|
31
|
+
)
|
|
32
|
+
self.price = Price()
|
|
33
|
+
self.balance = Balance(self.w3, network_config.contracts["usdc"])
|