bitunix-automated-crypto-trading 2.6.7__py3-none-any.whl → 2.6.8__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.
Files changed (31) hide show
  1. bitunix_automated_crypto_trading/AsyncThreadRunner.py +81 -81
  2. bitunix_automated_crypto_trading/BitunixApi.py +278 -278
  3. bitunix_automated_crypto_trading/BitunixSignal.py +1099 -1099
  4. bitunix_automated_crypto_trading/BitunixWebSocket.py +254 -254
  5. bitunix_automated_crypto_trading/DataFrameHtmlRenderer.py +74 -74
  6. bitunix_automated_crypto_trading/NotificationManager.py +23 -23
  7. bitunix_automated_crypto_trading/ThreadManager.py +68 -68
  8. bitunix_automated_crypto_trading/TickerManager.py +635 -635
  9. bitunix_automated_crypto_trading/bitunix.py +597 -594
  10. bitunix_automated_crypto_trading/config.py +90 -90
  11. bitunix_automated_crypto_trading/logger.py +84 -84
  12. {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/METADATA +36 -36
  13. bitunix_automated_crypto_trading-2.6.8.dist-info/RECORD +17 -0
  14. bitunix_automated_crypto_trading/config.txt +0 -60
  15. bitunix_automated_crypto_trading/sampleenv.txt +0 -5
  16. bitunix_automated_crypto_trading/static/chart.css +0 -28
  17. bitunix_automated_crypto_trading/static/chart.js +0 -362
  18. bitunix_automated_crypto_trading/static/modal.css +0 -68
  19. bitunix_automated_crypto_trading/static/modal.js +0 -147
  20. bitunix_automated_crypto_trading/static/script.js +0 -166
  21. bitunix_automated_crypto_trading/static/styles.css +0 -118
  22. bitunix_automated_crypto_trading/templates/charts.html +0 -98
  23. bitunix_automated_crypto_trading/templates/login.html +0 -19
  24. bitunix_automated_crypto_trading/templates/main.html +0 -551
  25. bitunix_automated_crypto_trading/templates/modal-chart.html +0 -26
  26. bitunix_automated_crypto_trading/templates/modal-config.html +0 -34
  27. bitunix_automated_crypto_trading/templates/modal-logs.html +0 -15
  28. bitunix_automated_crypto_trading-2.6.7.dist-info/RECORD +0 -31
  29. {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/WHEEL +0 -0
  30. {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/entry_points.txt +0 -0
  31. {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/top_level.txt +0 -0
@@ -1,594 +1,597 @@
1
- import asyncio
2
- import os
3
- import uvicorn
4
- import numpy as np
5
- import pandas as pd
6
- import json
7
- import time
8
- from datetime import datetime
9
- import pytz
10
-
11
- from .ThreadManager import ThreadManager
12
- from .BitunixApi import BitunixApi
13
- from .BitunixSignal import BitunixSignal
14
- from .NotificationManager import NotificationManager
15
- from .DataFrameHtmlRenderer import DataFrameHtmlRenderer
16
- from .config import Settings
17
- from .logger import Logger
18
- logger = Logger(__name__).get_logger()
19
-
20
- from fastapi import FastAPI, Request, Form, WebSocket, WebSocketDisconnect, Depends, Query
21
- from fastapi.responses import HTMLResponse , JSONResponse, RedirectResponse, StreamingResponse
22
- from fastapi.templating import Jinja2Templates
23
-
24
- from fastapi.security import OAuth2PasswordRequestForm
25
- from fastapi_login import LoginManager
26
- from fastapi_login.exceptions import InvalidCredentialsException
27
- from fastapi.middleware.cors import CORSMiddleware
28
- from fastapi.staticfiles import StaticFiles
29
-
30
- from dotenv import load_dotenv, dotenv_values, set_key
31
- from pydantic import ValidationError
32
-
33
- ENV_FILE = ".env"
34
- CONFIG_FILE = "./config.txt"
35
- LOG_FILE = "app.log"
36
-
37
- #load environment variables
38
- load_dotenv(ENV_FILE)
39
- API_KEY = os.getenv('API_KEY')
40
- SECRET_KEY = os.getenv('SECRET_KEY')
41
- SECRET = os.getenv('SECRET')
42
- PASSWORD = os.getenv('PASSWORD')
43
- HOST = os.getenv('HOST')
44
-
45
- #load config variables using setting class in config.py validating using pydantic
46
- settings = Settings()
47
-
48
- class bitunix():
49
- def __init__(self, password, api_key, secret_key, settings):
50
- self.screen_refresh_interval =settings.SCREEN_REFRESH_INTERVAL
51
-
52
- self.autoTrade=settings.AUTOTRADE
53
-
54
- self.threadManager = ThreadManager()
55
- self.notifications = NotificationManager()
56
- self.bitunixApi = BitunixApi(api_key, secret_key, settings)
57
- self.bitunixSignal = BitunixSignal(api_key, secret_key, settings, self.threadManager, self.notifications, self.bitunixApi)
58
-
59
- self.websocket_connections = set()
60
- self.DB = {"admin": {"password": password}}
61
-
62
- async def update_settings(self, settings):
63
- self.settings = settings
64
- await self.bitunixSignal.update_settings(settings)
65
- await self.bitunixApi.update_settings(settings)
66
-
67
- async def start(self):
68
- await asyncio.create_task(self.bitunixSignal.load_tickers())
69
- await asyncio.create_task(self.bitunixSignal.start_jobs())
70
-
71
- async def restart(self):
72
- await asyncio.create_task(self.bitunixSignal.restart_jobs())
73
-
74
- async def send_message_to_websocket(self,message):
75
- async def send_to_all():
76
- for ws in self.bitunixSignal.websocket_connections:
77
- await ws.send_text(message)
78
- asyncio.run(send_to_all())
79
-
80
- async def send_async_message_to_websocket(self,message):
81
- for ws in self.bitunixSignal.websocket_connections:
82
- await ws.send_text(message)
83
-
84
- app = FastAPI()
85
- app.mount("/static", StaticFiles(directory=os.path.abspath("./static")), name="static")
86
-
87
- app.add_middleware(
88
- CORSMiddleware,
89
- allow_origins=["*"], # Adjust this for production
90
- allow_credentials=True,
91
- allow_methods=["*"],
92
- allow_headers=["*"],
93
- )
94
- templates = Jinja2Templates(directory="./templates")
95
- SECRET=os.getenv('SECRET')
96
- login_manager = LoginManager(SECRET, token_url="/auth/login", use_cookie=True)
97
- login_manager.cookie_name = "auth_token"
98
-
99
-
100
- @app.post("/auth/login")
101
- async def login(data: OAuth2PasswordRequestForm = Depends()):
102
- start=time.time()
103
- username = data.username
104
- password = data.password
105
-
106
- user = load_user(username, some_callable_object)
107
- if not user or password != user["password"]:
108
- raise InvalidCredentialsException
109
- access_token = login_manager.create_access_token(data={"sub": username})
110
- response = RedirectResponse(url="/main", status_code=302)
111
- login_manager.set_cookie(response, access_token)
112
- logger.info(f"/auth/login: elapsed time {time.time()-start}")
113
- return response
114
-
115
- def some_callable_object():
116
- logger.info("called")
117
-
118
- @login_manager.user_loader(some_callable=some_callable_object)
119
- def load_user(username, some_callable=some_callable_object):
120
- user = bitunix.DB.get(username)
121
- return user
122
-
123
- @app.get("/private")
124
- async def get_private_endpoint(user=Depends(login_manager)):
125
- return "You are an authenticated user"
126
-
127
- @app.on_event("startup")
128
- async def startup_event():
129
- await asyncio.create_task(get_server_states(app, settings))
130
- await bitunix.start()
131
-
132
- #load inital states for html
133
- async def get_server_states(app, settings):
134
- app.state.element_states = {}
135
- app.state.element_states['autoTrade']=settings.AUTOTRADE
136
- app.state.element_states['optionMovingAverage']=settings.OPTION_MOVING_AVERAGE
137
- app.state.element_states['maxAutoTrades']=settings.MAX_AUTO_TRADES
138
-
139
- @app.post("/reload")
140
- async def refresh_detected():
141
- bitunix.bitunixSignal.notifications.add_notification("Reload detected!")
142
- await asyncio.create_task(bitunix.restart())
143
-
144
- @app.post("/save_states")
145
- async def save_states(states: dict):
146
- app.state.element_states.update(states)
147
- return {"message": "States saved"}
148
-
149
- @app.post("/get_states")
150
- async def get_states(payload: dict):
151
- element_ids = payload.get("element_ids", [])
152
- states = {element_id: app.state.element_states.get(element_id, "No state found") for element_id in element_ids}
153
- return {"states": states}
154
-
155
- async def set_server_states():
156
- settings.AUTOTRADE = True if app.state.element_states['autoTrade']=='true' else False
157
-
158
- if settings.AUTOTRADE:
159
- settings.OPTION_MOVING_AVERAGE = app.state.element_states['optionMovingAverage']
160
- settings.notifications.add_notification(f"optionMovingAverage: {settings.OPTION_MOVING_AVERAGE}")
161
-
162
- settings.PROFIT_AMOUNT = app.state.element_states['profitAmount']
163
- settings.notifications.add_notification(f"profitAmount: {settings.PROFIT_AMOUNT}")
164
-
165
- settings.LOSS_AMOUNT = app.state.element_states['lossAmount']
166
- settings.notifications.add_notification(f"lossAmount: {settings.LOSS_AMOUNT}")
167
-
168
- settings.MAX_AUTO_TRADES = app.state.element_states['maxAutoTrades']
169
- settings.notifications.add_notification(f"maxAutoTrades: {settings.MAX_AUTO_TRADES}")
170
-
171
- settings.notifications.add_notification(" AutoTrade activated" if settings.AUTOTRADE else "AutoTrade de-activated")
172
-
173
-
174
- @app.post("/autotrade")
175
- async def handle_autotrade():
176
- await asyncio.create_task(set_server_states())
177
- return {"status":settings.AUTOTRADE}
178
-
179
- @app.get("/", response_class=HTMLResponse)
180
- async def read_root(request: Request):
181
- return templates.TemplateResponse({"request": request}, "login.html")
182
-
183
- #when main page requested
184
- @app.get("/main", response_class=HTMLResponse)
185
- async def main_page(request: Request, user=Depends(login_manager)):
186
- return templates.TemplateResponse({"request": request, "user": user}, "main.html")
187
-
188
- #when main page opened
189
- @app.websocket("/wsmain")
190
- async def websocket_endpoint(websocket: WebSocket):
191
- await asyncio.create_task(wsmain(websocket))
192
-
193
- async def wsmain(websocket):
194
- query_params = websocket.query_params
195
-
196
- try:
197
- logger.info("local main page WebSocket connection opened")
198
-
199
- await websocket.accept()
200
- bitunix.websocket_connections.add(websocket)
201
-
202
- queue = asyncio.Queue()
203
- queueTask = asyncio.create_task(send_data_queue(websocket, queue))
204
- while True:
205
- stime=time.time()
206
- data={}
207
- try:
208
-
209
- # Handle incoming ping messages
210
- await asyncio.create_task(send_pong(websocket,queue))
211
-
212
- #combined data
213
- dataframes={
214
- "portfolio" : bitunix.bitunixSignal.portfoliodfStyle,
215
- "positions" : bitunix.bitunixSignal.positiondfStyle,
216
- "orders" : bitunix.bitunixSignal.orderdfStyle,
217
- "signals" : bitunix.bitunixSignal.signaldfStyle,
218
- "study" : bitunix.bitunixSignal.allsignaldfStyle,
219
- "positionHistory" : bitunix.bitunixSignal.positionHistorydfStyle
220
- }
221
- notifications=bitunix.bitunixSignal.notifications.get_notifications()
222
-
223
- utc_time = datetime.fromtimestamp(bitunix.bitunixSignal.lastAutoTradeTime, tz=pytz.UTC)
224
- atctime = utc_time.astimezone(pytz.timezone('US/Central')).strftime('%Y-%m-%d %H:%M:%S')
225
- utc_time = datetime.fromtimestamp(bitunix.bitunixSignal.lastTickerDataTime, tz=pytz.UTC)
226
- tdctime = utc_time.astimezone(pytz.timezone('US/Central')).strftime('%Y-%m-%d %H:%M:%S')
227
-
228
- data = {
229
- "dataframes": dataframes,
230
- "profit" : bitunix.bitunixSignal.profit,
231
- "atctime": atctime,
232
- "tdctime": tdctime,
233
- "status_messages": [] if len(notifications)==0 else notifications
234
- }
235
-
236
- await queue.put(json.dumps(data))
237
-
238
- except WebSocketDisconnect:
239
- bitunix.websocket_connections.remove(websocket)
240
- logger.info("local main page WebSocket connection closed")
241
- break
242
- except Exception as e:
243
- logger.info(f"local main page websocket unexpected error1: {e}")
244
- break
245
-
246
- elapsed_time = time.time() - stime
247
-
248
- if settings.VERBOSE_LOGGING:
249
- logger.info(f"wsmain: elapsed time {elapsed_time}")
250
- time_to_wait = max(0.01, bitunix.screen_refresh_interval - elapsed_time)
251
-
252
- await asyncio.sleep(time_to_wait)
253
-
254
- except Exception as e:
255
- logger.info(f"local main page websocket unexpected error2: {e}")
256
- finally:
257
- queueTask.cancel()
258
- try:
259
- await queueTask
260
- except asyncio.CancelledError:
261
- pass
262
-
263
- async def send_pong(websocket, queue):
264
- # Handle incoming ping messages
265
- try:
266
- message = await asyncio.wait_for(websocket.receive_text(), timeout=0.01)
267
- if message == "ping":
268
- await queue.put("pong")
269
- except asyncio.TimeoutError:
270
- pass
271
-
272
- async def send_data_queue(websocket, queue):
273
- while True:
274
- try:
275
- data = await queue.get()
276
- await websocket.send_text(data)
277
- except Exception as e:
278
- pass
279
-
280
- @app.get("/send-message/{msg}")
281
- async def send_message(msg: str):
282
- await asyncio.create_task(bitunix.send_message_to_websocket(msg))
283
- return {"message": f"Sent: {msg}"}
284
-
285
-
286
- @app.post("/handle_bitunix_click")
287
- async def handle_bitunix_click(symbol: str = Form(...)):
288
- # Handle the row click event here
289
- message = f"https://www.bitunix.com/contract-trade/{symbol}"
290
- return {"message": message}
291
-
292
- @app.post("/handle_order_close_click")
293
- async def handle_order_close_click(symbol: str = Form(...), orderId: str = Form(...)):
294
- datajs = await bitunix.bitunixApi.CancelOrder(symbol, orderId)
295
- bitunix.bitunixSignal.notifications.add_notification(f'closing pending order for {symbol} {datajs["msg"]}')
296
-
297
- @app.post("/handle_close_click")
298
- async def handle_close_click(symbol: str = Form(...), positionId: str = Form(...), qty: str = Form(...), unrealizedPNL: str = Form(...), realizedPNL: str = Form(...)):
299
- datajs = await bitunix.bitunixApi.FlashClose(positionId)
300
- pnl=float(unrealizedPNL)+float(realizedPNL)
301
- bitunix.bitunixSignal.notifications.add_notification(f'closing {qty} {symbol} with a profit/loss of {pnl} ({datajs["code"]} {datajs["msg"]}')
302
-
303
- @app.post("/handle_buy_click")
304
- async def handle_buy_click(symbol: str = Form(...), close: str = Form(...)):
305
- balance = bitunix.bitunixSignal.get_portfolio_tradable_balance()
306
- qty= str(balance * (float(settings.ORDER_AMOUNT_PERCENTAGE) / 100) / float(close) * int(settings.LEVERAGE))
307
- datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,'BUY')
308
- bitunix.bitunixSignal.notifications.add_notification(f'Buying {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
309
-
310
- @app.post("/handle_add_click")
311
- async def handle_add_click(symbol: str = Form(...), close: str = Form(...)):
312
- row = bitunix.bitunixSignal.positiondf.loc[bitunix.bitunixSignal.positiondf['symbol'] == symbol]
313
- if not row.empty:
314
- balance = float(bitunix.bitunixSignal.portfoliodf["available"])+float(bitunix.bitunixSignal.portfoliodf["crossUnrealizedPNL"])
315
- qty= str(balance * (float(settings.ORDER_AMOUNT_PERCENTAGE) / 100) / float(close) * int(settings.LEVERAGE))
316
- datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,row['side'].values[0])
317
- bitunix.bitunixSignal.notifications.add_notification(f'adding {row["side"].values[0]} {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
318
-
319
- @app.post("/handle_sell_click")
320
- async def handle_sell_click(symbol: str = Form(...), close: str = Form(...)):
321
- balance = bitunix.bitunixSignal.get_portfolio_tradable_balance()
322
- qty= str(balance * (float(settings.ORDER_AMOUNT_PERCENTAGE) / 100) / float(close) * int(settings.LEVERAGE))
323
- datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,'SELL')
324
- bitunix.bitunixSignal.notifications.add_notification(f'Selling {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
325
-
326
- @app.post("/handle_reduce_click")
327
- async def handle_reduce_click(symbol: str = Form(...), positionId: str = Form(...), qty: str = Form(...), close: str = Form(...)):
328
- row = bitunix.bitunixSignal.positiondf.loc[bitunix.bitunixSignal.positiondf['symbol'] == symbol]
329
- if not row.empty:
330
- balance = bitunix.bitunixSignal.get_portfolio_tradable_balance()
331
- qty= str(float(qty) / 2)
332
- datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,'BUY' if row['side'].values[0]=='SELL' else 'SELL',positionId=positionId, reduceOnly=True)
333
- bitunix.bitunixSignal.notifications.add_notification(f'reducing {row["side"]} {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
334
-
335
- @app.post("/handle_charts_click")
336
- async def handle_charts_click(symbol: str = Form(...)):
337
- chart_url = f"/charts?symbol={symbol}"
338
- return JSONResponse(content={"message": chart_url})
339
-
340
- # when charts page requested (multiple period)
341
- @app.get("/charts", response_class=HTMLResponse)
342
- async def show_charts(request: Request, symbol: str):
343
- return templates.TemplateResponse({"request": request, "data": symbol}, "charts.html")
344
-
345
- # when charts page opened (multiple period)
346
- @app.websocket("/wscharts")
347
- async def websocket_endpoint(websocket: WebSocket):
348
- await asyncio.create_task(wscharts(websocket))
349
-
350
- async def wscharts(websocket):
351
- query_params = websocket.query_params
352
- ticker = query_params.get("ticker")
353
-
354
- try:
355
- logger.info("local charts page WebSocket connection opened")
356
-
357
- await websocket.accept()
358
- bitunix.websocket_connections.add(websocket)
359
-
360
- queue = asyncio.Queue()
361
- queueTask = asyncio.create_task(send_data_queue(websocket, queue))
362
-
363
- while True:
364
- stime=time.time()
365
- try:
366
-
367
- # Handle incoming ping messages
368
- await asyncio.create_task(send_pong(websocket,queue))
369
-
370
- if ticker in bitunix.bitunixSignal.tickerObjects.symbols():
371
- bars=settings.BARS
372
- chart1m=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('1m').get_data()[-bars:])
373
- chart5m=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('5m').get_data()[-bars:])
374
- chart15m=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('15m').get_data()[-bars:])
375
- chart1h=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('1h').get_data()[-bars:])
376
- chart1d=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('1d').get_data()[-bars:])
377
- buysell=list(bitunix.bitunixSignal.tickerObjects.get(ticker).trades)
378
- close=bitunix.bitunixSignal.tickerObjects.get(ticker).get_last()
379
-
380
- data = {
381
- "symbol": ticker,
382
- "close":close,
383
- "chart1m":chart1m,
384
- "chart5m":chart5m,
385
- "chart15m":chart15m,
386
- "chart1h":chart1h,
387
- "chart1d":chart1d,
388
- "buysell": buysell,
389
- "ema_study": settings.EMA_STUDY,
390
- "ema_chart": settings.EMA_CHART,
391
- "macd_study": settings.MACD_STUDY,
392
- "macd_chart": settings.MACD_CHART,
393
- "bbm_study": settings.BBM_STUDY,
394
- "bbm_chart": settings.BBM_CHART,
395
- "rsi_study": settings.RSI_STUDY,
396
- "rsi_chart": settings.RSI_CHART,
397
- }
398
-
399
- await queue.put(json.dumps(data))
400
-
401
- except WebSocketDisconnect:
402
- bitunix.websocket_connections.remove(websocket)
403
- logger.info("local charts page WebSocket connection closed")
404
- break
405
- except Exception as e:
406
- logger.info(f"local charts page websocket unexpected error1: {e}")
407
- break
408
-
409
-
410
- elapsed_time = time.time() - stime
411
- if settings.VERBOSE_LOGGING:
412
- logger.info(f"wscharts: elapsed time {elapsed_time}")
413
- time_to_wait = max(0.01, bitunix.screen_refresh_interval - elapsed_time)
414
- await asyncio.sleep(time_to_wait)
415
- except Exception as e:
416
- logger.info(f"local charts page websocket unexpected error2: {e}")
417
- finally:
418
- queueTask.cancel()
419
- try:
420
- await queueTask
421
- except asyncio.CancelledError:
422
- pass
423
-
424
- # when modal chart page requested (current period)
425
- @app.get("/chart", response_class=HTMLResponse)
426
- async def chart_page(request: Request):
427
- return templates.TemplateResponse("modal-chart.html", {"request": request})
428
-
429
- # when chart page opened (current period)
430
- @app.websocket("/wschart")
431
- async def websocket_endpoint(websocket: WebSocket):
432
- await asyncio.create_task(wschart(websocket))
433
-
434
- async def wschart(websocket):
435
- query_params = websocket.query_params
436
- ticker = query_params.get("ticker")
437
-
438
- try:
439
- logger.info("local chart page WebSocket connection opened")
440
-
441
- await websocket.accept()
442
- bitunix.websocket_connections.add(websocket)
443
-
444
- queue = asyncio.Queue()
445
- queueTask = asyncio.create_task(send_data_queue(websocket, queue))
446
-
447
- while True:
448
- stime=time.time()
449
- try:
450
-
451
- # Handle incoming ping messages
452
- await asyncio.create_task(send_pong(websocket,queue))
453
-
454
- if ticker in bitunix.bitunixSignal.tickerObjects.symbols():
455
- period=settings.OPTION_MOVING_AVERAGE
456
- bars=settings.BARS
457
- chart=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks(period).get_data()[-bars:])
458
- buysell=list(bitunix.bitunixSignal.tickerObjects.get(ticker).trades)
459
- close=bitunix.bitunixSignal.tickerObjects.get(ticker).get_last()
460
- data = {
461
- "symbol": ticker,
462
- "close":close,
463
- "chart":chart,
464
- "buysell": buysell,
465
- "period": period,
466
- "ema_study": settings.EMA_STUDY,
467
- "ema_chart": settings.EMA_CHART,
468
- "macd_study": settings.MACD_STUDY,
469
- "macd_chart": settings.MACD_CHART,
470
- "bbm_study": settings.BBM_STUDY,
471
- "bbm_chart": settings.BBM_CHART,
472
- "rsi_study": settings.RSI_STUDY,
473
- "rsi_chart": settings.RSI_CHART,
474
- }
475
-
476
- await queue.put(json.dumps(data))
477
-
478
- except WebSocketDisconnect:
479
- bitunix.websocket_connections.remove(websocket)
480
- logger.info("local chart page WebSocket connection closed")
481
- break
482
- except Exception as e:
483
- logger.info(f"local chart page websocket unexpected error1: {e}")
484
- break
485
-
486
-
487
- elapsed_time = time.time() - stime
488
- if settings.VERBOSE_LOGGING:
489
- logger.info(f"wschart: elapsed time {elapsed_time}")
490
- time_to_wait = max(0.01, bitunix.screen_refresh_interval - elapsed_time)
491
- await asyncio.sleep(time_to_wait)
492
- except Exception as e:
493
- logger.info(f"local chart page websocket unexpected error2: {e}")
494
- finally:
495
- queueTask.cancel()
496
- try:
497
- await queueTask
498
- except asyncio.CancelledError:
499
- pass
500
-
501
- @app.get("/config", response_class=HTMLResponse)
502
- async def config_page(request: Request):
503
- return templates.TemplateResponse("modal-config.html", {"request": request})
504
-
505
- @app.get("/get-config")
506
- async def get_env_variables():
507
- config = read_config(CONFIG_FILE) # Load .env variables
508
- return JSONResponse(content=config)
509
-
510
- # Save updated environment variables
511
- @app.post("/save-config")
512
- async def save_env_variable(key: str = Form(...), value: str = Form(...)):
513
- global settings
514
- # Read the existing configuration
515
- config = read_config(CONFIG_FILE)
516
- # Temporarily update the config dictionary for validation
517
- config[key] = value
518
- try:
519
- # Test the new configuration using the Settings class
520
- temp_settings = Settings(**config)
521
- # Write the valid configuration back to the file
522
- write_config(CONFIG_FILE, config)
523
- settings = temp_settings
524
- await bitunix.update_settings(settings)
525
- await asyncio.create_task(get_server_states(app, settings))
526
- bitunix.bitunixSignal.notifications.add_notification(f"Updated {key} = {value} successfully")
527
- return {"message": f"Updated {key} = {value} successfully"}
528
- except ValidationError as e:
529
- bitunix.bitunixSignal.notifications.add_notification(f"{key} = {value} validation failed {e.errors()}")
530
- return {"error": f"Validation failed, details: {e.errors()}"}
531
-
532
- def read_config(file_path):
533
- """Read the config file into a dictionary."""
534
- config = {}
535
- if os.path.exists(file_path):
536
- with open(file_path, "r") as file:
537
- for line in file:
538
- if "=" in line:
539
- key, value = line.strip().split("=", 1)
540
- config[key] = value
541
- return config
542
-
543
- def write_config(file_path, config):
544
- """Write the dictionary back to the config file."""
545
- with open(file_path, "w") as file:
546
- for key, value in config.items():
547
- file.write(f"{key}={value}\n")
548
-
549
- @app.get("/logs", response_class=HTMLResponse)
550
- async def log_page(request: Request):
551
- return templates.TemplateResponse("modal-logs.html", {"request": request})
552
-
553
- @app.websocket("/wslogs")
554
- async def websocket_endpoint(websocket: WebSocket):
555
- await websocket.accept() # Accept the WebSocket connection
556
- await stream_log_file(websocket)
557
-
558
- async def stream_log_file(websocket: WebSocket):
559
- try:
560
- with open(LOG_FILE, "r") as log_file:
561
- # Read all lines into memory
562
- lines = log_file.readlines()
563
- # Determine starting point: offset_from_end
564
- start_index = max(len(lines) - 100, 0)
565
- for line in lines[start_index:]: # Yield existing lines from offset
566
- await websocket.send_text(line)
567
- # Stream new lines added to the file
568
- log_file.seek(0, 2) # Go to the end of the file
569
- while True:
570
- line = log_file.readline()
571
- if not line:
572
- await asyncio.sleep(0.1) # Wait briefly if no new line is added
573
- continue
574
- await websocket.send_text(line)
575
- except WebSocketDisconnect:
576
- print("log Client disconnected. Stopping the log stream.")
577
-
578
-
579
- def main():
580
- global bitunix
581
- bitunix = bitunix(PASSWORD, API_KEY, SECRET_KEY, settings)
582
- bitunix.bitunixSignal.notifications.add_notification(f"Starting....................")
583
- import uvicorn
584
- if settings.VERBOSE_LOGGING:
585
- llevel = "debug"
586
- else:
587
- llevel = "error"
588
- config1 = uvicorn.Config(app, host=HOST, port=8000, log_level=llevel, reload=False)
589
- server = uvicorn.Server(config1)
590
- server.run()
591
-
592
- if __name__ == '__main__':
593
- main()
594
-
1
+ import asyncio
2
+ import os
3
+ import uvicorn
4
+ import numpy as np
5
+ import pandas as pd
6
+ import json
7
+ import time
8
+ from datetime import datetime
9
+ import pytz
10
+ from pathlib import Path
11
+
12
+
13
+ from ThreadManager import ThreadManager
14
+ from BitunixApi import BitunixApi
15
+ from BitunixSignal import BitunixSignal
16
+ from NotificationManager import NotificationManager
17
+ from DataFrameHtmlRenderer import DataFrameHtmlRenderer
18
+ from config import Settings
19
+ from logger import Logger
20
+ logger = Logger(__name__).get_logger()
21
+
22
+ from fastapi import FastAPI, Request, Form, WebSocket, WebSocketDisconnect, Depends, Query
23
+ from fastapi.responses import HTMLResponse , JSONResponse, RedirectResponse, StreamingResponse
24
+ from fastapi.templating import Jinja2Templates
25
+
26
+ from fastapi.security import OAuth2PasswordRequestForm
27
+ from fastapi_login import LoginManager
28
+ from fastapi_login.exceptions import InvalidCredentialsException
29
+ from fastapi.middleware.cors import CORSMiddleware
30
+ from fastapi.staticfiles import StaticFiles
31
+
32
+ from dotenv import load_dotenv, dotenv_values, set_key
33
+ from pydantic import ValidationError
34
+
35
+ ENV_FILE = ".env"
36
+ CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+"/config.txt"
37
+ LOG_FILE = "app.log"
38
+
39
+ #load environment variables
40
+ load_dotenv(ENV_FILE)
41
+ API_KEY = os.getenv('API_KEY')
42
+ SECRET_KEY = os.getenv('SECRET_KEY')
43
+ SECRET = os.getenv('SECRET')
44
+ PASSWORD = os.getenv('PASSWORD')
45
+ HOST = os.getenv('HOST')
46
+
47
+ #load config variables using setting class in config.py validating using pydantic
48
+ settings = Settings()
49
+
50
+ class bitunix():
51
+ def __init__(self, password, api_key, secret_key, settings):
52
+ self.screen_refresh_interval =settings.SCREEN_REFRESH_INTERVAL
53
+
54
+ self.autoTrade=settings.AUTOTRADE
55
+
56
+ self.threadManager = ThreadManager()
57
+ self.notifications = NotificationManager()
58
+ self.bitunixApi = BitunixApi(api_key, secret_key, settings)
59
+ self.bitunixSignal = BitunixSignal(api_key, secret_key, settings, self.threadManager, self.notifications, self.bitunixApi)
60
+
61
+ self.websocket_connections = set()
62
+ self.DB = {"admin": {"password": password}}
63
+
64
+ async def update_settings(self, settings):
65
+ self.settings = settings
66
+ await self.bitunixSignal.update_settings(settings)
67
+ await self.bitunixApi.update_settings(settings)
68
+
69
+ async def start(self):
70
+ await asyncio.create_task(self.bitunixSignal.load_tickers())
71
+ await asyncio.create_task(self.bitunixSignal.start_jobs())
72
+
73
+ async def restart(self):
74
+ await asyncio.create_task(self.bitunixSignal.restart_jobs())
75
+
76
+ async def send_message_to_websocket(self,message):
77
+ async def send_to_all():
78
+ for ws in self.bitunixSignal.websocket_connections:
79
+ await ws.send_text(message)
80
+ asyncio.run(send_to_all())
81
+
82
+ async def send_async_message_to_websocket(self,message):
83
+ for ws in self.bitunixSignal.websocket_connections:
84
+ await ws.send_text(message)
85
+
86
+ app = FastAPI()
87
+ app.mount("/static", StaticFiles(directory=os.path.dirname(os.path.abspath(__file__))+("/static")), name="static")
88
+
89
+
90
+ app.add_middleware(
91
+ CORSMiddleware,
92
+ allow_origins=["*"], # Adjust this for production
93
+ allow_credentials=True,
94
+ allow_methods=["*"],
95
+ allow_headers=["*"],
96
+ )
97
+ templates = Jinja2Templates(directory=os.path.dirname(os.path.abspath(__file__))+"/templates")
98
+ SECRET=os.getenv('SECRET')
99
+ login_manager = LoginManager(SECRET, token_url="/auth/login", use_cookie=True)
100
+ login_manager.cookie_name = "auth_token"
101
+
102
+
103
+ @app.post("/auth/login")
104
+ async def login(data: OAuth2PasswordRequestForm = Depends()):
105
+ start=time.time()
106
+ username = data.username
107
+ password = data.password
108
+
109
+ user = load_user(username, some_callable_object)
110
+ if not user or password != user["password"]:
111
+ raise InvalidCredentialsException
112
+ access_token = login_manager.create_access_token(data={"sub": username})
113
+ response = RedirectResponse(url="/main", status_code=302)
114
+ login_manager.set_cookie(response, access_token)
115
+ logger.info(f"/auth/login: elapsed time {time.time()-start}")
116
+ return response
117
+
118
+ def some_callable_object():
119
+ logger.info("called")
120
+
121
+ @login_manager.user_loader(some_callable=some_callable_object)
122
+ def load_user(username, some_callable=some_callable_object):
123
+ user = bitunix.DB.get(username)
124
+ return user
125
+
126
+ @app.get("/private")
127
+ async def get_private_endpoint(user=Depends(login_manager)):
128
+ return "You are an authenticated user"
129
+
130
+ @app.on_event("startup")
131
+ async def startup_event():
132
+ await asyncio.create_task(get_server_states(app, settings))
133
+ await bitunix.start()
134
+
135
+ #load inital states for html
136
+ async def get_server_states(app, settings):
137
+ app.state.element_states = {}
138
+ app.state.element_states['autoTrade']=settings.AUTOTRADE
139
+ app.state.element_states['optionMovingAverage']=settings.OPTION_MOVING_AVERAGE
140
+ app.state.element_states['maxAutoTrades']=settings.MAX_AUTO_TRADES
141
+
142
+ @app.post("/reload")
143
+ async def refresh_detected():
144
+ bitunix.bitunixSignal.notifications.add_notification("Reload detected!")
145
+ await asyncio.create_task(bitunix.restart())
146
+
147
+ @app.post("/save_states")
148
+ async def save_states(states: dict):
149
+ app.state.element_states.update(states)
150
+ return {"message": "States saved"}
151
+
152
+ @app.post("/get_states")
153
+ async def get_states(payload: dict):
154
+ element_ids = payload.get("element_ids", [])
155
+ states = {element_id: app.state.element_states.get(element_id, "No state found") for element_id in element_ids}
156
+ return {"states": states}
157
+
158
+ async def set_server_states():
159
+ settings.AUTOTRADE = True if app.state.element_states['autoTrade']=='true' else False
160
+
161
+ if settings.AUTOTRADE:
162
+ settings.OPTION_MOVING_AVERAGE = app.state.element_states['optionMovingAverage']
163
+ settings.notifications.add_notification(f"optionMovingAverage: {settings.OPTION_MOVING_AVERAGE}")
164
+
165
+ settings.PROFIT_AMOUNT = app.state.element_states['profitAmount']
166
+ settings.notifications.add_notification(f"profitAmount: {settings.PROFIT_AMOUNT}")
167
+
168
+ settings.LOSS_AMOUNT = app.state.element_states['lossAmount']
169
+ settings.notifications.add_notification(f"lossAmount: {settings.LOSS_AMOUNT}")
170
+
171
+ settings.MAX_AUTO_TRADES = app.state.element_states['maxAutoTrades']
172
+ settings.notifications.add_notification(f"maxAutoTrades: {settings.MAX_AUTO_TRADES}")
173
+
174
+ settings.notifications.add_notification(" AutoTrade activated" if settings.AUTOTRADE else "AutoTrade de-activated")
175
+
176
+
177
+ @app.post("/autotrade")
178
+ async def handle_autotrade():
179
+ await asyncio.create_task(set_server_states())
180
+ return {"status":settings.AUTOTRADE}
181
+
182
+ @app.get("/", response_class=HTMLResponse)
183
+ async def read_root(request: Request):
184
+ return templates.TemplateResponse({"request": request}, "login.html")
185
+
186
+ #when main page requested
187
+ @app.get("/main", response_class=HTMLResponse)
188
+ async def main_page(request: Request, user=Depends(login_manager)):
189
+ return templates.TemplateResponse({"request": request, "user": user}, "main.html")
190
+
191
+ #when main page opened
192
+ @app.websocket("/wsmain")
193
+ async def websocket_endpoint(websocket: WebSocket):
194
+ await asyncio.create_task(wsmain(websocket))
195
+
196
+ async def wsmain(websocket):
197
+ query_params = websocket.query_params
198
+
199
+ try:
200
+ logger.info("local main page WebSocket connection opened")
201
+
202
+ await websocket.accept()
203
+ bitunix.websocket_connections.add(websocket)
204
+
205
+ queue = asyncio.Queue()
206
+ queueTask = asyncio.create_task(send_data_queue(websocket, queue))
207
+ while True:
208
+ stime=time.time()
209
+ data={}
210
+ try:
211
+
212
+ # Handle incoming ping messages
213
+ await asyncio.create_task(send_pong(websocket,queue))
214
+
215
+ #combined data
216
+ dataframes={
217
+ "portfolio" : bitunix.bitunixSignal.portfoliodfStyle,
218
+ "positions" : bitunix.bitunixSignal.positiondfStyle,
219
+ "orders" : bitunix.bitunixSignal.orderdfStyle,
220
+ "signals" : bitunix.bitunixSignal.signaldfStyle,
221
+ "study" : bitunix.bitunixSignal.allsignaldfStyle,
222
+ "positionHistory" : bitunix.bitunixSignal.positionHistorydfStyle
223
+ }
224
+ notifications=bitunix.bitunixSignal.notifications.get_notifications()
225
+
226
+ utc_time = datetime.fromtimestamp(bitunix.bitunixSignal.lastAutoTradeTime, tz=pytz.UTC)
227
+ atctime = utc_time.astimezone(pytz.timezone('US/Central')).strftime('%Y-%m-%d %H:%M:%S')
228
+ utc_time = datetime.fromtimestamp(bitunix.bitunixSignal.lastTickerDataTime, tz=pytz.UTC)
229
+ tdctime = utc_time.astimezone(pytz.timezone('US/Central')).strftime('%Y-%m-%d %H:%M:%S')
230
+
231
+ data = {
232
+ "dataframes": dataframes,
233
+ "profit" : bitunix.bitunixSignal.profit,
234
+ "atctime": atctime,
235
+ "tdctime": tdctime,
236
+ "status_messages": [] if len(notifications)==0 else notifications
237
+ }
238
+
239
+ await queue.put(json.dumps(data))
240
+
241
+ except WebSocketDisconnect:
242
+ bitunix.websocket_connections.remove(websocket)
243
+ logger.info("local main page WebSocket connection closed")
244
+ break
245
+ except Exception as e:
246
+ logger.info(f"local main page websocket unexpected error1: {e}")
247
+ break
248
+
249
+ elapsed_time = time.time() - stime
250
+
251
+ if settings.VERBOSE_LOGGING:
252
+ logger.info(f"wsmain: elapsed time {elapsed_time}")
253
+ time_to_wait = max(0.01, bitunix.screen_refresh_interval - elapsed_time)
254
+
255
+ await asyncio.sleep(time_to_wait)
256
+
257
+ except Exception as e:
258
+ logger.info(f"local main page websocket unexpected error2: {e}")
259
+ finally:
260
+ queueTask.cancel()
261
+ try:
262
+ await queueTask
263
+ except asyncio.CancelledError:
264
+ pass
265
+
266
+ async def send_pong(websocket, queue):
267
+ # Handle incoming ping messages
268
+ try:
269
+ message = await asyncio.wait_for(websocket.receive_text(), timeout=0.01)
270
+ if message == "ping":
271
+ await queue.put("pong")
272
+ except asyncio.TimeoutError:
273
+ pass
274
+
275
+ async def send_data_queue(websocket, queue):
276
+ while True:
277
+ try:
278
+ data = await queue.get()
279
+ await websocket.send_text(data)
280
+ except Exception as e:
281
+ pass
282
+
283
+ @app.get("/send-message/{msg}")
284
+ async def send_message(msg: str):
285
+ await asyncio.create_task(bitunix.send_message_to_websocket(msg))
286
+ return {"message": f"Sent: {msg}"}
287
+
288
+
289
+ @app.post("/handle_bitunix_click")
290
+ async def handle_bitunix_click(symbol: str = Form(...)):
291
+ # Handle the row click event here
292
+ message = f"https://www.bitunix.com/contract-trade/{symbol}"
293
+ return {"message": message}
294
+
295
+ @app.post("/handle_order_close_click")
296
+ async def handle_order_close_click(symbol: str = Form(...), orderId: str = Form(...)):
297
+ datajs = await bitunix.bitunixApi.CancelOrder(symbol, orderId)
298
+ bitunix.bitunixSignal.notifications.add_notification(f'closing pending order for {symbol} {datajs["msg"]}')
299
+
300
+ @app.post("/handle_close_click")
301
+ async def handle_close_click(symbol: str = Form(...), positionId: str = Form(...), qty: str = Form(...), unrealizedPNL: str = Form(...), realizedPNL: str = Form(...)):
302
+ datajs = await bitunix.bitunixApi.FlashClose(positionId)
303
+ pnl=float(unrealizedPNL)+float(realizedPNL)
304
+ bitunix.bitunixSignal.notifications.add_notification(f'closing {qty} {symbol} with a profit/loss of {pnl} ({datajs["code"]} {datajs["msg"]}')
305
+
306
+ @app.post("/handle_buy_click")
307
+ async def handle_buy_click(symbol: str = Form(...), close: str = Form(...)):
308
+ balance = bitunix.bitunixSignal.get_portfolio_tradable_balance()
309
+ qty= str(balance * (float(settings.ORDER_AMOUNT_PERCENTAGE) / 100) / float(close) * int(settings.LEVERAGE))
310
+ datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,'BUY')
311
+ bitunix.bitunixSignal.notifications.add_notification(f'Buying {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
312
+
313
+ @app.post("/handle_add_click")
314
+ async def handle_add_click(symbol: str = Form(...), close: str = Form(...)):
315
+ row = bitunix.bitunixSignal.positiondf.loc[bitunix.bitunixSignal.positiondf['symbol'] == symbol]
316
+ if not row.empty:
317
+ balance = float(bitunix.bitunixSignal.portfoliodf["available"])+float(bitunix.bitunixSignal.portfoliodf["crossUnrealizedPNL"])
318
+ qty= str(balance * (float(settings.ORDER_AMOUNT_PERCENTAGE) / 100) / float(close) * int(settings.LEVERAGE))
319
+ datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,row['side'].values[0])
320
+ bitunix.bitunixSignal.notifications.add_notification(f'adding {row["side"].values[0]} {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
321
+
322
+ @app.post("/handle_sell_click")
323
+ async def handle_sell_click(symbol: str = Form(...), close: str = Form(...)):
324
+ balance = bitunix.bitunixSignal.get_portfolio_tradable_balance()
325
+ qty= str(balance * (float(settings.ORDER_AMOUNT_PERCENTAGE) / 100) / float(close) * int(settings.LEVERAGE))
326
+ datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,'SELL')
327
+ bitunix.bitunixSignal.notifications.add_notification(f'Selling {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
328
+
329
+ @app.post("/handle_reduce_click")
330
+ async def handle_reduce_click(symbol: str = Form(...), positionId: str = Form(...), qty: str = Form(...), close: str = Form(...)):
331
+ row = bitunix.bitunixSignal.positiondf.loc[bitunix.bitunixSignal.positiondf['symbol'] == symbol]
332
+ if not row.empty:
333
+ balance = bitunix.bitunixSignal.get_portfolio_tradable_balance()
334
+ qty= str(float(qty) / 2)
335
+ datajs = await bitunix.bitunixApi.PlaceOrder(symbol,qty,close,'BUY' if row['side'].values[0]=='SELL' else 'SELL',positionId=positionId, reduceOnly=True)
336
+ bitunix.bitunixSignal.notifications.add_notification(f'reducing {row["side"]} {qty} {symbol} @ {close} ({datajs["code"]} {datajs["msg"]})')
337
+
338
+ @app.post("/handle_charts_click")
339
+ async def handle_charts_click(symbol: str = Form(...)):
340
+ chart_url = f"/charts?symbol={symbol}"
341
+ return JSONResponse(content={"message": chart_url})
342
+
343
+ # when charts page requested (multiple period)
344
+ @app.get("/charts", response_class=HTMLResponse)
345
+ async def show_charts(request: Request, symbol: str):
346
+ return templates.TemplateResponse({"request": request, "data": symbol}, "charts.html")
347
+
348
+ # when charts page opened (multiple period)
349
+ @app.websocket("/wscharts")
350
+ async def websocket_endpoint(websocket: WebSocket):
351
+ await asyncio.create_task(wscharts(websocket))
352
+
353
+ async def wscharts(websocket):
354
+ query_params = websocket.query_params
355
+ ticker = query_params.get("ticker")
356
+
357
+ try:
358
+ logger.info("local charts page WebSocket connection opened")
359
+
360
+ await websocket.accept()
361
+ bitunix.websocket_connections.add(websocket)
362
+
363
+ queue = asyncio.Queue()
364
+ queueTask = asyncio.create_task(send_data_queue(websocket, queue))
365
+
366
+ while True:
367
+ stime=time.time()
368
+ try:
369
+
370
+ # Handle incoming ping messages
371
+ await asyncio.create_task(send_pong(websocket,queue))
372
+
373
+ if ticker in bitunix.bitunixSignal.tickerObjects.symbols():
374
+ bars=settings.BARS
375
+ chart1m=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('1m').get_data()[-bars:])
376
+ chart5m=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('5m').get_data()[-bars:])
377
+ chart15m=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('15m').get_data()[-bars:])
378
+ chart1h=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('1h').get_data()[-bars:])
379
+ chart1d=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks('1d').get_data()[-bars:])
380
+ buysell=list(bitunix.bitunixSignal.tickerObjects.get(ticker).trades)
381
+ close=bitunix.bitunixSignal.tickerObjects.get(ticker).get_last()
382
+
383
+ data = {
384
+ "symbol": ticker,
385
+ "close":close,
386
+ "chart1m":chart1m,
387
+ "chart5m":chart5m,
388
+ "chart15m":chart15m,
389
+ "chart1h":chart1h,
390
+ "chart1d":chart1d,
391
+ "buysell": buysell,
392
+ "ema_study": settings.EMA_STUDY,
393
+ "ema_chart": settings.EMA_CHART,
394
+ "macd_study": settings.MACD_STUDY,
395
+ "macd_chart": settings.MACD_CHART,
396
+ "bbm_study": settings.BBM_STUDY,
397
+ "bbm_chart": settings.BBM_CHART,
398
+ "rsi_study": settings.RSI_STUDY,
399
+ "rsi_chart": settings.RSI_CHART,
400
+ }
401
+
402
+ await queue.put(json.dumps(data))
403
+
404
+ except WebSocketDisconnect:
405
+ bitunix.websocket_connections.remove(websocket)
406
+ logger.info("local charts page WebSocket connection closed")
407
+ break
408
+ except Exception as e:
409
+ logger.info(f"local charts page websocket unexpected error1: {e}")
410
+ break
411
+
412
+
413
+ elapsed_time = time.time() - stime
414
+ if settings.VERBOSE_LOGGING:
415
+ logger.info(f"wscharts: elapsed time {elapsed_time}")
416
+ time_to_wait = max(0.01, bitunix.screen_refresh_interval - elapsed_time)
417
+ await asyncio.sleep(time_to_wait)
418
+ except Exception as e:
419
+ logger.info(f"local charts page websocket unexpected error2: {e}")
420
+ finally:
421
+ queueTask.cancel()
422
+ try:
423
+ await queueTask
424
+ except asyncio.CancelledError:
425
+ pass
426
+
427
+ # when modal chart page requested (current period)
428
+ @app.get("/chart", response_class=HTMLResponse)
429
+ async def chart_page(request: Request):
430
+ return templates.TemplateResponse("modal-chart.html", {"request": request})
431
+
432
+ # when chart page opened (current period)
433
+ @app.websocket("/wschart")
434
+ async def websocket_endpoint(websocket: WebSocket):
435
+ await asyncio.create_task(wschart(websocket))
436
+
437
+ async def wschart(websocket):
438
+ query_params = websocket.query_params
439
+ ticker = query_params.get("ticker")
440
+
441
+ try:
442
+ logger.info("local chart page WebSocket connection opened")
443
+
444
+ await websocket.accept()
445
+ bitunix.websocket_connections.add(websocket)
446
+
447
+ queue = asyncio.Queue()
448
+ queueTask = asyncio.create_task(send_data_queue(websocket, queue))
449
+
450
+ while True:
451
+ stime=time.time()
452
+ try:
453
+
454
+ # Handle incoming ping messages
455
+ await asyncio.create_task(send_pong(websocket,queue))
456
+
457
+ if ticker in bitunix.bitunixSignal.tickerObjects.symbols():
458
+ period=settings.OPTION_MOVING_AVERAGE
459
+ bars=settings.BARS
460
+ chart=list(bitunix.bitunixSignal.tickerObjects.get(ticker).get_interval_ticks(period).get_data()[-bars:])
461
+ buysell=list(bitunix.bitunixSignal.tickerObjects.get(ticker).trades)
462
+ close=bitunix.bitunixSignal.tickerObjects.get(ticker).get_last()
463
+ data = {
464
+ "symbol": ticker,
465
+ "close":close,
466
+ "chart":chart,
467
+ "buysell": buysell,
468
+ "period": period,
469
+ "ema_study": settings.EMA_STUDY,
470
+ "ema_chart": settings.EMA_CHART,
471
+ "macd_study": settings.MACD_STUDY,
472
+ "macd_chart": settings.MACD_CHART,
473
+ "bbm_study": settings.BBM_STUDY,
474
+ "bbm_chart": settings.BBM_CHART,
475
+ "rsi_study": settings.RSI_STUDY,
476
+ "rsi_chart": settings.RSI_CHART,
477
+ }
478
+
479
+ await queue.put(json.dumps(data))
480
+
481
+ except WebSocketDisconnect:
482
+ bitunix.websocket_connections.remove(websocket)
483
+ logger.info("local chart page WebSocket connection closed")
484
+ break
485
+ except Exception as e:
486
+ logger.info(f"local chart page websocket unexpected error1: {e}")
487
+ break
488
+
489
+
490
+ elapsed_time = time.time() - stime
491
+ if settings.VERBOSE_LOGGING:
492
+ logger.info(f"wschart: elapsed time {elapsed_time}")
493
+ time_to_wait = max(0.01, bitunix.screen_refresh_interval - elapsed_time)
494
+ await asyncio.sleep(time_to_wait)
495
+ except Exception as e:
496
+ logger.info(f"local chart page websocket unexpected error2: {e}")
497
+ finally:
498
+ queueTask.cancel()
499
+ try:
500
+ await queueTask
501
+ except asyncio.CancelledError:
502
+ pass
503
+
504
+ @app.get("/config", response_class=HTMLResponse)
505
+ async def config_page(request: Request):
506
+ return templates.TemplateResponse("modal-config.html", {"request": request})
507
+
508
+ @app.get("/get-config")
509
+ async def get_env_variables():
510
+ config = read_config(CONFIG_FILE) # Load .env variables
511
+ return JSONResponse(content=config)
512
+
513
+ # Save updated environment variables
514
+ @app.post("/save-config")
515
+ async def save_env_variable(key: str = Form(...), value: str = Form(...)):
516
+ global settings
517
+ # Read the existing configuration
518
+ config = read_config(CONFIG_FILE)
519
+ # Temporarily update the config dictionary for validation
520
+ config[key] = value
521
+ try:
522
+ # Test the new configuration using the Settings class
523
+ temp_settings = Settings(**config)
524
+ # Write the valid configuration back to the file
525
+ write_config(CONFIG_FILE, config)
526
+ settings = temp_settings
527
+ await bitunix.update_settings(settings)
528
+ await asyncio.create_task(get_server_states(app, settings))
529
+ bitunix.bitunixSignal.notifications.add_notification(f"Updated {key} = {value} successfully")
530
+ return {"message": f"Updated {key} = {value} successfully"}
531
+ except ValidationError as e:
532
+ bitunix.bitunixSignal.notifications.add_notification(f"{key} = {value} validation failed {e.errors()}")
533
+ return {"error": f"Validation failed, details: {e.errors()}"}
534
+
535
+ def read_config(file_path):
536
+ """Read the config file into a dictionary."""
537
+ config = {}
538
+ if os.path.exists(file_path):
539
+ with open(file_path, "r") as file:
540
+ for line in file:
541
+ if "=" in line:
542
+ key, value = line.strip().split("=", 1)
543
+ config[key] = value
544
+ return config
545
+
546
+ def write_config(file_path, config):
547
+ """Write the dictionary back to the config file."""
548
+ with open(file_path, "w") as file:
549
+ for key, value in config.items():
550
+ file.write(f"{key}={value}\n")
551
+
552
+ @app.get("/logs", response_class=HTMLResponse)
553
+ async def log_page(request: Request):
554
+ return templates.TemplateResponse("modal-logs.html", {"request": request})
555
+
556
+ @app.websocket("/wslogs")
557
+ async def websocket_endpoint(websocket: WebSocket):
558
+ await websocket.accept() # Accept the WebSocket connection
559
+ await stream_log_file(websocket)
560
+
561
+ async def stream_log_file(websocket: WebSocket):
562
+ try:
563
+ with open(LOG_FILE, "r") as log_file:
564
+ # Read all lines into memory
565
+ lines = log_file.readlines()
566
+ # Determine starting point: offset_from_end
567
+ start_index = max(len(lines) - 100, 0)
568
+ for line in lines[start_index:]: # Yield existing lines from offset
569
+ await websocket.send_text(line)
570
+ # Stream new lines added to the file
571
+ log_file.seek(0, 2) # Go to the end of the file
572
+ while True:
573
+ line = log_file.readline()
574
+ if not line:
575
+ await asyncio.sleep(0.1) # Wait briefly if no new line is added
576
+ continue
577
+ await websocket.send_text(line)
578
+ except WebSocketDisconnect:
579
+ print("log Client disconnected. Stopping the log stream.")
580
+
581
+
582
+ def main():
583
+ global bitunix
584
+ bitunix = bitunix(PASSWORD, API_KEY, SECRET_KEY, settings)
585
+ bitunix.bitunixSignal.notifications.add_notification(f"Starting....................")
586
+ import uvicorn
587
+ if settings.VERBOSE_LOGGING:
588
+ llevel = "debug"
589
+ else:
590
+ llevel = "error"
591
+ config1 = uvicorn.Config(app, host=HOST, port=8000, log_level=llevel, reload=False)
592
+ server = uvicorn.Server(config1)
593
+ server.run()
594
+
595
+ if __name__ == '__main__':
596
+ main()
597
+