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,604 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
import os
|
|
4
|
+
from humanize import naturaltime
|
|
5
|
+
from web3 import Web3
|
|
6
|
+
from ast import literal_eval
|
|
7
|
+
|
|
8
|
+
from .constants import MAX_PROFIT_P, MAX_STOP_LOSS_P
|
|
9
|
+
from .formulae import GetTakeProfitPrice
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def send_slack_message(client, from_username, message):
|
|
13
|
+
# Send a message
|
|
14
|
+
if client is not None:
|
|
15
|
+
message = f'{from_username}: {message}'
|
|
16
|
+
client.chat_postMessage(
|
|
17
|
+
channel="telegram-bot",
|
|
18
|
+
text=message,
|
|
19
|
+
username="User Feedback"
|
|
20
|
+
)
|
|
21
|
+
else:
|
|
22
|
+
print(
|
|
23
|
+
f'********* client is None, from_username: {from_username}, message: {message}')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_asset_group_name(from_asset, to_asset):
|
|
27
|
+
if from_asset in ['BTC', 'ETH', 'SOL']:
|
|
28
|
+
return 'crypto'
|
|
29
|
+
elif from_asset in ['HG', 'CL', 'XAU', 'XAG', 'XPD', 'XPT', 'NG', 'LCO']:
|
|
30
|
+
return 'commodities'
|
|
31
|
+
elif from_asset in ['SPX', 'HSI', 'NIK', 'FTS', 'DAX']:
|
|
32
|
+
return 'indices'
|
|
33
|
+
elif from_asset in ['EUR', 'GBP'] or (from_asset == 'USD' and to_asset == 'JPY'):
|
|
34
|
+
return 'forex'
|
|
35
|
+
else:
|
|
36
|
+
print('------> get_asset_group_name need to map asset asset_group_name',
|
|
37
|
+
from_asset, to_asset)
|
|
38
|
+
return ''
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# rename market_price as open_price. Pass Leverage.
|
|
42
|
+
# Get the None value here returned by calling GetTakeProfitPrice
|
|
43
|
+
# and consolidate the logic of these 2 functions to one
|
|
44
|
+
#
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_asset_change_emoji(asset_group_name, change, period_hours):
|
|
48
|
+
"""
|
|
49
|
+
Returns an emoji indicating price movement severity based on asset type and time period.
|
|
50
|
+
Different assets have different volatility profiles:
|
|
51
|
+
- Crypto: More volatile but realistic (2% in 24h is notable, 100% in 1y is extreme)
|
|
52
|
+
- Commodities: Medium volatility
|
|
53
|
+
- Forex/Indices: Lower volatility (tighter thresholds)
|
|
54
|
+
"""
|
|
55
|
+
# Define thresholds for each period and asset group (values in percentages)
|
|
56
|
+
thresholds = {
|
|
57
|
+
'crypto': {
|
|
58
|
+
1: [0.3, 0.7, 1.2, 2, 3], # 1h
|
|
59
|
+
8: [0.7, 1.5, 2.5, 4, 6], # 8h
|
|
60
|
+
24: [1, 2, 4, 6, 8], # 24h
|
|
61
|
+
365*24: [10, 25, 50, 75, 100] # 1y: More realistic yearly moves
|
|
62
|
+
},
|
|
63
|
+
'commodities': {
|
|
64
|
+
1: [0.2, 0.5, 1, 2, 3], # 1h
|
|
65
|
+
8: [0.5, 1, 2, 3, 5], # 8h
|
|
66
|
+
24: [1, 2, 3, 5, 7], # 24h
|
|
67
|
+
365*24: [10, 20, 30, 50, 70] # 1y
|
|
68
|
+
},
|
|
69
|
+
'forex': {
|
|
70
|
+
1: [0.1, 0.2, 0.3, 0.5, 1], # 1h
|
|
71
|
+
8: [0.2, 0.4, 0.6, 1, 1.5], # 8h
|
|
72
|
+
24: [0.3, 0.6, 1, 1.5, 2], # 24h
|
|
73
|
+
365*24: [5, 10, 15, 20, 25] # 1y
|
|
74
|
+
},
|
|
75
|
+
'indices': {
|
|
76
|
+
1: [0.2, 0.4, 0.6, 1, 1.5], # 1h
|
|
77
|
+
8: [0.4, 0.8, 1.2, 2, 3], # 8h
|
|
78
|
+
24: [0.8, 1.5, 2.5, 3.5, 5], # 24h
|
|
79
|
+
365*24: [8, 15, 25, 35, 45] # 1y
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if period_hours not in thresholds[asset_group_name]:
|
|
84
|
+
return ''
|
|
85
|
+
|
|
86
|
+
# Get absolute change for comparison
|
|
87
|
+
abs_change = abs(change)
|
|
88
|
+
period_thresholds = thresholds[asset_group_name][period_hours]
|
|
89
|
+
|
|
90
|
+
# Determine severity level
|
|
91
|
+
if abs_change < period_thresholds[0]:
|
|
92
|
+
emoji = '' # Minimal change
|
|
93
|
+
elif abs_change < period_thresholds[1]:
|
|
94
|
+
emoji = '📈' if change > 0 else '📉'
|
|
95
|
+
elif abs_change < period_thresholds[2]:
|
|
96
|
+
emoji = '⬆️' if change > 0 else '⬇️'
|
|
97
|
+
elif abs_change < period_thresholds[3]:
|
|
98
|
+
emoji = '🔥' if change > 0 else '💧'
|
|
99
|
+
elif abs_change < period_thresholds[4]:
|
|
100
|
+
emoji = '🚀' if change > 0 else '🌪'
|
|
101
|
+
else:
|
|
102
|
+
emoji = '⚡️' if change > 0 else '💀'
|
|
103
|
+
|
|
104
|
+
return f' {emoji}'
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_tp_sl_min_max_allowed_values(is_long: bool, market_price: Decimal, leverage: Decimal, is_tp: bool) -> tuple[Decimal, Decimal]:
|
|
108
|
+
"""
|
|
109
|
+
Returns (min_allowed, max_allowed) tuple for take profit or stop loss prices based on position direction
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
is_long: True if long position, False if short
|
|
113
|
+
market_price: Current market price
|
|
114
|
+
is_tp: True if checking take profit, False if checking stop loss
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
tp_or_sl_price = GetTakeProfitPrice(
|
|
118
|
+
is_tp, market_price, leverage, is_long, MAX_PROFIT_P if is_tp else MAX_STOP_LOSS_P)
|
|
119
|
+
if is_tp:
|
|
120
|
+
# Take profit must be above market for longs, below for shorts
|
|
121
|
+
return (
|
|
122
|
+
(Decimal(market_price), tp_or_sl_price) if is_long else
|
|
123
|
+
(tp_or_sl_price, Decimal(market_price))
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
# Stop loss must be below market for longs, above for shorts
|
|
127
|
+
return (
|
|
128
|
+
(tp_or_sl_price, Decimal(market_price)) if is_long else
|
|
129
|
+
(Decimal(market_price), tp_or_sl_price)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def is_valid_evm_address(address):
|
|
134
|
+
is_valid = Web3.is_address(address)
|
|
135
|
+
print('----->is_valid_evm_address called with',
|
|
136
|
+
address, 'and returns', is_valid)
|
|
137
|
+
return is_valid
|
|
138
|
+
|
|
139
|
+
# returns (is_valid, percentage_inputted or None is not relevant, not relevant is is_valid is False)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def is_valid_input(text, validation):
|
|
143
|
+
|
|
144
|
+
if validation is None:
|
|
145
|
+
return True, None
|
|
146
|
+
|
|
147
|
+
if 'is_address_and_none_self' in validation and validation['is_address_and_none_self']:
|
|
148
|
+
trader_address = validation['own_address']
|
|
149
|
+
return is_valid_evm_address(text) and (text).lower() != trader_address.lower(), None
|
|
150
|
+
|
|
151
|
+
# Check if it's a valid decimal number
|
|
152
|
+
if not is_valid_decimal(text):
|
|
153
|
+
if 'percentage_allowed_for' not in validation:
|
|
154
|
+
return False, None
|
|
155
|
+
else:
|
|
156
|
+
# replace only 1 occurrence of '%' so 5%% isn't valid
|
|
157
|
+
text = text.replace('%', '', 1)
|
|
158
|
+
if not is_valid_decimal(text) or Decimal(text) == 0: # cant accept 0%
|
|
159
|
+
return False, None
|
|
160
|
+
else:
|
|
161
|
+
return True, Decimal(text)
|
|
162
|
+
|
|
163
|
+
# By now, text is not specified as a percentage
|
|
164
|
+
|
|
165
|
+
# Convert to Decimal for comparison
|
|
166
|
+
value = Decimal(text)
|
|
167
|
+
|
|
168
|
+
# Special case: if zero_to_remove is present and value is 0
|
|
169
|
+
if 'zero_to_remove' in validation and validation['zero_to_remove'] and value == 0:
|
|
170
|
+
return True, False
|
|
171
|
+
|
|
172
|
+
if ('zero_to_remove' not in validation or not validation['zero_to_remove']) and value == 0:
|
|
173
|
+
return False, False
|
|
174
|
+
|
|
175
|
+
# Check minimum value if specified
|
|
176
|
+
if 'min' in validation and value < Decimal(validation['min']):
|
|
177
|
+
return False, False
|
|
178
|
+
|
|
179
|
+
# Check maximum value if specified
|
|
180
|
+
if 'max' in validation and value > Decimal(validation['max']):
|
|
181
|
+
return False, False
|
|
182
|
+
|
|
183
|
+
return True, False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_period_hours_text(period_hours):
|
|
187
|
+
if period_hours == 1:
|
|
188
|
+
return '1h'
|
|
189
|
+
elif period_hours == 8:
|
|
190
|
+
return '8h'
|
|
191
|
+
elif period_hours == 24:
|
|
192
|
+
return '24h'
|
|
193
|
+
elif period_hours == 365*24:
|
|
194
|
+
return '1y'
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def build_asset_callback_data(asset_id, period_hours=None):
|
|
198
|
+
return f'chooseAsset|{asset_id}|{period_hours if period_hours else "24"}'
|
|
199
|
+
|
|
200
|
+
# period_hours is 1, 8, 24, 8760 (365*24)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def parse_performances(performances, period_hours):
|
|
204
|
+
try:
|
|
205
|
+
if performances:
|
|
206
|
+
if period_hours == 24:
|
|
207
|
+
low = Decimal(performances['low24h'])
|
|
208
|
+
high = Decimal(performances['high24h'])
|
|
209
|
+
change = Decimal(performances['24hChange'])
|
|
210
|
+
elif period_hours == 8:
|
|
211
|
+
low = Decimal(performances['low8h'])
|
|
212
|
+
high = Decimal(performances['high8h'])
|
|
213
|
+
change = Decimal(performances['8hChange'])
|
|
214
|
+
elif period_hours == 1:
|
|
215
|
+
low = Decimal(performances['low1h'])
|
|
216
|
+
high = Decimal(performances['high1h'])
|
|
217
|
+
change = Decimal(performances['1hChange'])
|
|
218
|
+
elif period_hours == 365*24:
|
|
219
|
+
low = Decimal(performances['low1y']
|
|
220
|
+
) if 'low1y' in performances else None
|
|
221
|
+
high = Decimal(performances['high1y']
|
|
222
|
+
) if 'high1y' in performances else None
|
|
223
|
+
change = Decimal(
|
|
224
|
+
performances['1yChange']) if '1yChange' in performances else None
|
|
225
|
+
else:
|
|
226
|
+
low = None
|
|
227
|
+
high = None
|
|
228
|
+
change = None
|
|
229
|
+
|
|
230
|
+
return low, high, change
|
|
231
|
+
else:
|
|
232
|
+
return Decimal(0), Decimal(0), Decimal(0)
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
return Decimal(0), Decimal(0), Decimal(0)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_tp_sl_prices(trade_params):
|
|
239
|
+
tp_price = 0
|
|
240
|
+
sl_price = 0
|
|
241
|
+
|
|
242
|
+
if 'tp' in trade_params and str(trade_params['tp']) != '0':
|
|
243
|
+
tp_price = float(trade_params['tp'])
|
|
244
|
+
|
|
245
|
+
if 'sl' in trade_params and str(trade_params['sl']) != '0':
|
|
246
|
+
sl_price = float(trade_params['sl'])
|
|
247
|
+
|
|
248
|
+
return tp_price, sl_price
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_oi_usage_emoji(oi, max_oi_cap):
|
|
252
|
+
oi_percent = oi/max_oi_cap*100
|
|
253
|
+
if oi_percent >= 100:
|
|
254
|
+
return '🚫'
|
|
255
|
+
elif oi_percent > 80:
|
|
256
|
+
return '🔥🔥🔥'
|
|
257
|
+
elif oi_percent > 50:
|
|
258
|
+
return '🔥🔥'
|
|
259
|
+
elif oi_percent > 40:
|
|
260
|
+
return '🔥'
|
|
261
|
+
else:
|
|
262
|
+
return ''
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_validation_text(validation_dict):
|
|
266
|
+
if 'min' in validation_dict and 'max' in validation_dict:
|
|
267
|
+
return f"between {validation_dict['min']} and {validation_dict['max']}"
|
|
268
|
+
else:
|
|
269
|
+
if 'min' in validation_dict and 'max' not in validation_dict:
|
|
270
|
+
return f"equal or above {validation_dict['min']}"
|
|
271
|
+
elif 'max' in validation_dict and 'min' not in validation_dict:
|
|
272
|
+
return f"equal or below {validation_dict['max']}"
|
|
273
|
+
else:
|
|
274
|
+
return ''
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def format_awaiting_input_text(text, validation_dict, market_mid_price=None):
|
|
278
|
+
if 'min' in validation_dict and 'max' in validation_dict:
|
|
279
|
+
from_to_str = f"range:\n{validation_dict['min']} - {validation_dict['max']}"
|
|
280
|
+
else:
|
|
281
|
+
if 'min' in validation_dict and 'max' not in validation_dict:
|
|
282
|
+
from_to_str = f"min. {validation_dict['min']}"
|
|
283
|
+
elif 'max' in validation_dict and 'min' not in validation_dict:
|
|
284
|
+
from_to_str = f"max. {validation_dict['max']}"
|
|
285
|
+
else:
|
|
286
|
+
from_to_str = ''
|
|
287
|
+
|
|
288
|
+
note = f"\nOr 0 to remove" if 'zero_to_remove' in validation_dict and validation_dict[
|
|
289
|
+
'zero_to_remove'] else ''
|
|
290
|
+
|
|
291
|
+
tp_sl_examples = ""
|
|
292
|
+
if 'percentage_allowed_for' in validation_dict:
|
|
293
|
+
purpose = validation_dict['percentage_allowed_for']
|
|
294
|
+
percentages = [25, 50, 75, 100, 500,
|
|
295
|
+
900] if purpose == "tp" else [5, 10, 25, 50, 85]
|
|
296
|
+
|
|
297
|
+
tp_sl_examples = f"\n\nOr specify {purpose.title()} as a percentage, i.e:\n"
|
|
298
|
+
for percent in percentages:
|
|
299
|
+
price = GetTakeProfitPrice(
|
|
300
|
+
is_tp=(purpose == "tp"),
|
|
301
|
+
open_price=validation_dict['metadata']['open_price'],
|
|
302
|
+
leverage=validation_dict['metadata']['leverage'],
|
|
303
|
+
long=validation_dict['metadata']['is_long'],
|
|
304
|
+
profit_p=percent
|
|
305
|
+
)
|
|
306
|
+
tp_sl_examples += f"{percent}% 👉 ${format_with_precision(price, precision=5)}\n"
|
|
307
|
+
|
|
308
|
+
return f'{text}\n\n{from_to_str}{tp_sl_examples}{note}'
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_profit_emoji(profit_percent):
|
|
312
|
+
if profit_percent >= 80:
|
|
313
|
+
return '💰💰'
|
|
314
|
+
elif profit_percent >= 60:
|
|
315
|
+
return '💰'
|
|
316
|
+
elif profit_percent >= 50:
|
|
317
|
+
return '🚀'
|
|
318
|
+
elif profit_percent >= 20:
|
|
319
|
+
return '🔥'
|
|
320
|
+
elif profit_percent > -3:
|
|
321
|
+
return ''
|
|
322
|
+
elif profit_percent >= -5:
|
|
323
|
+
return '🤨'
|
|
324
|
+
elif profit_percent >= -20:
|
|
325
|
+
return '😟'
|
|
326
|
+
elif profit_percent >= -25:
|
|
327
|
+
return '😲'
|
|
328
|
+
elif profit_percent >= -30:
|
|
329
|
+
return '😢'
|
|
330
|
+
elif profit_percent >= -40:
|
|
331
|
+
return '😱'
|
|
332
|
+
elif profit_percent >= -50:
|
|
333
|
+
return '💣'
|
|
334
|
+
else:
|
|
335
|
+
return '💀' # Skull for severe losses
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def get_oi_in_usd(oi, price):
|
|
339
|
+
return format_with_precision(Web3.from_wei(int(oi) * price, 'ether'), precision=2)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def get_oi_state(pair_details, mid_price):
|
|
343
|
+
long_oi = get_oi_in_usd(pair_details['longOI'], mid_price)
|
|
344
|
+
short_oi = get_oi_in_usd(pair_details['shortOI'], mid_price)
|
|
345
|
+
|
|
346
|
+
max_oi_cap = format_with_precision(Web3.from_wei(
|
|
347
|
+
int(pair_details['maxOI']), 'mwei'), precision=2)
|
|
348
|
+
|
|
349
|
+
return Decimal(long_oi), Decimal(short_oi), Decimal(max_oi_cap)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def format_with_precision(number, precision):
|
|
353
|
+
"""
|
|
354
|
+
Formats a number to a specified decimal precision, removing trailing zeros.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
number: The number to be formatted (can be int, float, or numeric string)
|
|
358
|
+
precision (int): Maximum number of decimal places to round to
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
str: Formatted number with up to specified decimal precision, trailing zeros removed
|
|
362
|
+
"""
|
|
363
|
+
try:
|
|
364
|
+
if callable(number):
|
|
365
|
+
raise TypeError("Input cannot be a function")
|
|
366
|
+
|
|
367
|
+
float_number = float(number)
|
|
368
|
+
precision = int(precision)
|
|
369
|
+
|
|
370
|
+
# Format with specified precision first
|
|
371
|
+
formatted = "{:.{}f}".format(round(float_number, precision), precision)
|
|
372
|
+
# Remove trailing zeros after decimal point, but keep at least one digit before decimal
|
|
373
|
+
formatted = formatted.rstrip('0').rstrip(
|
|
374
|
+
'.') if '.' in formatted else formatted
|
|
375
|
+
|
|
376
|
+
return float(formatted)
|
|
377
|
+
|
|
378
|
+
except (TypeError, ValueError) as e:
|
|
379
|
+
raise TypeError(f"Invalid input: {e}")
|
|
380
|
+
|
|
381
|
+
# my_time is datetime.fromtimestamp
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def format_time_ago(my_time):
|
|
385
|
+
ago = naturaltime(datetime.now() - my_time).replace('minutes',
|
|
386
|
+
'm').replace('seconds', 's').replace('hours', 'h').replace('days', 'd')
|
|
387
|
+
return f'{my_time.strftime("%H:%M %a %b %-d")} ({ago})'
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def get_order_details(order_details):
|
|
391
|
+
open_price = Web3.from_wei(int(order_details['openPrice']), 'ether')
|
|
392
|
+
|
|
393
|
+
limit_order_created_time = datetime.fromtimestamp(
|
|
394
|
+
int(order_details['initiatedAt']))
|
|
395
|
+
|
|
396
|
+
leverage = Web3.from_wei(int(order_details['leverage']), 'kwei')*10
|
|
397
|
+
collateral = Web3.from_wei(int(order_details['collateral']), 'mwei')
|
|
398
|
+
is_long = order_details['isBuy']
|
|
399
|
+
limit_type = order_details['limitType']
|
|
400
|
+
pairIndex, index = parse_limit_order_id(order_details['id'])
|
|
401
|
+
|
|
402
|
+
sl_price = Web3.from_wei(int(order_details['stopLossPrice']), 'ether')
|
|
403
|
+
tp_price = Web3.from_wei(int(order_details['takeProfitPrice']), 'ether')
|
|
404
|
+
|
|
405
|
+
atLeastTradeNotional = 0
|
|
406
|
+
if open_price:
|
|
407
|
+
atLeastTradeNotional = collateral*leverage/open_price
|
|
408
|
+
|
|
409
|
+
return limit_type, sl_price, tp_price, open_price, leverage, limit_order_created_time, collateral, pairIndex, index, is_long, atLeastTradeNotional
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_trade_details(trade_details):
|
|
413
|
+
# print('\n\nget_trade_details called with trade_details\n\n', trade_details, '\n\n')
|
|
414
|
+
open_price = Web3.from_wei(int(trade_details['openPrice']), 'ether')
|
|
415
|
+
sl_price = Web3.from_wei(int(trade_details['stopLossPrice']), 'ether')
|
|
416
|
+
tp_price = Web3.from_wei(int(trade_details['takeProfitPrice']), 'ether')
|
|
417
|
+
tradeNotional = Web3.from_wei(int(trade_details['tradeNotional']), 'ether')
|
|
418
|
+
trans_time = datetime.fromtimestamp(int(trade_details['timestamp']))
|
|
419
|
+
leverage = Web3.from_wei(int(trade_details['leverage']), 'kwei')*10
|
|
420
|
+
collateral = Web3.from_wei(int(trade_details['collateral']), 'mwei')
|
|
421
|
+
is_long = trade_details['isBuy']
|
|
422
|
+
pairIndex = trade_details['pair']['id']
|
|
423
|
+
index = trade_details['index']
|
|
424
|
+
|
|
425
|
+
return open_price, round(tradeNotional, 18), trans_time, leverage, collateral, pairIndex, index, is_long, sl_price, tp_price
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def parse_limit_order_id(limit_order_id):
|
|
429
|
+
# 0x3750a14869d419f1069cbf7cbe47a89b2dc1d4c4_0_0
|
|
430
|
+
trader, pairIndex, index = limit_order_id.split('_')
|
|
431
|
+
|
|
432
|
+
return pairIndex, index
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def is_numeric(value):
|
|
436
|
+
try:
|
|
437
|
+
float(value)
|
|
438
|
+
return True
|
|
439
|
+
except ValueError:
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def fromErrorCodeToMessage(error_code):
|
|
444
|
+
# ----->fromErrorCodeToMessage(error_code) called with ('execution reverted: ERC20: transfer amount exceeds balance', '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000')
|
|
445
|
+
# ----->Did't find the error, so returning - fromErrorCodeToMessage(error_code) returns Unknown error (missing in error_map?)
|
|
446
|
+
|
|
447
|
+
print('----->fromErrorCodeToMessage(error_code) called with', str(error_code))
|
|
448
|
+
|
|
449
|
+
# Create reverse mapping of hash -> error name
|
|
450
|
+
error_map = {
|
|
451
|
+
"80a71fc5": "AboveMaxAllowedCollateral()",
|
|
452
|
+
"f77a8069": "AlreadyMarketClosed(address,uint16,uint8)",
|
|
453
|
+
"eca695e1": "BelowMinLevPos()",
|
|
454
|
+
"5be5878a": "DelegatedActionFailed()",
|
|
455
|
+
"46c4ede2": "ExposureLimits()",
|
|
456
|
+
"4f285592": "IsContract(address)",
|
|
457
|
+
"084986e7": "IsDone()",
|
|
458
|
+
"1309a563": "IsPaused()",
|
|
459
|
+
"5c12ea62": "MaxPendingMarketOrdersReached(address)",
|
|
460
|
+
"e6f47fab": "MaxTradesPerPairReached(address,uint16)",
|
|
461
|
+
"2a917859": "NoDelegate(address)",
|
|
462
|
+
"a35ee470": "NoLimitFound(address,uint16,uint8)",
|
|
463
|
+
"17e08e97": "NoTradeFound(address,uint16,uint8)",
|
|
464
|
+
"efa9e5be": "NoTradeToTimeoutFound(uint256)",
|
|
465
|
+
"c7fe4d00": "NotCloseMarketTimeoutOrder(uint256)",
|
|
466
|
+
"502b946d": "NotDelegate(address,address)",
|
|
467
|
+
"093650d5": "NotGov(address)",
|
|
468
|
+
"1add0915": "NotOpenMarketTimeoutOrder(uint256)",
|
|
469
|
+
"432b6c83": "NotTradesUpKeep(address)",
|
|
470
|
+
"df17e316": "NotWhitelisted(address)",
|
|
471
|
+
"5ac89f62": "NotYourOrder(uint256,address)",
|
|
472
|
+
"f3d0b126": "NullAddr()",
|
|
473
|
+
"cb87b762": "PairNotListed(uint16)",
|
|
474
|
+
"dd9397bb": "TriggerPending(address,uint16,uint8)",
|
|
475
|
+
"3e0b1869": "WaitTimeout(uint256)",
|
|
476
|
+
"35fe85c5": "WrongLeverage(uint32)",
|
|
477
|
+
"5863f789": "WrongParams()",
|
|
478
|
+
"083fbd78": "WrongSL()",
|
|
479
|
+
"a41bb918": "WrongTP()"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# Search for any of the known error hashes within the error_code string
|
|
483
|
+
for hash_code, error_message in error_map.items():
|
|
484
|
+
if hash_code in str(error_code):
|
|
485
|
+
ret = error_message
|
|
486
|
+
print('----->fromErrorCodeToMessage(error_code) returns', ret)
|
|
487
|
+
return str(ret), None
|
|
488
|
+
|
|
489
|
+
# If we couldn't find the error in error_map, try to parse the error
|
|
490
|
+
try:
|
|
491
|
+
# Convert string representation to dictionary if needed
|
|
492
|
+
error_dict = error_code if isinstance(
|
|
493
|
+
error_code, dict) else literal_eval(str(error_code))
|
|
494
|
+
if isinstance(error_dict, dict) and 'message' in error_dict:
|
|
495
|
+
if 'insufficient funds for gas * price + value' in error_dict['message']:
|
|
496
|
+
suggestion = 'Please top up your account with more ETH'
|
|
497
|
+
return error_dict["message"], suggestion
|
|
498
|
+
else:
|
|
499
|
+
return error_dict['message'], None
|
|
500
|
+
except (ValueError, SyntaxError):
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
if 'execution reverted: ERC20: transfer amount exceeds balance' in str(error_code):
|
|
504
|
+
suggestion = 'Please top up your account with more USDC'
|
|
505
|
+
return 'execution reverted: ERC20: transfer amount exceeds balance', suggestion
|
|
506
|
+
|
|
507
|
+
ret = 'Unknown error (missing in error_map?)'
|
|
508
|
+
print('----->Did\'t find the error, so returning - fromErrorCodeToMessage(error_code) returns', ret)
|
|
509
|
+
return str(ret), None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# def is_production():
|
|
513
|
+
# return 'sepolia' not in (os.getenv('RPC_PROVIDER')).lower()
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def get_arbiscan_transaction_url(transaction_hash):
|
|
517
|
+
if (is_production()):
|
|
518
|
+
return f'https://arbiscan.io/tx/0x{transaction_hash}'
|
|
519
|
+
else:
|
|
520
|
+
return f'https://sepolia.arbiscan.io/tx/0x{transaction_hash}'
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def get_max_leverage_and_min_leverage_position(pair_details):
|
|
524
|
+
max_leverage = int(pair_details['maxLeverage']) / 100
|
|
525
|
+
min_leverage_position = int(pair_details['fee']['minLevPos']) / 10 ** 6
|
|
526
|
+
|
|
527
|
+
if (max_leverage == 0):
|
|
528
|
+
max_leverage = int(pair_details['group']['maxLeverage']) / 100
|
|
529
|
+
|
|
530
|
+
return int(max_leverage), int(min_leverage_position)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def to_base_units(amount: float, decimals: int = 6) -> int:
|
|
534
|
+
"""
|
|
535
|
+
Converts a decimal number to base units by multiplying by 10^decimals
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
amount (float): The amount to convert (e.g., 1.23)
|
|
539
|
+
decimals (int, optional): Number of decimal places. Defaults to 6 for USDC.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
int: The amount in base units (e.g., 1.23 -> 1230000 for decimals=6)
|
|
543
|
+
"""
|
|
544
|
+
return int(float(amount) * 10**decimals)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def convert_to_scaled_integer(value, precision=5, scale=18):
|
|
548
|
+
# First scale to the precision we want to preserve (e.g., 5 decimal places)
|
|
549
|
+
precise_value = round(Decimal(value) * (10 ** precision))
|
|
550
|
+
# Then pad with zeros to reach 18 decimals
|
|
551
|
+
scaled_value = precise_value * (10 ** (scale - precision))
|
|
552
|
+
return scaled_value
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def is_valid_decimal(s, must_be_positive=True):
|
|
556
|
+
try:
|
|
557
|
+
float(s)
|
|
558
|
+
except ValueError:
|
|
559
|
+
return False
|
|
560
|
+
else:
|
|
561
|
+
if must_be_positive and float(s) < 0:
|
|
562
|
+
return False
|
|
563
|
+
return True
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def format_available_balance(balance_of_ether, usdc_balance):
|
|
567
|
+
eth_warning = ' ✖️ <i>gas</i>' if Decimal(
|
|
568
|
+
balance_of_ether) < Decimal('0.00015') else ''
|
|
569
|
+
return f'Available: {format_with_precision(usdc_balance, precision=2)} USDC, {format_with_precision(balance_of_ether, precision=5)} ETH{eth_warning}'
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def format_current_portfolio(total_open_trades_net_value, usdc_balance):
|
|
573
|
+
return f'Wallet worth: <b>{format_with_precision(Decimal(total_open_trades_net_value) + Decimal(usdc_balance), precision=2)} USDC</b>'
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def get_asset_name(from_asset, to_asset):
|
|
577
|
+
asset = f'{from_asset}/{to_asset}'
|
|
578
|
+
switch = {
|
|
579
|
+
'BTC/USD': 'Bitcoin',
|
|
580
|
+
'ETH/USD': 'Ethereum',
|
|
581
|
+
'SOL/USD': 'Solana',
|
|
582
|
+
'XAU/USD': 'Gold',
|
|
583
|
+
'XAG/USD': 'Silver',
|
|
584
|
+
'EUR/USD': 'Euro',
|
|
585
|
+
'GBP/USD': 'Pound',
|
|
586
|
+
'USD/JPY': 'Yen',
|
|
587
|
+
'CL/USD': 'Crude Oil',
|
|
588
|
+
'NG/USD': 'Natural Gas',
|
|
589
|
+
'HG/USD': 'Copper',
|
|
590
|
+
'SPX/USD': 'S&P 500',
|
|
591
|
+
}
|
|
592
|
+
return switch.get(asset, asset)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def convert_decimals(obj):
|
|
596
|
+
if isinstance(obj, dict):
|
|
597
|
+
return {key: convert_decimals(value) for key, value in obj.items()}
|
|
598
|
+
elif isinstance(obj, list):
|
|
599
|
+
return [convert_decimals(item) for item in obj]
|
|
600
|
+
elif isinstance(obj, Decimal):
|
|
601
|
+
return str(obj) # or float(obj) if you prefer
|
|
602
|
+
return obj
|
|
603
|
+
|
|
604
|
+
# timestamp is a string in seconds as returned from graph
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ostium-python-sdk
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A python based SDK developed for interacting with Ostium, a leveraged trading application for trading currencies, commodities, indices, crypto and more.
|
|
5
|
+
Home-page: https://github.com/0xOstium/ostium-python-sdk
|
|
6
|
+
Author: ami@ostium.io
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: web3>=6.0.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Ostium Python SDK
|
|
18
|
+
|
|
19
|
+
A python based SDK developed for interacting with Ostium v1 Trading Platform (https://ostium.app/)
|
|
20
|
+
|
|
21
|
+
Ostium is a decentralized perpetuals exchange on Arbitrum (Ethereum L2) with a focus on providing a seamless experience for traders for trading currencies, commodities, indices, crypto and more.
|
|
22
|
+
|
|
23
|
+
This SDK is designed to be used by developers who want to build applications on top of Ostium and automate their trading strategies.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Pip Install
|
|
27
|
+
|
|
28
|
+
The SDK can be installed via pip:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install ostium-python-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
Developed using:
|
|
37
|
+
```python
|
|
38
|
+
python=3.8
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Example Usage Script
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Read Block Number
|
|
45
|
+
|
|
46
|
+
To run the example:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python examples/example-read-block-number.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
See [example-read-block-number.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-read-block-number.py) for an example of how to use the SDK.
|
|
53
|
+
|
|
54
|
+
### Read Positions
|
|
55
|
+
|
|
56
|
+
To run the example:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python examples/example-read-positions.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [example-read-positions.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-read-positions.py) for an example of how to use the SDK.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
### Get Feed Prices
|
|
66
|
+
|
|
67
|
+
To open a trade you need the latest feed price.
|
|
68
|
+
|
|
69
|
+
See this example script on how to get the latest feed prices.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python examples/example-get-prices.py
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
See [example-get-prices.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-get-prices.py) for an example of how to use the SDK.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
### Get Balance of an Address
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
See this example script on how to get the latest feed prices.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python examples/example-get-balance.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [example-get-balance.py](https://github.com/0xOstium/ostium_python_sdk/blob/main/examples/example-get-balance.py) for an example of how to use the SDK.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|