88"""
99
1010import enum
11+ import inspect
1112import json
13+ import logging
1214import time
15+ from datetime import datetime
1316
1417from websocket import create_connection
1518from websocket ._exceptions import WebSocketConnectionClosedException
@@ -92,6 +95,34 @@ def _check_volume(volume):
9295 return volume
9396
9497
98+ def login_class_decorator (cls ):
99+ for name , method in inspect .getmembers (cls ):
100+ if hasattr (method , "_use_check_login" ):
101+ LOGGER .debug (name )
102+ def wrapper (clss , func ):
103+ clss = func .__
104+ if clss .status == STATUS .NOT_LOGGED :
105+ raise NotLogged ()
106+ try :
107+ return func
108+ except SocketError as e :
109+ LOGGER .info (
110+ "re-logging in due to LOGIN_TIMEOUT gone" )
111+ clss .login (clss ._login_data [0 ], clss ._login_data [1 ])
112+ return func
113+ #except Exception as e:
114+ # LOGGER.warning(e)
115+ #finally:
116+ # clss.login(clss._login_data[0], clss._login_data[1])
117+ # return method(clss, *args, **kwargs)
118+ setattr (cls , name , wrapper (cls , method ))
119+ return cls
120+
121+ def check_login_dec (view ):
122+ view ._use_check_login = True
123+ return view
124+
125+ @login_class_decorator
95126class BaseClient (object ):
96127 """main client class"""
97128
@@ -103,14 +134,28 @@ def __init__(self):
103134 LOGGER .debug ("BaseClient inited" )
104135 self .LOGGER = logging .getLogger ('XTBApi.api.BaseClient' )
105136
137+ def _login_decorator (self , func , * args , ** kwargs ):
138+ if self .status == STATUS .NOT_LOGGED :
139+ raise NotLogged ()
140+ try :
141+ return func (* args , ** kwargs )
142+ except SocketError as e :
143+ LOGGER .info ("re-logging in due to LOGIN_TIMEOUT gone" )
144+ self .login (self ._login_data [0 ], self ._login_data [1 ])
145+ return func (* args , ** kwargs )
146+ except Exception as e :
147+ LOGGER .warning (e )
148+ self .login (self ._login_data [0 ], self ._login_data [1 ])
149+ return func (* args , ** kwargs )
150+
106151 def _send_command (self , dict_data ):
107152 """send command to api"""
108153 time_interval = time .time () - self ._time_last_request
109154 self .LOGGER .debug ("took {} s." .format (time_interval ))
110155 if time_interval < MAX_TIME_INTERVAL :
111156 time .sleep (MAX_TIME_INTERVAL - time_interval )
112- self .ws .send (json .dumps (dict_data ))
113157 try :
158+ self .ws .send (json .dumps (dict_data ))
114159 response = self .ws .recv ()
115160 except WebSocketConnectionClosedException :
116161 raise SocketError ()
@@ -123,17 +168,14 @@ def _send_command(self, dict_data):
123168 self .LOGGER .debug (res ['returnData' ])
124169 return res ['returnData' ]
125170
126- def _check_login (self ):
127- if self .status == STATUS .NOT_LOGGED :
128- raise NotLogged ()
129- elif time .time () - self ._time_last_request >= LOGIN_TIMEOUT :
130- self .LOGGER .info ("re-logging in due to LOGIN_TIMEOUT gone" )
131- self .login (self ._login_data [0 ], self ._login_data [1 ])
171+ def _send_command_with_check (self , dict_data ):
172+ """with check login"""
173+ return self ._login_decorator (self ._send_command , dict_data )
132174
133- def login (self , user_id , password ):
175+ def login (self , user_id , password , mode = 'demo' ):
134176 """login command"""
135177 data = _get_data ("login" , userId = user_id , password = password )
136- self .ws = create_connection ("wss://ws.xapi.pro/demo " )
178+ self .ws = create_connection (f "wss://ws.xtb.com/ { mode } " )
137179 response = self ._send_command (data )
138180 self ._login_data = (user_id , password )
139181 self .status = STATUS .LOGGED
@@ -150,22 +192,19 @@ def logout(self):
150192
151193 def get_all_symbols (self ):
152194 """getAllSymbols command"""
153- self ._check_login ()
154195 data = _get_data ("getAllSymbols" )
155196 self .LOGGER .info ("CMD: get all symbols..." )
156- return self ._send_command (data )
197+ return self ._send_command_with_check (data )
157198
158199 def get_calendar (self ):
159200 """getCalendar command"""
160- self ._check_login ()
161201 data = _get_data ("getCalendar" )
162202 self .LOGGER .info ("CMD: get calendar..." )
163- return self ._send_command (data )
203+ return self ._send_command_with_check (data )
164204
165205 def get_chart_last_request (self , symbol , period , start ):
166206 """getChartLastRequest command"""
167207 _check_period (period )
168- self ._check_login ()
169208 args = {
170209 "period" : period ,
171210 "start" : start * 1000 ,
@@ -175,11 +214,10 @@ def get_chart_last_request(self, symbol, period, start):
175214 self .LOGGER .info (f"CMD: get chart last request for { symbol } of period"
176215 f" { period } from { start } ..." )
177216
178- return self ._send_command (data )
217+ return self ._send_command_with_check (data )
179218
180219 def get_chart_range_request (self , symbol , period , start , end , ticks ):
181220 """getChartRangeRequest command"""
182- _check_period (period )
183221 if not isinstance (ticks , int ):
184222 raise ValueError (f"ticks value { ticks } must be int" )
185223 self ._check_login ()
@@ -194,115 +232,111 @@ def get_chart_range_request(self, symbol, period, start, end, ticks):
194232 self .LOGGER .info (f"CMD: get chart range request for { symbol } of "
195233 f"{ period } from { start } to { end } with ticks of "
196234 f"{ ticks } ..." )
197- return self ._send_command (data )
235+ return self ._send_command_with_check (data )
198236
199237 def get_commission (self , symbol , volume ):
200238 """getCommissionDef command"""
201239 volume = _check_volume (volume )
202- self ._check_login ()
203240 data = _get_data ("getCommissionDef" , symbol = symbol , volume = volume )
204241 self .LOGGER .info (f"CMD: get commission for { symbol } of { volume } ..." )
205- return self ._send_command (data )
242+ return self ._send_command_with_check (data )
206243
207244 def get_margin_level (self ):
208245 """getMarginLevel command
209246 get margin information"""
210- self ._check_login ()
211247 data = _get_data ("getMarginLevel" )
212248 self .LOGGER .info ("CMD: get margin level..." )
213- return self ._send_command (data )
249+ return self ._send_command_with_check (data )
214250
215251 def get_margin_trade (self , symbol , volume ):
216252 """getMarginTrade command
217253 get expected margin for volumes used symbol"""
218254 volume = _check_volume (volume )
219- self ._check_login ()
220255 data = _get_data ("getMarginTrade" , symbol = symbol , volume = volume )
221256 self .LOGGER .info (f"CMD: get margin trade for { symbol } of { volume } ..." )
222- return self ._send_command (data )
257+ return self ._send_command_with_check (data )
223258
224259 def get_profit_calculation (self , symbol , mode , volume , op_price , cl_price ):
225260 """getProfitCalculation command
226261 get profit calculation for symbol with vol, mode and op, cl prices"""
227262 _check_mode (mode )
228263 volume = _check_volume (volume )
229- self ._check_login ()
230264 data = _get_data ("getProfitCalculation" , closePrice = cl_price ,
231265 cmd = mode , openPrice = op_price , symbol = symbol ,
232266 volume = volume )
233267 self .LOGGER .info (f"CMD: get profit calculation for { symbol } of "
234268 f"{ volume } from { op_price } to { cl_price } in mode "
235269 f"{ mode } ..." )
236- return self ._send_command (data )
270+ return self ._send_command_with_check (data )
237271
238272 def get_server_time (self ):
239273 """getServerTime command"""
240- self ._check_login ()
241274 data = _get_data ("getServerTime" )
242275 self .LOGGER .info ("CMD: get server time..." )
243- return self ._send_command (data )
276+ return self ._send_command_with_check (data )
244277
245278 def get_symbol (self , symbol ):
246279 """getSymbol command"""
247- self ._check_login ()
248280 data = _get_data ("getSymbol" , symbol = symbol )
249281 self .LOGGER .info (f"CMD: get symbol { symbol } ..." )
250- return self ._send_command (data )
282+ return self ._send_command_with_check (data )
251283
252284 def get_tick_prices (self , symbols , start , level = 0 ):
253285 """getTickPrices command"""
254- self ._check_login ()
255286 data = _get_data ("getTickPrices" , level = level , symbols = symbols ,
256287 timestamp = start )
257288 self .LOGGER .info (f"CMD: get tick prices of { symbols } from { start } "
258289 f"with level { level } ..." )
259- return self ._send_command (data )
290+ return self ._send_command_with_check (data )
260291
261292 def get_trade_records (self , trade_position_list ):
262293 """getTradeRecords command
263294 takes a list of position id"""
264- self ._check_login ()
265295 data = _get_data ("getTradeRecords" , orders = trade_position_list )
266296 self .LOGGER .info (f"CMD: get trade records of len "
267297 f"{ len (trade_position_list )} ..." )
268- return self ._send_command (data )
298+ return self ._send_command_with_check (data )
269299
270300 def get_trades (self , opened_only = True ):
271301 """getTrades command"""
272- self ._check_login ()
273302 data = _get_data ("getTrades" , openedOnly = opened_only )
274303 self .LOGGER .info ("CMD: get trades..." )
275- return self ._send_command (data )
304+ return self ._send_command_with_check (data )
276305
277306 def get_trades_history (self , start , end ):
278307 """getTradesHistory command
279308 can take 0 as actual time"""
280- self ._check_login ()
281309 data = _get_data ("getTradesHistory" , end = end , start = start )
282310 self .LOGGER .info (f"CMD: get trades history from { start } to { end } ..." )
283- return self ._send_command (data )
311+ return self ._send_command_with_check (data )
284312
285313 def get_trading_hours (self , trade_position_list ):
286314 """getTradingHours command"""
287- self . _check_login ()
315+ # EDITED IN ALPHA2
288316 data = _get_data ("getTradingHours" , symbols = trade_position_list )
289317 self .LOGGER .info (f"CMD: get trading hours of len "
290318 f"{ len (trade_position_list )} ..." )
291- return self ._send_command (data )
319+ response = self ._send_command_with_check (data )
320+ for symbol in response :
321+ for day in symbol ['trading' ]:
322+ day ['fromT' ] = int (day ['fromT' ] / 1000 )
323+ day ['toT' ] = int (day ['toT' ] / 1000 )
324+ for day in symbol ['quotes' ]:
325+ day ['fromT' ] = int (day ['fromT' ] / 1000 )
326+ day ['toT' ] = int (day ['toT' ] / 1000 )
327+ return response
292328
293329 def get_version (self ):
294330 """getVersion command"""
295- self ._check_login ()
296331 data = _get_data ("getVersion" )
297332 self .LOGGER .info ("CMD: get version..." )
298- return self ._send_command (data )
333+ return self ._send_command_with_check (data )
299334
300335 def ping (self ):
301336 """ping command"""
302- self ._check_login ()
303337 data = _get_data ("ping" )
304338 self .LOGGER .info ("CMD: get ping..." )
305- return self ._send_command (data )
339+ self ._send_command_with_check (data )
306340
307341 def trade_transaction (self , symbol , mode , trans_type , volume , ** kwargs ):
308342 """tradeTransaction command"""
@@ -315,7 +349,6 @@ def trade_transaction(self, symbol, mode, trans_type, volume, **kwargs):
315349 assert all ([val in accepted_values for val in kwargs .keys ()])
316350 _check_mode (mode ) # check if mode is acceptable
317351 volume = _check_volume (volume ) # check if volume is valid
318- self ._check_login () # check if logged in
319352 info = {
320353 'cmd' : mode ,
321354 'symbol' : symbol ,
@@ -330,31 +363,31 @@ def trade_transaction(self, symbol, mode, trans_type, volume, **kwargs):
330363 self .LOGGER .info (f"CMD: trade transaction of { symbol } of mode "
331364 f"{ name_of_mode } with type { name_of_type } of "
332365 f"{ volume } ..." )
333- return self ._send_command (data )
366+ return self ._send_command_with_check (data )
334367
335368 def trade_transaction_status (self , order_id ):
336369 """tradeTransactionStatus command"""
337- self ._check_login ()
338370 data = _get_data ("tradeTransactionStatus" , order = order_id )
339371 self .LOGGER .info (f"CMD: trade transaction status for { order_id } ..." )
340- return self ._send_command (data )
372+ return self ._send_command_with_check (data )
341373
342374 def get_user_data (self ):
343375 """getCurrentUserData command"""
344- self ._check_login ()
345376 data = _get_data ("getCurrentUserData" )
346377 self .LOGGER .info ("CMD: get user data..." )
347- return self ._send_command (data )
378+ return self ._send_command_with_check (data )
348379
349380
350381class Transaction (object ):
351382 def __init__ (self , trans_dict ):
352383 self ._trans_dict = trans_dict
384+ self .mode = {0 : 'buy' , 1 : 'sell' }[trans_dict ['cmd' ]]
353385 self .order_id = trans_dict ['order' ]
354386 self .symbol = trans_dict ['symbol' ]
355387 self .volume = trans_dict ['volume' ]
356388 self .price = trans_dict ['close_price' ]
357389 self .actual_profit = trans_dict ['profit' ]
390+ self .timestamp = trans_dict ['open_time' ] / 1000
358391 LOGGER .debug (f"Transaction { self .order_id } inited" )
359392
360393
@@ -366,13 +399,63 @@ def __init__(self):
366399 self .LOGGER = logging .getLogger ('XTBApi.api.Client' )
367400 self .LOGGER .info ("Client inited" )
368401
402+ def check_if_market_open (self , list_of_symbols ):
403+ """check if market is open for symbol in symbols"""
404+ _td = datetime .today ()
405+ actual_tmsp = _td .hour * 3600 + _td .minute * 60 + _td .second
406+ response = self .get_trading_hours (list_of_symbols )
407+ market_values = {}
408+ for symbol in response :
409+ today_values = [day for day in symbol ['trading' ] if day ['day' ] ==
410+ _td .isoweekday ()][0 ]
411+ if today_values ['fromT' ] <= actual_tmsp <= today_values ['toT' ]:
412+ market_values [symbol ['symbol' ]] = True
413+ else :
414+ market_values [symbol ['symbol' ]] = False
415+ return market_values
416+
417+ def get_lastn_candle_history (self , symbol , timeframe_in_seconds , number ):
418+ """get last n candles of timeframe"""
419+ acc_tmf = [60 , 300 , 900 , 1800 , 3600 , 14400 , 86400 , 604800 , 2592000 ]
420+ if timeframe_in_seconds not in acc_tmf :
421+ raise ValueError (f"timeframe not accepted, not in "
422+ f"{ ', ' .join ([str (x ) for x in acc_tmf ])} " )
423+ sec_prior = timeframe_in_seconds * number
424+ LOGGER .debug (f"sym: { symbol } , tmf: { timeframe_in_seconds } ,"
425+ f" { time .time () - sec_prior } " )
426+ res = {'rateInfos' : []}
427+ while len (res ['rateInfos' ]) < number :
428+ res = self .get_chart_last_request (symbol ,
429+ timeframe_in_seconds // 60 , time .time () - sec_prior )
430+ LOGGER .debug (res )
431+ res ['rateInfos' ] = res ['rateInfos' ][- number :]
432+ sec_prior *= 3
433+ candle_history = []
434+ for candle in res ['rateInfos' ]:
435+ _pr = candle ['open' ]
436+ op_pr = _pr / 10 ** res ['digits' ]
437+ cl_pr = (_pr + candle ['close' ]) / 10 ** res ['digits' ]
438+ hg_pr = (_pr + candle ['high' ]) / 10 ** res ['digits' ]
439+ lw_pr = (_pr + candle ['low' ]) / 10 ** res ['digits' ]
440+ new_candle_entry = {'timestamp' : candle ['ctm' ] / 1000 , 'open' :
441+ op_pr , 'close' : cl_pr , 'high' : hg_pr , 'low' : lw_pr ,
442+ 'volume' : candle ['vol' ]}
443+ candle_history .append (new_candle_entry )
444+ LOGGER .debug (candle_history )
445+ return candle_history
446+
369447 def update_trades (self ):
370448 """update trade list"""
371449 trades = self .get_trades ()
372- data = trades
373- for trade in data :
450+ self . trade_rec . clear ()
451+ for trade in trades :
374452 obj_trans = Transaction (trade )
375453 self .trade_rec [obj_trans .order_id ] = obj_trans
454+ #values_to_del = [key for key, trad_not_listed in
455+ # self.trade_rec.items() if trad_not_listed.order_id
456+ # not in [x['order'] for x in trades]]
457+ #for key in values_to_del:
458+ # del self.trade_rec[key]
376459 self .LOGGER .info (f"updated { len (self .trade_rec )} trades" )
377460 return self .trade_rec
378461
0 commit comments