diff --git a/main/.DS_Store b/main/.DS_Store index 6b5e74f656eba8670e3dd860975b345bb0c5faef..36bd23a29fd01e459ee9050142a5b5a30ac952f2 100644 Binary files a/main/.DS_Store and b/main/.DS_Store differ diff --git a/main/Decision.py b/main/Decision.py index 7ebc93db3b0f03e4d7cedc313834dfa8d5437547..4ce5faa7e82aa9b0e4d5ebee981bb1dcb600c522 100644 --- a/main/Decision.py +++ b/main/Decision.py @@ -14,7 +14,6 @@ import os SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.dirname(SCRIPT_DIR)) -import settings import asyncio import websockets from orders import TradesManagement @@ -59,8 +58,11 @@ class Decision: self.amount = None self.export_result = [] self.mas = [] - #Launch a simualtion for a given market ==> focused on MA - def simulationGen(self,sim,symbol,sizeCandle_min,mas): + self.timeRandom = [] + self.tmpRandom = 0 + self.tickRandom = 0 + #Launch a simualtion for a given market + def simulationGen(self,sim,symbol,sizeCandle_min,indicator,mas=[],timeRandom=[]): cpt = 0 self.sim = sim random.seed(dt.now()) @@ -78,29 +80,52 @@ class Decision: print(f"time period = {self.timePeriod}") self.sim.indexTicker +=1 - #Iterate on each price of a market - for i in range(1,len(sim.histo_prices)): - self.sim.indexTicker +=1 - self.indexTickerSim+=1 - try: - price = self.sim.histo_prices[self.indexTickerSim][0] - date = self.sim.histo_prices[self.indexTickerSim][1] - except: - print("Erreur dans la simulation dans le calcul du SMA.") - print(f"Liste len : {len(self.sim.histo_prices)}") - print(f"Index : {self.indexTickerSim}") - break + if indicator == "SMA": + #Iterate on each price of a market for SMA calculation + for i in range(1,len(sim.histo_prices)): + self.sim.indexTicker +=1 + self.indexTickerSim+=1 + try: + price = self.sim.histo_prices[self.indexTickerSim][0] + date = self.sim.histo_prices[self.indexTickerSim][1] + except: + print("Erreur dans la simulation dans le calcul du SMA.") + print(f"Liste len : {len(self.sim.histo_prices)}") + print(f"Index : {self.indexTickerSim}") + break - self.pricesSim.append([price,date]) - #Launch SMA calculation + self.pricesSim.append([price,date]) + #Launch SMA calculation - # Mettre les décisions - self.calculateSMA(date,symbol,price,mas) - if self.indexTickerSim >= len(sim.histo_prices)-1: - break - print(f"date End = {date}") - #add results to list of results - self.export_result.append(sim.endSimulation(mas)) + # Mettre les décisions + self.calculateSMA(date,symbol,price,mas) + if self.indexTickerSim >= len(sim.histo_prices)-1: + break + print(f"date End = {date}") + #add results to list of results + self.export_result.append(sim.endSimulation(mas)) + if indicator == "random": + #Iterate on each price of a market for random decision + for i in range(1,len(sim.histo_prices)): + self.sim.indexTicker +=1 + self.indexTickerSim +=1 + try: + price = self.sim.histo_prices[self.indexTickerSim][0] + date = self.sim.histo_prices[self.indexTickerSim][1] + except: + print("Erreur dans la simulation dans le calcul du random.") + print(f"Liste len : {len(self.sim.histo_prices)}") + print(f"Index : {self.indexTickerSim}") + break + self.pricesSim.append([price,date]) + + self.calculateRandom(date,symbol,price,timeRandom) + if self.indexTickerSim >= len(sim.histo_prices)-1: + break + print(f"date End = {date}") + self.export_result.append(sim.endSimulation(timeRandom)) + else: + print("Error in the indicator choice") #Detection of the crossing of a mooving average def detectCrossMA(self,short_ma,long_ma,price,date): @@ -114,14 +139,17 @@ class Decision: self.crossed = "down" #Launch our decision algorithme for a simulated or realtime - def launch(self,dateStart, dateEnd, show_graph = True, SL = 0.03, TP = 0.03, candle_size = 10, mas=[20,40]): + def launch(self,dateStart, dateEnd, indicator, show_graph = True, SL = 0.03, TP = 0.03, candle_size = 10 ,mas=[20,40], timeRandom=[15,20]): testSL = [float(SL)] testTP = [float(TP)] candle_sizes = [int(candle_size)] # Moyenne glissante # mas = [[10,20],[5,15]] - self.mas = [mas] + if indicator == "SMA": + self.mas = [mas] + if indicator == "random": + self.timeRandom = [timeRandom] markets = ["BTCUSDT"] bases= ["BTC"] @@ -147,19 +175,29 @@ class Decision: #amout is 10% of balance self.amount = balance*0.1 - for sl in testSL: - self.sim.sl = sl - for tp in testTP: - self.sim.tp = tp - for c in candle_sizes: - for ma in self.mas: - print(f"Simulation of market : {market} with sl = {sl} and tp = {tp} and candle_size = {c} and ma = {ma}") - print("Amount = ",self.amount) - print(self.sim) - self.simulationGen(self.sim,market,c,ma) - # self.sim.resetSimul(sl, tp, market,bases[i], targets[i]) - # self.resetLists(market) - # plot_MA_Price_Orders(self.sma['BTCUSDT'],self.sim.histo_prices,[20,40],self.sim.active_orders,"BTCUSDT") + if indicator == "SMA": + for sl in testSL: + self.sim.sl = sl + for tp in testTP: + self.sim.tp = tp + for c in candle_sizes: + for ma in self.mas: + print(f"Simulation of market : {market} with sl = {sl} and tp = {tp} and candle_size = {c} and ma = {ma}") + print("Amount = ",self.amount) + print(self.sim) + self.simulationGen(self.sim,market,c,indicator,mas=ma) + if indicator == "random": + for sl in testSL: + self.sim.sl = sl + for tp in testTP: + self.sim.tp = tp + for c in candle_sizes: + for timeRand in self.timeRandom: + print(f"Simulation of market : {market} with sl = {sl} and tp = {tp} and candle_size = {c} and random time = {timeRand}") + print("Amount = ",self.amount) + print(self.sim) + self.simulationGen(self.sim,market,c,indicator,timeRandom=timeRand) + export_Simulation_All(self.export_result) @@ -188,6 +226,10 @@ class Decision: self.amount = None self.export_result = [] self.mas = [] + self.timeRandom = [] + self.tmpRandom = 0 + self.tickRandom = 0 + #Format string to datetime def formatDateStr(self,datestr,format): return datetime.strptime(datestr,format) @@ -294,4 +336,29 @@ class Decision: self.detectCrossMA(ma1,ma2,price,date) return 1 return 0 - #print(self.sma) \ No newline at end of file + #print(self.sma) + + #Choose a random time to buy or sell + def calculateRandom(self, date, symbol, price, timeRandom): + if self.init: + self.init = False + self.randomAVorVA(date,price) + return 1 + if not self.init: + # 1 tick = 10s + if self.sim.end_Active < self.sim.indexTicker and self.tmpRandom == 0: + self.tmpRandom = 1 + rand = random.randint(timeRandom[0],timeRandom[1]) + rand = (rand*60)/10 + self.tickRandom = self.sim.indexTicker + rand + if self.sim.indexTicker == self.tickRandom: + self.tmpRandom = 0 + self.randomAVorVA(date,price) + return 1 + + def randomAVorVA(self, date,price): + rand = random.randint(0,1) + if rand == 0: + self.sim.placeOrder(price,self.amount,date,"AV","GTC") + elif rand == 1: + self.sim.placeOrder(price,self.amount,date,"VA","GTC") \ No newline at end of file diff --git a/main/__pycache__/Decision.cpython-310.pyc b/main/__pycache__/Decision.cpython-310.pyc index 31410ac5b63ed1dafc47bab0ccf17b90fc033e4f..588d4e35b656af4aa8b0f4a4dc1a0ff0f28b0a07 100644 Binary files a/main/__pycache__/Decision.cpython-310.pyc and b/main/__pycache__/Decision.cpython-310.pyc differ diff --git a/main/__pycache__/settings.cpython-310.pyc b/main/__pycache__/settings.cpython-310.pyc index 65766bb99a0b8558a53a733e3133f29d579d0426..a58aa08ba9ef7cf1551657b2ad446cc9e41e96f2 100644 Binary files a/main/__pycache__/settings.cpython-310.pyc and b/main/__pycache__/settings.cpython-310.pyc differ diff --git a/main/__pycache__/simulation.cpython-310.pyc b/main/__pycache__/simulation.cpython-310.pyc index e6f8c914f44e21d1aee9d3a287b06ef1cb1f8865..8369cbfc9bebda0632912157ca577bca7636a32c 100644 Binary files a/main/__pycache__/simulation.cpython-310.pyc and b/main/__pycache__/simulation.cpython-310.pyc differ diff --git a/main/backend.py b/main/backend.py index cb890d272110f3b934beda82749cdb1cc9b98553..7382429afab782d58299caa9cc17843349ea12c9 100644 --- a/main/backend.py +++ b/main/backend.py @@ -25,17 +25,30 @@ from matplotlib.dates import DateFormatter from matplotlib.lines import Line2D import json import plotly +import zipfile +import binascii + + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend + +import base64 import time import pandas as pd from matplotlib.figure import Figure import datetime import plotly.graph_objects as go +import plotly.io as pio + from Decision import Decision import base64 import io -import settings from APIWrappers.ApiManagement import createOrder, createOrderLimit, deleteOrder, getBalance, getTicker, getOrders @@ -44,9 +57,6 @@ from APIWrappers.ApiManagement import createOrder, createOrderLimit, deleteOrder async_mode = "eventlet" app = Flask(__name__) - -# app.config['SECRET_KEY'] = 'secret!' - socket_ = SocketIO(app, async_mode=async_mode,cors_allowed_origins="*") market = "BTCUSDT" @@ -56,9 +66,16 @@ dec = Decision("simulated",market) simulationInProgress = False dictError = {} -stickyForm = dict(takeProfit='3.0',stopLoss='3.0',dateSimStart='2022-02-05',dateSimEnd='2022-04-09',indicatorSelect='',MA1='20',MA2='40') +stickyForm = dict(takeProfit='3.0',stopLoss='3.0',dateSimStart='2022-02-05',dateSimEnd='2022-04-09',indicatorSelect='',MA1='20',MA2='40',minimumTime='15',maximumTime='60') resetParams = 1 - +lstSimulations = [] +lstErrorSimulations = [] +showImportError = False +figJson = None +fig = None +results = None +nbrDoublons = 0 +indicatorSim = None @app.route('/') def index(): @@ -66,18 +83,21 @@ def index(): global dictError global stickyForm global resetParams - - figJson = None + global figJson + global results results = None + figJson = None if resetParams == 1: - stickyForm = dict(takeProfit='3.0',stopLoss='3.0',dateSimStart='2022-02-05',dateSimEnd='2022-04-09',indicatorSelect='',MA1='20',MA2='40') + stickyForm = dict(takeProfit='3.0',stopLoss='3.0',dateSimStart='2022-02-05',dateSimEnd='2022-04-09',indicatorSelect='',MA1='20',MA2='40',minimumTime='15',maximumTime='60') if dec.export_result != []: simulationInProgress = False # figJson = createFig(dec.sim.histo_prices, dec.sim.active_orders, dec.sma[market], dec.mas[0], market) tmpOrder = [dec.sim.winsAV, dec.sim.lossAV, dec.sim.winsVA, dec.sim.lossVA] - # print(tmpOrder) - figJson = createFigPyplot(market,dec.sma[market], dec.mas[0], tmpOrder) + if indicatorSim == "SMA": + figJson = createFigPyplot(market,tmpOrder, mas=dec.sma[market], arraySma=dec.mas[0]) + if indicatorSim == "random": + figJson = createFigPyplot(market,tmpOrder) results = getResults() reponse = make_response(render_template('index.html', async_mode=socket_.async_mode, inProgress=simulationInProgress, fig=figJson, res=results, err=dictError, stickyForm=stickyForm)) # reponse.headers['Content-Security-Policy'] = "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.5.1/plotly.min.js https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js https://fonts.googleapis.com/ https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" @@ -86,6 +106,24 @@ def index(): dictError = {} return reponse +@app.route('/mySimulation') +def mySimulation(): + global showImportError + global lstErrorSimulations + global nbrDoublons + + if lstSimulations == []: + print("lstSimulations is empty") + return redirect("/importSimulation") + + if showImportError == False: + print("showImportError is False") + lstErrorSimulations = [] + nbrDoublons = 0 + if showImportError: + print("showImportError is True") + showImportError = False + return render_template('mySimulation.html', lstSimulations=lstSimulations, err=lstErrorSimulations, doublons=nbrDoublons) @app.route("/startSimulation", methods=["POST"]) def startSimulation(): @@ -94,8 +132,8 @@ def startSimulation(): global simulationInProgress global stickyForm global resetParams + global indicatorSim resetParams = 2 - # genral input stickyForm = request.form.to_dict() tp = request.form.get("takeProfit") sl = request.form.get("stopLoss") @@ -104,6 +142,8 @@ def startSimulation(): dateSimStart = request.form.get("dateSimStart") + " 00:00:00.000000Z" dateSimEnd = request.form.get("dateSimEnd") + " 23:59:59.000000Z" ma = None + timeMin = 0 + timeMax = 0 indicator = request.form.get("indicatorSelect") if indicator == "SMA": @@ -113,7 +153,13 @@ def startSimulation(): ma = [int(ma1),int(ma2)] except: return redirect(request.referrer) - else: + elif indicator == "random": + try: + timeMin = int(request.form.get("minimumTime")) + timeMax = int(request.form.get("maximumTime")) + except: + return redirect(request.referrer) + else: return redirect(request.referrer) try: tp = float(tp) @@ -122,11 +168,11 @@ def startSimulation(): except: return redirect(request.referrer) - dictError = checkGeneralInput(tp, sl, candleSize, dateSimStart, dateSimEnd, indicator, ma) + dictError = checkGeneralInput(tp, sl, candleSize, dateSimStart, dateSimEnd, indicator, ma, timeMin, timeMax) tp = tp/100 sl = sl/100 - + timeRandom = [timeMin, timeMax] dictValue = dictError.values() if 'is-invalid' in dictValue: return redirect(request.referrer) @@ -136,21 +182,171 @@ def startSimulation(): if not simulationInProgress: resetParams = 0 - settings.init() dec.resetLists(market) simulationInProgress = True - thread = Thread(target=dec.launch, args=(dateSimStart,dateSimEnd,False,sl,tp,candleSize,ma)) + indicatorSim = indicator + thread = Thread(target=dec.launch, args=(dateSimStart,dateSimEnd,indicator,False,sl,tp,candleSize,ma,timeRandom)) thread.start() return redirect(request.referrer) +@app.route("/importSimulation") +def importSimulation(): + global showImportError + global lstErrorSimulations + global nbrDoublons + if showImportError == False: + lstErrorSimulations = [] + nbrDoublons = 0 + if showImportError: + showImportError = False + return render_template('importSimulation.html', err=lstErrorSimulations, doublons=nbrDoublons) + +@app.route("/uploadFile", methods=["POST"]) +def uploadFile(): + global lstSimulations + global lstErrorSimulations + global showImportError + global nbrDoublons + nbrDoublons = 0 + lstErrorSimulations = [] + showImportError = True + files = request.files.getlist('fileInput') + files += request.files.getlist('filesDragAndDrop') + for file in files: + filename = file.filename + fileExtension = os.path.splitext(filename)[1].lower() + file.seek(0) + + typeOfFile = file.content_type + + if fileExtension not in ['.json', '.zip']: + lstErrorSimulations.append("L'extension du fichier n'est pas prise en charge.") + continue + if typeOfFile == "application/json": + fileData = file.read() + try: + fileData = fileData.decode('utf-8') + except UnicodeDecodeError: + lstErrorSimulations.append("Le fichier JSON ("+ file.filename +") n'est pas encodé en UTF-8.") + continue + try: + jsonFile = json.loads(fileData) + if 'signature' not in jsonFile: + lstErrorSimulations.append("Le fichier JSON ("+ file.filename +") ne contient pas de signature.") + continue + try: + signature = base64.b64decode(jsonFile.pop('signature')) + dataStr = json.dumps(jsonFile, sort_keys=True) + except: + lstErrorSimulations.append("La signature du fichier JSON ("+ file.filename +") n'est pas correcte.") + continue + try: + publicKey.verify( + signature=signature, + data=dataStr.encode(), + padding=padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + algorithm=hashes.SHA256() + ) + for profit in jsonFile['res']['moneyProfits']: + jsonFile['res']['moneyProfits'][profit] = float(jsonFile['res']['moneyProfits'][profit]) + for losse in jsonFile['res']['moneyLosses']: + jsonFile['res']['moneyLosses'][losse] = float(jsonFile['res']['moneyLosses'][losse]) + jsonFile['filename'] = file.filename + lstSimulations.append(jsonFile) + except InvalidSignature: + lstErrorSimulations.append("La signature du fichier JSON ("+ file.filename +") n'est pas correcte.") + continue + except json.JSONDecodeError: + lstErrorSimulations.append("Le contenu du fichier JSON ("+ file.filename +") n'est pas valide.") + continue + elif typeOfFile == "application/zip": + nameZIP = filename + try: + fileData = file.read() + z = zipfile.ZipFile(io.BytesIO(fileData)) + for filename in z.namelist(): + if filename.startswith('__MACOSX/'): + continue + if not filename.endswith('.json'): + lstErrorSimulations.append("Le fichier ZIP ("+nameZIP+") contient un fichier non JSON ("+ filename +").") + continue + json_data = z.read(filename) + try: + json_data = json_data.decode("utf-8") + jsonFile = json.loads(json_data) + if 'signature' not in jsonFile: + lstErrorSimulations.append("Le fichier JSON ("+ filename +") dans le ZIP ("+nameZIP+") ne contient pas de signature.") + continue + try: + signature = base64.b64decode(jsonFile.pop('signature')) + dataStr = json.dumps(jsonFile, sort_keys=True) + except: + lstErrorSimulations.append("La signature du fichier JSON ("+ filename +") dans le ZIP ("+nameZIP+") n'est pas correcte.") + continue + try: + publicKey.verify( + signature=signature, + data=dataStr.encode(), + padding=padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + algorithm=hashes.SHA256() + ) + except InvalidSignature: + lstErrorSimulations.append("La signature du fichier JSON ("+ filename +") dans le ZIP ("+nameZIP+") n'est pas correcte.") + continue + for profit in jsonFile['res']['moneyProfits']: + jsonFile['res']['moneyProfits'][profit] = float(jsonFile['res']['moneyProfits'][profit]) + for losse in jsonFile['res']['moneyLosses']: + jsonFile['res']['moneyLosses'][losse] = float(jsonFile['res']['moneyLosses'][losse]) + jsonFile['filename'] = filename + lstSimulations.append(jsonFile) + except json.JSONDecodeError: + lstErrorSimulations.append("Le contenu du fichier JSON ("+ filename +") dans le ZIP ("+nameZIP+") n'est pas valide.") + continue + except zipfile.BadZipFile: + lstErrorSimulations.append("Le fichier ZIP ("+ filename +") est invalide ou corrompu.") + continue + else: + lstErrorSimulations.append("Le format du fichier ("+ filename +") n'est pas pris en charge.") + continue + nbrDoublons = 0 + i = 0 + while i < len(lstSimulations): + j = i + 1 + while j < len(lstSimulations): + if lstSimulations[i]['params'] == lstSimulations[j]['params']: + print("doublon supp : ", lstSimulations[j]['filename']) + del lstSimulations[j] + nbrDoublons += 1 + else: + j += 1 + i += 1 + return jsonify({'nbrFileUpload': len(lstSimulations)}) @app.route("/progress") def progress(): - if settings.progressBar < 60: - settings.progressBar += 4 - return jsonify({'progress': settings.progressBar}) + if dec.sim.progressBar < 60: + dec.sim.progressBar += 4 + return jsonify({'progress': dec.sim.progressBar}) +@app.route("/deleteSimulation", methods=["GET"]) +def deleteSimulation(): + global lstSimulations + + index = request.args.get('index') + try: + index = int(index) + if index < len(lstSimulations): + del lstSimulations[index] + except: + return redirect('/mySimulation') + return redirect('/mySimulation') @app.route("/resetSimulation") def resetSimulation(): @@ -175,8 +371,60 @@ def resetSimulationWithParams(): return redirect('/') +@app.route("/restartSimulationWithParamsFromIndex") +def restartSimulationWithParamsFromIndex(): + global simulationInProgress + global resetParams + global lstSimulations + global stickyForm + + index = request.args.get('index') + try: + index = int(index) + except: + return redirect('/mySimulation') + if not simulationInProgress: + if index >= len(lstSimulations): + return redirect('/mySimulation') + params = lstSimulations[index]['params'] + stickyForm = params + resetParams = 0 + dec.export_result = [] + + return redirect('/') + +@app.route("/signJSON", methods=["POST"]) +def signJSON(): + global results + if fig != None: + image = request.get_json() + resultsCopy = results + for profit in resultsCopy['moneyProfits']: + resultsCopy['moneyProfits'][profit] = str(resultsCopy['moneyProfits'][profit]) + for losse in resultsCopy['moneyLosses']: + resultsCopy['moneyLosses'][losse] = str(resultsCopy['moneyLosses'][losse]) + jsonData = { + 'image': image['image'], + 'res': resultsCopy, + 'params': stickyForm, + } + jsonData = json.dumps(jsonData, sort_keys=True) + signature = privateKey.sign( + data=jsonData.encode(), + padding=padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + algorithm=hashes.SHA256() + ) + base64Signature = base64.b64encode(signature).decode() + jsonData = json.loads(jsonData) + jsonData['signature'] = base64Signature + return jsonify(jsonData) + return jsonify({}) + -def checkGeneralInput(takeprofit, stoploss, candleSize, dateSimStart, dateSimEnd, indicator, ma): +def checkGeneralInput(takeprofit, stoploss, candleSize, dateSimStart, dateSimEnd, indicator, ma, timeMin, timeMax): dictError = {} # Check if takeprofit and stoploss are between 0 and 100 if takeprofit > 100 or takeprofit < 1: @@ -207,9 +455,16 @@ def checkGeneralInput(takeprofit, stoploss, candleSize, dateSimStart, dateSimEnd dictError['ma'] = 'is-invalid' else: dictError['ma'] = 'is-valid' + elif indicator == "random": + if timeMin > timeMax or timeMin < 15 or timeMax > 1440 or timeMax < timeMin or timeMin == timeMax: + dictError['random'] = 'is-invalid' + else: + dictError['random'] = 'is-valid' return dictError -def createFigPyplot(market, mas, arraySma, orders): +def createFigPyplot(market, orders, mas=[], arraySma=[]): + global fig + dataChart = dec.sim.candle_prices months = {"January": "Janvier", "February": "Février", "March": "Mars", "April": "Avril", "May": "Mai", "June": "Juin", "July": "Juillet", "August": "Août", "September": "Septembre", "October": "Octobre", "November": "Novembre", "December": "Décembre"} @@ -241,27 +496,26 @@ def createFigPyplot(market, mas, arraySma, orders): name="Prix" )]) - - smaData = {sma: {'price': [], 'date': []} for sma in arraySma} - for sma, values in mas.items(): - for value in values: - smaData[int(sma)]['price'].append(value[0]) - smaData[int(sma)]['date'].append(value[1]) - - for i, sma in enumerate(arraySma): - fig.add_trace( - go.Scatter( - x=smaData[sma]['date'], - y=smaData[sma]['price'], - mode='lines', - name=f'SMA{sma}', - hoverinfo="none", - line=dict(color=['rgba(0, 0, 255, 0.3)', 'rgba(128, 0, 128, 0.3)'][i]) + if indicatorSim == "SMA": + smaData = {sma: {'price': [], 'date': []} for sma in arraySma} + for sma, values in mas.items(): + for value in values: + smaData[int(sma)]['price'].append(value[0]) + smaData[int(sma)]['date'].append(value[1]) + + for i, sma in enumerate(arraySma): + fig.add_trace( + go.Scatter( + x=smaData[sma]['date'], + y=smaData[sma]['price'], + mode='lines', + name=f'SMA{sma}', + hoverinfo="none", + line=dict(color=['rgba(0, 0, 255, 0.3)', 'rgba(128, 0, 128, 0.3)'][i]) + ) ) - ) avWins, avLoss, vaWins, vaLoss = orders - print(avWins) for data, color, symbol, name, endName in [ (avWins, 'green', 'triangle-up', 'AV Gains', 'Fin AV Gains'), @@ -279,7 +533,8 @@ def createFigPyplot(market, mas, arraySma, orders): marker=dict( symbol=symbol, size=17, - color=color + color=color, + line=dict(color='blue', width=1) ), legendgroup=name ) @@ -315,20 +570,21 @@ def createFigPyplot(market, mas, arraySma, orders): ) ) fig.update_layout( + margin=dict(l=10, r=10, t=50, b=10), xaxis_rangeslider_visible=False, title="Simulation sur le marché " + market, xaxis_title="Date", yaxis_title="Prix", hovermode="closest", + plot_bgcolor='#f2f2f2', hoverlabel=dict( bgcolor="rgba(123, 132, 144, 0.4)", font_size=16, align="auto", ) ) - fig.update_layout(plot_bgcolor='#f2f2f2') fig.update_yaxes(fixedrange=False) - fig.update_layout() + # fig.update_layout() figJson = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) return figJson @@ -337,17 +593,7 @@ def createFig(prices,orders,mas,periodes,market): prices = dec.sim.histo_prices prices=pd.DataFrame(prices,columns=['price','date']) - - print(prices) - print("//////////////////////////////// -> orders") - print(orders) - print("//////////////////////////////// -> mas") - print(mas) - print("//////////////////////////////// -> periodes") - print(periodes) - print("//////////////////////////////// -> market") - - print(market) + figJson= dec.sim.candle_prices fig = go.Figure() @@ -442,6 +688,40 @@ def getResults(): dict['moneyLosses'][i] = dict['startBalance'][i] * (round(dict['losses'][j],2) * 0.01) return dict +def getKeys(): + privateKeyPath = "privateKey.pem" + publicKeyPath = "publicKey.pem" + + if os.path.exists(privateKeyPath) and os.path.exists(publicKeyPath): + with open(privateKeyPath, "rb") as key_file: + privateKey = serialization.load_pem_private_key( + data=key_file.read(), + password=None + ) + with open(publicKeyPath, "rb") as key_file: + publicKey = serialization.load_pem_public_key( + data=key_file.read() + ) + else: + privateKey = rsa.generate_private_key(public_exponent=65537, key_size=2048) + publicKey = privateKey.public_key() + + with open(privateKeyPath, "wb") as key_file: + key_file.write(privateKey.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) + + with open(publicKeyPath, "wb") as key_file: + key_file.write(publicKey.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + )) + + return privateKey, publicKey + +privateKey, publicKey = getKeys() # #Route wallet ==> order creation diff --git a/main/settings.py b/main/settings.py deleted file mode 100644 index 87bf53ac047b12282244b9a0bda142bc0a9c15c7..0000000000000000000000000000000000000000 --- a/main/settings.py +++ /dev/null @@ -1,3 +0,0 @@ -def init(): - global progressBar - progressBar = 0 \ No newline at end of file diff --git a/main/simulation.py b/main/simulation.py index 4100240d386f2e7ab5c89ed533250d44f4706867..1f08122accd62cadfb59667269dc6b3553bc073a 100644 --- a/main/simulation.py +++ b/main/simulation.py @@ -28,7 +28,6 @@ import random import numpy.polynomial.polynomial as poly import pickle -import settings import sys import os @@ -67,7 +66,9 @@ class Simulation: self.end_Active = 0 self.base = base self.target = target - + self.progressBar = 0 + self.orderFinished = True + #Get closing prices and date from histo_candles into 1 list def reformatPrices(self): print("REFORMAT PRICES") @@ -129,7 +130,8 @@ class Simulation: self.market = market self.start_time =dt.now() self.candle_prices = [] - + self.progressBar = 0 + self.orderFinished = True #Get ochl candle and polynomial transform to get a price ticker and a given time periode def calcOCLH(self,slices_sec,periode_base,format,symbol,show_graph,dateStart,dateEnd): @@ -147,7 +149,8 @@ class Simulation: self.histo_candles = getHistoCandles(symbol,dateStart, dateEnd) # print(self.histo_candles) candles = self.histo_candles - pourcentage = settings.progressBar + # pourcentage = settings.progressBar + pourcentage = self.progressBar for c in candles: Candles = {} xs=[] @@ -176,7 +179,8 @@ class Simulation: totalDuration = (tmpDateEnd - tmpDateStart).total_seconds() elapsedDuration = (currentDate - tmpDateStart).total_seconds() - settings.progressBar = pourcentage + (elapsedDuration / totalDuration) * (95-pourcentage) + # settings.progressBar = pourcentage + (elapsedDuration / totalDuration) * (95-pourcentage) + self.progressBar = pourcentage + (elapsedDuration / totalDuration) * (95-pourcentage) # print(percentage) #print(f"DATE Candle : {dt.fromtimestamp(date)}") date = dt.fromtimestamp(date) - datetime.timedelta(0,periode_base) @@ -264,6 +268,7 @@ class Simulation: return self.end_Active else: + # self.orderFinished = True # print("Can't place, 1 order is still active") # print("End active : ",self.end_Active) return self.end_Active @@ -359,7 +364,8 @@ class Simulation: "VA_loss" : nb_lossVA,"avg_time_order":self.avgtime_order, "losses": losses, "profits":profits} print(res) - settings.progressBar = 100 + self.progressBar = 100 + # settings.progressBar = 100 return res #Calculate profit of the simulation diff --git a/main/static/.DS_Store b/main/static/.DS_Store index a31724f9a298e95e1f177e5a6234051e3051b4af..3c2f395e2e1ed09fb39bc71d99c2a344f0d5ac21 100644 Binary files a/main/static/.DS_Store and b/main/static/.DS_Store differ diff --git a/main/templates/base.html b/main/templates/base.html index 391fa2ebd242083b7862cc63eeaf32f143915abe..53540b6bf2041b3ca62144dbaa284eb324d63363 100644 --- a/main/templates/base.html +++ b/main/templates/base.html @@ -14,7 +14,6 @@ </head> <body id="page-top" class="d-flex flex-column min-vh-100"> - <!-- Navigation--> <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top" id="mainNav"> <div class="container px-4"> <a class="navbar-brand" href="{{ url_for('index')}}">Silmulateur de trading de cryptomonnaies</a> @@ -23,8 +22,7 @@ class="navbar-toggler-icon"></span></button> <div class="collapse navbar-collapse" id="navbarResponsive"> <ul class="navbar-nav ms-auto"> - <!-- <li class="nav-item"><a class="nav-link" href="#launchSimulation">Démarrer une simulation</a></li> --> - <li class="nav-item"><a class="nav-link" href="#simulations">Mes simulations</a></li> + <li class="nav-item"><a class="nav-link" href="{{ url_for('mySimulation') }}">Mes simulations</a></li> <li class="nav-item"><a class="nav-link" href="#comparaison">Comparaison</a></li> </ul> </div> @@ -32,8 +30,7 @@ </nav> {% block content %} {% endblock %} - <!-- Footer--> - <footer class="py-5 bg-dark"> + <footer class="py-4 bg-dark"> <div class="container-fluid px-4"> <div class="row justify-content-center"> <div class="col-12 text-center"> @@ -42,7 +39,6 @@ </div> </div> </footer> - <!-- Bootstrap core JS--> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script> </body> </html> \ No newline at end of file diff --git a/main/templates/importSimulation.html b/main/templates/importSimulation.html new file mode 100644 index 0000000000000000000000000000000000000000..a16d02a8f2c9114d13f74fdb9ebcf5d2fe531e73 --- /dev/null +++ b/main/templates/importSimulation.html @@ -0,0 +1,154 @@ +{% extends 'base.html' %} +{% block content %} +<section class="gradient-custom p-5 h-100 align-items-center my-auto"> + <div class="container py-5 h-100"> + <div class="row justify-content-center align-items-center h-100"> + <div class="col-12 col-lg-9 col-xl-7"> + <h3 class="mb-4 pb-2 pb-md-0 mb-md-2">Importation de simulations</h3> + <div class="card shadow-2-strong card-registration" style="border-radius: 15px;"> + <div class="card-body p-4 p-md-5 dropzone" ondragover="handleDragOver(event)"> + <form action="uploadFile" method="post" enctype="multipart/form-data"> + <div class="container"> + <div class="row"> + <div class="col-lg-12"> + <div class="text-center"> + <h3>Faites glisser et déposez les fichiers JSON ou ZIP ici</h3> + <i class="bi bi-cloud-upload"></i> + <p>Ou sélectionnez les fichiers en cliquant ici :</p> + <input class="form-control form-control-sm w-50 mx-auto" type="file" + name="fileInput" accept=".json,.zip" capture="*.json,*.zip" multiple> + </div> + </div> + </div> + </div> + <br> + <table id="fileTable" class="table"></table> + </div> + </div> + <br> + {% for error in err %} + <div class="alert alert-danger alert-dismissible fade show" role="alert"> + {{ error }} + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + {% endfor %} + <div class="row text-center"> + <div class="mt-4 pt-2"> + <button type="submit" class="btn btn-primary btn-lg">Envoyer</button> + </div> + </div> + </form> + </div> + </div> + </div> +</section> +<script> + var filesToUpload = []; + + function handleDragOver(event) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + document.querySelector('.dropzone').style.backgroundColor = '#f2f2f2'; + document.querySelector('.dropzone').style.borderRadius = '15px'; + } + + function handleDragLeave(event) { + event.preventDefault(); + document.querySelector('.dropzone').style.backgroundColor = ''; + } + + function handleDrop(event) { + event.preventDefault(); + + var files = event.dataTransfer.files; + for (var i = 0, f; f = files[i]; i++) { + if (!['application/json', 'application/zip'].includes(f.type)) { + alert('Type de fichier invalide. Veuillez uploader un fichier JSON ou ZIP.'); + continue; + } + + filesToUpload.push(f); + createFileElement(f); + } + + document.querySelector('.dropzone').style.backgroundColor = ''; + } + + function createFileElement(file) { + var row = document.createElement('tr'); + + var fileName = document.createElement('td'); + fileName.textContent = file.name; + row.appendChild(fileName); + + var removeFile = document.createElement('td'); + var trashIcon = document.createElement('i'); + trashIcon.classList.add('bi'); + trashIcon.classList.add('bi-trash'); + removeFile.appendChild(trashIcon); + + + removeFile.style.color = 'red'; + removeFile.style.cursor = 'pointer'; + removeFile.style.textAlign = 'right'; + removeFile.addEventListener('click', function (e) { + var index = filesToUpload.indexOf(file); + if (index > -1) { + filesToUpload.splice(index, 1); + } + row.remove(); + }); + row.appendChild(removeFile); + + row.style.borderBottom = '1px solid #000'; + row.style.borderTop = '1px solid #000'; + + document.querySelector('#fileTable').appendChild(row); + } + + + function handleSubmit(event) { + event.preventDefault(); + + var fileInput = document.querySelector('input[name="fileInput"]'); + + for (var i = 0, f; f = fileInput.files[i]; i++) { + if (!['application/json', 'application/zip'].includes(f.type)) { + alert('Type de fichier invalide. Veuillez uploader un fichier JSON ou ZIP.'); + continue; + } + filesToUpload.push(f); + } + + var formData = new FormData(); + + for (var i = 0, f; f = filesToUpload[i]; i++) { + formData.append('filesDragAndDrop', f, f.name); + } + + fetch('uploadFile', { method: 'POST', body: formData }) + .then(response => response.json()) + .then(data => { + filesToUpload = []; + fileInput.value = ''; + + window.location.href = "/mySimulation"; + }) + .catch(error => { + console.error('Erreur:', error); + }); + + + } + + var dropzone = document.querySelector('.dropzone'); + dropzone.addEventListener('dragover', handleDragOver, false); + dropzone.addEventListener('dragleave', handleDragLeave, false); + dropzone.addEventListener('drop', handleDrop, false); + + var submitButton = document.querySelector('button[type="submit"]'); + submitButton.addEventListener('click', handleSubmit, false); + + dropzone.style.overflowY = 'scroll'; +</script> +{% endblock %} \ No newline at end of file diff --git a/main/templates/index.html b/main/templates/index.html index b93db00284fa157bf523c36f1a94a443eded4243..9bbb17b69943f72f958f72bd3b546a1befa598de 100644 --- a/main/templates/index.html +++ b/main/templates/index.html @@ -115,11 +115,11 @@ {% else %} <option value="random">Aléatoire</option> {% endif %} - {% if stickyForm['indicatorSelect'] == 'test' %} + <!-- {% if stickyForm['indicatorSelect'] == 'test' %} <option value="test" selected>test</option> {% else %} <option value="test">test</option> - {% endif %} + {% endif %} --> </select> <div class="valid-feedback"> Correct ! @@ -167,17 +167,35 @@ <div class="row" id="rowRandom" style="display: none;"> <div class="col-md-6 mb-4"> <div class="form-outline"> - <label class="form-label" for="M">Random</label> - <input type="number" class="form-control form-control-lg" id="MA1" name="MA1" - max="100" min="5" value="20" step="5"> + <label class="form-label" for="minimumTime">Temps aléatoire minimum + (minute)</label> + <input type="number" class="form-control form-control-lg {{ err['random'] }}" + id="minimumTime" name="minimumTime" max="1440" min="15" + value="{{ stickyForm['minimumTime']}}" step="5"> + <div class="valid-feedback"> + Correct ! + </div> + <div class="invalid-feedback"> + Veuillez entrer un nombre entre 15 et 1440 minutes. Le temps aléatoire minimum + doit être plus petit que le maximum. + </div> </div> </div> <div class="col-md-6 mb-4"> <div class="form-outline"> - <label class="form-label" for="M">Random</label> - <input type="number" class="form-control form-control-lg" id="MA2" name="MA2" - max="105" min="10" value="40" step="5"> + <label class="form-label" for="maximumTime">Temps aléatoire maximum + (minute)</label> + <input type="number" class="form-control form-control-lg {{ err['random'] }}" + id="maximumTime" name="maximumTime" max="1440" min="15" + value="{{ stickyForm['maximumTime']}}" step="5"> + <div class="valid-feedback"> + Correct ! + </div> + <div class="invalid-feedback"> + Veuillez entrer un nombre entre 15 et 1440 minutes. Le temps aléatoire minimum + doit être plus petit que le maximum. + </div> </div> </div> @@ -200,8 +218,12 @@ </div> </div> - <div class="row text-center"> - <div class="mt-4 pt-2"> + <div class="row"> + <div class="col-md-6 mb-4"> + <a href="resetSimulation"><button type="button" + class="btn btn-secondary btn-lg">Réinitialiser les paramètres</button></a> + </div> + <div class="col-md-6 mb-4"> <button type="submit" class="btn btn-primary btn-lg">Lancer la simulation</button> </div> </div> @@ -281,13 +303,11 @@ résultats</button> </div> <div class="col-sm mb-4 text-center"> - <!-- Button trigger modal --> <button type="button" class="btn btn-primary btn-lg" data-bs-toggle="modal" data-bs-target="#modalSimulation"> Démarrer une nouvelle simulation </button> - <!-- Modal --> <div class="modal fade" id="modalSimulation" tabindex="-1" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> @@ -396,23 +416,23 @@ const SMA = document.getElementById('rowSMA'); const random = document.getElementById('rowRandom'); const test = document.getElementById('rowTest'); - + var inProgress = {{ inProgress| lower }}; var chart = null; {% if fig != None %} - chart = {{ fig | safe }}; - Plotly.plot("chart", chart, {}); - showMoreInfo.addEventListener('click', function () { - if (moreResults.style.display === 'none') { - moreResults.style.display = 'block'; - showMoreInfo.innerHTML = 'Masquer les résultats'; - } else { - moreResults.style.display = 'none'; - showMoreInfo.innerHTML = 'Afficher plus de résultats'; - } - }); - exportSimulation.addEventListener('click', exportFile); + chart = {{ fig | safe }}; + Plotly.plot("chart", chart, {}); + showMoreInfo.addEventListener('click', function () { + if (moreResults.style.display === 'none') { + moreResults.style.display = 'block'; + showMoreInfo.innerHTML = 'Masquer les résultats'; + } else { + moreResults.style.display = 'none'; + showMoreInfo.innerHTML = 'Afficher plus de résultats'; + } + }); + exportSimulation.addEventListener('click', exportFile); {% endif %} @@ -455,29 +475,64 @@ } } - function exportFile() { - Plotly.toImage(document.getElementById('chart'), { format: 'png' }) - .then(function(dataUrl){ - const filename = "{{ res['market'] }}.json"; - const jsonData = { - image: dataUrl, - res: {{ res | tojson }}, - params: {{ stickyForm | tojson }} + async function exportFile() { + const chartElement = document.getElementById('chart'); + const currentData = chartElement.data; + const currentLayout = chartElement.layout; + + const newChartElement = document.createElement('div'); + Plotly.newPlot(newChartElement, currentData, currentLayout); + + const layout = { + margin: { + t: 20, + r: 20, + b: 20, + l: 20 + } + }; + + Plotly.relayout(newChartElement, { + showlegend: false, + title: { + text: '' } - const jsonStr = JSON.stringify(jsonData); - - let element = document.createElement('a'); - element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(jsonStr)); - element.setAttribute('download', filename); - - element.style.display = 'none'; - document.body.appendChild(element); - - element.click(); - - document.body.removeChild(element); - }) + }); + + Plotly.update(newChartElement, {}, layout); + + Plotly.toImage(newChartElement, { format: 'png' }) + .then(function (dataUrl) { + const filename = "{{ res['market'] }}.json"; + const jsonData = { + image: dataUrl, + }; + const jsonStr = JSON.stringify(jsonData); + + fetch('/signJSON', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: jsonStr + }) + .then(response => response.json()) + .then(signedJson => { + const signedJsonStr = JSON.stringify(signedJson); + let element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(signedJsonStr)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + }); + }); } + </script> {% endblock %} \ No newline at end of file diff --git a/main/templates/mySimulation.html b/main/templates/mySimulation.html new file mode 100644 index 0000000000000000000000000000000000000000..925a3363cb5ca00f570139150c2b401cd4768d61 --- /dev/null +++ b/main/templates/mySimulation.html @@ -0,0 +1,138 @@ +{% extends 'base.html' %} +{% block content %} +{% if lstSimulations != [] %} +<section class="gradient-custom p-5 h-100 align-items-center my-auto"> + <div class="album py-5"> + <div class="container" style="padding: 0%; margin: 0%;"> + {% for error in err %} + <div class="alert alert-danger alert-dismissible fade show" role="alert"> + {{ error }} + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + {% endfor %} + {% if doublons != 0 %} + <div class="alert alert-warning alert-dismissible fade show" role="alert"> + {{ doublons }} doublon(s) supprimé(s) + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + {% endif %} + <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> + {% for simulation in lstSimulations %} + <div class="col"> + <div class="card shadow-sm"> + <h5 class="px-4 pt-4">Marché : {{ simulation['res']['market']}}</h5> + <p class="px-4">{{ simulation['filename'] }}</p> + <img src="{{ simulation['image']}}" class="bd-placeholder-img card-img-top" width="100%" + height="225"> + <div class="card-body px-0"> + <div class="container h-100"> + <div class="row"> + {% for symbol in simulation['res']['losses']|reverse %} + {% if symbol == "USDT" %} + <div class="col-md-6 mb-2"> + <span class="badge badge-pill bg-danger">{{ symbol }}</span><span + style="color:red"> + {{ simulation['res']['moneyLosses'][symbol]|round(2)|string + }}₮ ({{ simulation['res']['losses'][symbol]|round(2)|string+"%" }})</span> + </div> + {% elif symbol == "BTC" %} + <div class="col-md-6 mb-2" style="padding-right: 0%;"> + <span class="badge badge-pill bg-danger">{{ symbol }}</span><span + style="color:red"> + {{ + "{:.5f}".format(simulation['res']['moneyLosses'][symbol])|string + }}₿ ({{ simulation['res']['losses'][symbol]|round(2)|string+"%" }})</span> + </div> + {% endif %} + {% endfor %} + + {% for symbol in simulation['res']['profits']|reverse %} + {% if symbol == "USDT" %} + <div class="col-md-6 mb-2"> + <span class="badge badge-pill bg-success">{{ symbol }}</span><span + style="color:green"> +{{ + simulation['res']['moneyProfits'][symbol]|round(2)|string + }}₮ ({{ simulation['res']['profits'][symbol]|round(2)|string+"%" }})</span> + </div> + {% elif symbol == "BTC" %} + <div class="col-md-6 mb-2" style="padding-right: 0%;"> + <span class="badge badge-pill bg-success">{{ symbol }}</span><span + style="color:green"> +{{ + "{:.5f}".format(simulation['res']['moneyProfits'][symbol])|string + }}₿ ({{ simulation['res']['profits'][symbol]|round(2)|string+"%" }})</span> + </div> + {% endif %} + {% endfor %} + {% if simulation['res']['profits'] == {} and simulation['res']['losses'] == {} %} + <span class="text-center mb-2">Aucun profit ou perte</span> + {% endif %} + </div> + <div class="row"> + <div class="col-md-6 mb-2"> + <span class="badge badge-pill bg-warning">Take Profit</span><span> {{ + simulation['params']['takeProfit'] }}</span> + </div> + <div class="col-md-6 mb-2"> + <span class="badge badge-pill bg-warning">Stop Loss</span><span> {{ + simulation['params']['stopLoss'] }}</span> + </div> + </div> + <div class="row"> + <div class="col-md-6 mb-2"> + <span class="badge badge-pill bg-warning">Stratégie</span><span> {{ + simulation['params']['indicatorSelect'] }}</span> + </div> + <div class="col-md-6 mb-2"> + <span class="badge badge-pill bg-warning">Winrate</span><span> {{ + simulation['res']['winrate'] }}%</span> + </div> + </div> + <br> + <div class="row"> + <div class="col-md-6 mb-2"> + <a href="/deleteSimulation?index={{ loop.index-1 }}" + class="btn btn-sm btn-danger">Supprimer</a> + </div> + <div class="col-md-6 mb-2"> + <a href="/restartSimulationWithParamsFromIndex?index={{ loop.index-1 }}" + class="btn btn btn-sm btn-primary">Relancer la simulation</a> + </div> + </div> + </div> + </div> + </div> + </div> + {% endfor %} + {% if lstSimulations|length > 0 %} + <div class="col"> + <div class="card shadow-sm h-100"> + <div class="card-body px-0"> + <div class="container h-100"> + <div class="d-flex justify-content-center align-items-center" style="height: 100%;"> + <a href="importSimulation"><i + class="bi bi-plus-square" id="logo" style="font-size: 70px; color:green;"></i></a> + </div> + </div> + </div> + </div> + </div> + <script> + const logo = document.getElementById('logo'); + logo.addEventListener('mouseover', function () { + logo.classList.remove('bi-plus-square'); + logo.classList.add('bi-plus-square-fill'); + }); + + logo.addEventListener('mouseout', function () { + logo.classList.remove('bi-plus-square-fill'); + logo.classList.add('bi-plus-square'); + }); + </script> + {% endif %} + </div> + </div> + </div> + </div> +</section> +{% endif %} +{% endblock %} \ No newline at end of file