From 957d2a82b71a169437da204369d11da15c253e66 Mon Sep 17 00:00:00 2001 From: ozdemirozcelik Date: Wed, 5 Apr 2023 09:33:50 -0700 Subject: [PATCH] security updates --- app.py | 907 +------------------------------------------ config.ini | 2 +- db.py | 4 +- demo.py | 699 +++++++++++++++++++++++++++++++++ handlers.py | 80 ++++ models/account.py | 9 +- models/session.py | 24 +- models/signals.py | 301 +++++++------- models/users.py | 5 +- notify.py | 83 ++++ resources/account.py | 39 +- resources/pairs.py | 55 ++- resources/signals.py | 86 ++-- resources/tickers.py | 44 ++- resources/users.py | 81 ++-- routes.py | 60 +++ security.py | 38 ++ static/pairs.js | 66 ++-- static/signals.js | 40 +- static/style.css | 351 ++++++++++++++++- static/tickers.js | 69 +++- static/users.js | 4 +- templates/base.html | 15 +- templates/dash.html | 24 +- templates/list.html | 40 +- templates/login.html | 26 +- templates/pos.html | 70 ++-- templates/setup.html | 170 +++++--- templates/watch.html | 47 +-- 29 files changed, 2010 insertions(+), 1429 deletions(-) create mode 100644 demo.py create mode 100644 handlers.py create mode 100644 notify.py create mode 100644 routes.py create mode 100644 security.py diff --git a/app.py b/app.py index 876153d..daba6a6 100644 --- a/app.py +++ b/app.py @@ -1,900 +1,29 @@ -import os -from flask import Flask, render_template, request, flash, redirect, url_for +""" +Pairs-Api by ozdemirozcelik +for details: https://github.com/ozdemirozcelik/pairs-api +""" -# get configuration variables +from flask import Flask import configparser -configs = configparser.ConfigParser() -# change to your config file name -configs.read("config.ini") - -# (flask-session-change) enable if using flask sessions, currently using custom created session management: -# from flask_session import Session -# from flask import session -from flask_restful import Api -from flask_jwt_extended import JWTManager -from jwt import ExpiredSignatureError -from blacklist import BLACKLIST -from models.signals import SignalModel -from models.tickers import TickerModel -from models.account import AccountModel -from models.pairs import PairModel -from models.session import SessionModel -from resources.pairs import PairRegister, PairList, Pair -from resources.signals import ( - SignalWebhook, - SignalUpdateOrder, - SignalList, - SignalListTicker, - SignalListStatus, - Signal, -) -from resources.tickers import TickerRegister, TickerUpdatePNL, TickerList, Ticker -from resources.users import ( - UserRegister, - UserList, - User, - UserLogin, - UserLogout, - TokenRefresh, -) -from resources.account import PNLRegister, PNLList, PNL -from db import db -from datetime import datetime -from datetime import timedelta -import pytz -from pytz import timezone -import pandas as pd -import yfinance as yf - +# Create Flask application app = Flask(__name__) -# Use below config to use with POSTGRES: -# check for env variable (postgres), if not found use local sqlite database -app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( - "DATABASE_URL_SQLALCHEMY", "sqlite:///data.db" -) -app.config[ - "SQLALCHEMY_TRACK_MODIFICATIONS" -] = False # flask-sqlalchemy tracker is off, sqlalchemy has its own tracker -app.config[ - "PROPAGATE_EXCEPTIONS" -] = True # to allow flask propagating exception even if debug is set to false - if __name__ == "__main__": # to avoid duplicate calls - app.run(debug=True) - -api = Api(app) - -db.init_app(app) - - -# Create tables and default users -@app.before_first_request -def create_tables(): - db.create_all() - UserRegister.default_users() - - -# Session configuration (Start) - -app.secret_key = os.urandom(24) # need this for session management - -# (flask-session-change) Enable below to keep session data with SQLAlchemy: -# sessions work ok locally but may not be persistent with Heroku free tier. -# TODO: try to keep session data with Redis - -# app.config["SESSION_TYPE"] = "sqlalchemy" -# app.config["SESSION_SQLALCHEMY"] = db # SQLAlchemy object -# app.config["SESSION_SQLALCHEMY_TABLE"] = "session" # session table name -# app.config["SESSION_PERMANENT"] = True -# app.config["SESSION_USE_SIGNER"] = False # browser session cookie value to encrypt -# app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=30) -# app.config[ -# "SESSION_KEY_PREFIX" -# ] = "session:" # the prefix of the value stored in session - - -# (flask-session-change) Enable below to keep session data in the file system: - -# app.config["SESSION_TYPE"] = "filesystem" -# app.config["SESSION_PERMANENT"] = True -# app.config["SESSION_USE_SIGNER"] = False # browser session cookie value to encrypt -# app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=30) -# app.config[ -# "SESSION_KEY_PREFIX" -# ] = "session:" # the prefix of the value stored in session - -## Enable if using sessions with SQLAlchemy to create table to store session data: -## -# Fsession = Session(app) -# -# with app.app_context(): -# if app.config["SESSION_TYPE"] == "sqlalchemy": -# Fsession.app.session_interface.db.create_all() -## - -# Session configuration (End) - -# JWT configuration (Start) -app.config["JWT_SECRET_KEY"] = os.urandom(24) -app.config["JWT_BLACKLIST_ENABLED"] = True # enable blacklist feature -app.config["JWT_BLACKLIST_TOKEN_CHECKS"] = [ - "access", - "refresh", -] # allow blacklisting for access and refresh tokens - -jwt = JWTManager(app) - - -# If necessary to check admin rights, is_admin can be used -@jwt.additional_claims_loader -def add_claims_to_jwt(identity): - if identity == "admin": # TODO: read from a config file - return {"is_admin": True} - return {"is_admin": False} - - -# This method will check if a token is blacklisted, and will be called automatically when blacklist is enabled -@jwt.token_in_blocklist_loader -def check_if_token_in_blacklist(jwt_header, jwt_payload): - return jwt_payload["jti"] in BLACKLIST - - -@jwt.expired_token_loader -def my_expired_token_callback(*args): - return {"message": "The token has expired.", "error": "token_expired"}, 401 - - -# TODO: Check for another solution to invalid token returns 500 instead of 401 -# jwt.exceptions.ExpiredSignatureError: Signature has expired -# check the workaround: _handle_expired_signature -@jwt.invalid_token_loader -def my_invalid_token_callback(*args): - # (flask-session-change) enable if using sessions to end session: - # session["token"] = None - return {"message": "The token is invalid.", "error": "token_invalid"}, 401 - - -@app.errorhandler(ExpiredSignatureError) -def _handle_expired_signature(error): - # (flask-session-change) enable if using sessions to end session: - # session["token"] = None - return {"message": "The token is invalid.", "error": "token_invalid"}, 401 - - -@jwt.unauthorized_loader -def my_missing_token_callback(error): - return ( - { - "message": "Request does not contain an access token.", - "error": "authorization_required", - }, - 401, - ) - - -@jwt.needs_fresh_token_loader -def my_token_not_fresh_callback(jwt_header, jwt_payload): - return {"message": "The token is not fresh.", "error": "fresh_token_required"}, 401 - - -@jwt.revoked_token_loader -def my_revoked_token_callback(jwt_header, jwt_payload): - return {"message": "The token has been revoked.", "error": "token_revoked"}, 401 - - -# JWT configuration (End) - -# Resource definitions (Start) - -api.add_resource(SignalWebhook, "/v4/webhook") -api.add_resource(SignalUpdateOrder, "/v4/signal/updateorder") -api.add_resource(SignalList, "/v4/signals/") -api.add_resource( - SignalListStatus, - "/v4/signals/status//", -) -api.add_resource( - SignalListTicker, "/v4/signals/ticker//" -) -api.add_resource(Signal, "/v4/signal/") - -api.add_resource(PairRegister, "/v4/regpair") -api.add_resource(PairList, "/v4/pairs/") -api.add_resource(Pair, "/v4/pair/") - -api.add_resource(TickerRegister, "/v4/regticker") -api.add_resource(TickerUpdatePNL, "/v4/ticker/updatepnl") -api.add_resource(TickerList, "/v4/tickers/") -api.add_resource(Ticker, "/v4/ticker/") - -api.add_resource(UserRegister, "/v4/reguser") -api.add_resource(UserList, "/v4/users/") -api.add_resource(User, "/v4/user/") -api.add_resource(UserLogin, "/v4/login") -api.add_resource(UserLogout, "/v4/logout") -api.add_resource(TokenRefresh, "/v4/refresh") - -api.add_resource(PNLRegister, "/v4/regpnl") -api.add_resource(PNLList, "/v4/pnl/") - - -# Resource definitions (End) - - -@app.route("/") -def home(): - return redirect("/dashboard") - - -@app.get("/dashboard") -def dashboard(): - # (flask-session-change) Flask sessions may not be persistent in Heroku, works fine in local - # consider disabling below for Heroku - - ## - # if session.get("token") is None: - # session["token"] = None - # - # if session["token"] == "yes_token": - # items = SignalModel.get_rows(str(50)) - # else: - # items = SignalModel.get_rows(str(5)) - # flash("Login to see more!", "login_for_more") - # - # signals = [item.json() for item in items] - ## - - # consider enabling below for Heroku: - # this method uses a simple custom session table created in the database - - access_token = request.cookies.get("access_token") - - SessionModel.delete_expired() # clean expired session data - - if access_token: - simplesession = SessionModel.find_by_value(access_token[-10:]) - else: - simplesession = None - - if simplesession: - items = SignalModel.get_rows(str(20)) - - else: - items = SignalModel.get_rows(str(5)) - flash("Login to see more rows!", "login_for_more") - - signals = [item.json() for item in items] - - return render_template( - "dash.html", - signals=signals, - title="DASHBOARD", - tab1="tab active", - tab2="tab", - tab3="tab", - tab4="tab", - ) - - -@app.get("/list") -def dashboard_list(): - # (flask-session-change) Flask sessions may not be persistent in Heroku, works fine in local - # consider disabling below for Heroku - - ## - # selected_ticker = request.args.get('ticker_webhook') # get from the form submission - - # if session.get("token") is None: - # session["token"] = None - # - # if session["token"] == "yes_token": - # items = SignalModel.get_list_ticker(selected_ticker,"0") - # else: - # items = SignalModel.get_list_ticker(selected_ticker,"5") - # flash("Login to see more!", "login_for_more") - # - # signals = [item.json() for item in items] - ## - - # consider enabling below for Heroku: - # this method uses a simple custom session table created in the database - - # get form submission - selected_ticker = request.args.get("ticker_webhook") - selected_trade_type = request.args.get("tradetype") - start_date_selected = request.args.get("start_date") - end_date_selected = request.args.get("end_date") - - date_format = "%Y-%m-%d" - - try: - start_date = start_date_selected.split(".")[ - 0 - ] # clean the timezone info if necessary - start_date = datetime.strptime( - start_date, date_format - ) # convert string to timestamp - end_date = end_date_selected.split(".")[ - 0 - ] # clean the timezone info if necessary - end_date = datetime.strptime( - end_date, date_format - ) # convert string to timestamp - end_date = end_date + timedelta( - days=1 - ) # Add 1 day to "%Y-%m-%d" 00:00:00 to reach end of day - - except: - start_date = datetime.now(tz=pytz.utc) - timedelta(days=1) - date_now = datetime.now(tz=pytz.utc) - date_now_formatted = date_now.strftime(date_format) # format as string - start_date = datetime.strptime( - date_now_formatted, date_format - ) # convert to timestamp - end_date = start_date + timedelta(days=1) # Today end of day - - access_token = request.cookies.get("access_token") - - SessionModel.delete_expired() # clean expired session data - - if access_token: - simplesession = SessionModel.find_by_value(access_token[-10:]) - else: - simplesession = None - - if selected_ticker: - - slip_dic = SignalModel.get_avg_slip(selected_ticker, start_date, end_date) - - if simplesession: - items = SignalModel.get_list_ticker_dates( - selected_ticker, "0", start_date, end_date - ) - else: - items = SignalModel.get_list_ticker_dates( - selected_ticker, "5", start_date, end_date - ) - flash("Login to see more rows!", "login_for_more") - else: - return render_template( - "list.html", - title="LIST SIGNALS", - tab2="tab active", - tab1="tab", - tab3="tab", - tab4="tab", - ) - - signals = [item.json() for item in items] - - slip_buy = "?" - slip_sell = "?" - slip_avg = "?" - - if slip_dic["buy"]: - slip_buy = str(round(slip_dic["buy"], 5)) - if slip_dic["sell"]: - slip_sell = str(round(slip_dic["sell"], 5)) - if slip_dic["avg"]: - slip_avg = str(round(slip_dic["avg"], 5)) - - return render_template( - "list.html", - signals=signals, - title="LIST SIGNALS", - tab1="tab", - tab2="tab active", - tab3="tab", - tab4="tab", - start_date=start_date, - end_date=end_date - timedelta(days=1), - selected_ticker=selected_ticker, - selected_trade_type=selected_trade_type, - slip_buy=slip_buy, - slip_sell=slip_sell, - slip_avg=slip_avg, - ) - - -@app.get("/positions") -def positions(): - # (flask-session-change) Flask sessions may not be persistent in Heroku, works fine in local - # consider disabling below for Heroku - - ## - # if session.get("token") is None: - # session["token"] = None - # - # if session["token"] == "yes_token": - # items = SignalModel.get_rows(str(50)) - # else: - # items = SignalModel.get_rows(str(5)) - # flash("Login to see more!", "login_for_more") - # - # signals = [item.json() for item in items] - ## - - # consider enabling below for Heroku: - # this method uses a simple custom session table created in the database - - access_token = request.cookies.get("access_token") - - SessionModel.delete_expired() # clean expired session data - - if access_token: - simplesession = SessionModel.find_by_value(access_token[-10:]) - else: - simplesession = None - - pnl = { - "rowid": "NA", - } - - if simplesession: - active_tickers = TickerModel.get_active_tickers(str(20)) - active_pairs = PairModel.get_active_pairs(str(20)) - acc_pnl = AccountModel.get_rows(str(1)) - - # having index problems with heroku deployment with the following. - # len() doesn't work. - # if acc_pnl[0]: - # pnl = acc_pnl[0].json() - - # using this instead: - try: - pnl = acc_pnl[0].json() - except IndexError: - pass - - else: - active_tickers = TickerModel.get_active_tickers(str(3)) - active_pairs = PairModel.get_active_pairs(str(3)) - - flash("Login to see PNL details", "login_for_pnl") - flash("Login to see more positions!", "login_for_more") - - pair_pos_all = [] - sum_act_sma = 0 - - for pair in active_pairs: - pair_pos_all.append( - { - "pair": pair.json(), - "ticker1": TickerModel.find_by_symbol(pair.ticker1).json(), - "ticker2": TickerModel.find_by_symbol(pair.ticker2).json(), - } - ) - - if pair.status == 1 and pair.sma_dist: - act_pos = TickerModel.find_by_symbol(pair.ticker1).active_pos - sum_act_sma = sum_act_sma + pair.sma_dist * act_pos - - # pair_pairs_ticker = [] - # - # for item in active_pairs: - # pair_pairs_ticker.append(TickerModel.find_by_symbol(item.ticker1).json()) - # pair_pairs_ticker.append(TickerModel.find_by_symbol(item.ticker2).json()) - - other_pos = [item.json() for item in active_tickers] - - resolution = "5m" - tickersfile = "tickers_" + resolution.upper() + ".csv" - - # TODO: os.path changes depending on the server. use a better method - # if os.path.exists("app"): - # tickersfile = "app/tickers_" + resolution.upper() + ".csv" - - if os.path.exists(tickersfile): - prices = pd.read_csv(tickersfile, index_col="time") - last_price_update = prices.index[-1] - else: - last_price_update = "" - - print(last_price_update) - - # TODO: add ceiling to the account history - - return render_template( - "pos.html", - pair_pos_all=pair_pos_all, - other_pos=other_pos, - pnl=pnl, - sum_act_sma=sum_act_sma, - last_price_update=last_price_update, - title="POSITIONS", - tab1="tab", - tab2="tab", - tab3="tab active", - tab4="tab", - ) - - -@app.get("/watchlist") -def watchlist(): - access_token = request.cookies.get("access_token") - - SessionModel.delete_expired() # clean expired session data - - if access_token: - simplesession = SessionModel.find_by_value(access_token[-10:]) - else: - simplesession = None - - if simplesession: - watchlist_tickers = TickerModel.get_watchlist_tickers(str(40)) - watchlist_pairs = PairModel.get_watchlist_pairs(str(40)) - - else: - watchlist_tickers = TickerModel.get_watchlist_tickers(str(5)) - watchlist_pairs = PairModel.get_watchlist_pairs(str(5)) - - flash("Login to update!", "login_to_update") - flash("Login to see more!", "login_for_more") - - pair_pos_all = [] - - for pair in watchlist_pairs: - pair_pos_all.append( - { - "pair": pair.json(), - "ticker1": TickerModel.find_by_symbol(pair.ticker1).json(), - "ticker2": TickerModel.find_by_symbol(pair.ticker2).json(), - } - ) - - other_pos = [item.json() for item in watchlist_tickers] - - resolution = "5m" - tickersfile = "watchlist_" + resolution.upper() + ".csv" - - # TODO: os.path changes depending on the server. use a better method - if os.path.exists(tickersfile): - prices = pd.read_csv(tickersfile, index_col="time") - last_price_update = prices.index[-1] - else: - last_price_update = "" - - print(last_price_update) - - return render_template( - "watch.html", - pair_pos_all=pair_pos_all, - last_price_update=last_price_update, - title="WATCHLIST", - tab1="tab", - tab2="tab", - tab3="tab", - tab4="tab active", - ) - - -@app.get("/setup") -def setup(): - # (flask-session-change) session may not be persistent in Heroku, works fine in local - # consider disabling below for Heroku - - ## - # if session.get("token") is None: - # session["token"] = None - # - # if session["token"] == "yes_token": - # return render_template("setup.html") - # else: - # # show login message and bo back to dashboard - # flash("Please login!","login") - # return redirect(url_for('dashboard')) - ## - - # consider enabling below for Heroku: - # this method does not confirm the session on the server side - # JWT tokens are still needed for API, so no need to worry - - access_token = request.cookies.get("access_token") - - if access_token: - return render_template("setup.html") - else: - # show login message and bo back to dashboard - flash("Please login!", "login") - return redirect(url_for("dashboard")) - - -@app.get("/sma") -def calculate_sma_dist(): - try: - calculate_sma_distance() - except Exception as e: - print("***Calculate Err***") - print(e) - - return redirect(url_for("positions")) - - -@app.get("/update_watchlist") -def update_watchlist(): - try: - calculate_watchlist() - except Exception as e: - print("***Calculate Err***") - print(e) - - return redirect(url_for("watchlist")) - - -# TEMPLATE FILTERS BELOW: - -# check if the date is today's date -@app.template_filter("iftoday") -def iftoday(value): - date_format = "%Y-%m-%d %H:%M:%S" - value = value.split(".")[0] # clean the timezone info if necessary - date_signal = datetime.strptime(value, date_format) # convert string to timestamp - - date_now = datetime.now(tz=pytz.utc) - date_now_formatted = date_now.strftime(date_format) # format as string - date_now_final = datetime.strptime( - date_now_formatted, date_format - ) # convert to timestamps - - if date_now_final.day == date_signal.day: - return True - else: - return False - - -# edit the timezone to display at the dashboard, default from UTC to PCT -@app.template_filter("pct_time") -def pct_time(value, fromzone="UTC"): - date_format = "%Y-%m-%d %H:%M:%S" - value = value.split(".")[0] # clean the timezone info if necessary - date_signal = datetime.strptime(value, date_format) # convert string to timestamp - if fromzone == "EST": - date_signal_utc = date_signal.replace( - tzinfo=pytz.timezone("US/Eastern") - ) # add tz info - else: - date_signal_utc = date_signal.replace(tzinfo=pytz.UTC) # add tz info - date_pct = date_signal_utc.astimezone(timezone("US/Pacific")) # change tz - date_final = date_pct.strftime(date_format) # convert to str - - if value is None: - return "" - return date_final - - -# calculate time difference in minutes, default is from UTC -@app.template_filter("timediff") -def timediff(value, fromzone="UTC"): - date_format = "%Y-%m-%d %H:%M:%S" - - try: - value = value.split(".")[0] # clean the timezone info if necessary - - date_signal = datetime.strptime( - value, date_format - ) # convert string to timestamp - - if fromzone == "EST": - date_now = datetime.now(tz=pytz.timezone("US/Eastern")) - else: - date_now = datetime.now(tz=pytz.utc) - - date_now_formatted = date_now.strftime(date_format) # format as string - date_now_final = datetime.strptime( - date_now_formatted, date_format - ) # convert to timestamp - - date_diff = (date_now_final - date_signal).total_seconds() / 60.0 - except: - return "" - - return round(date_diff) - - -# SMA Calculation - - -def SMA(values, n): - sma = pd.Series(values).rolling(n).mean() - std = ( - pd.Series(values).rolling(n).std(ddof=1) - ) # default ddof=1, sample standard deviation, divide by (n-1) - return sma, std - - -def download_data(tickerStrings, int_per, file_name): - print(tickerStrings) - - df_list = list() - - for key in int_per: - for ticker in tickerStrings: - data = yf.download( - ticker, group_by="Ticker", period=int_per[key], interval=key - ) - data["ticker"] = ticker - data.index.names = ["time"] - - df_list.append(data) - - # combine all dataframes into a single dataframe - df_download = pd.concat(df_list) - - # save to csv - df_download.to_csv(file_name + "_" + key.upper() + ".csv") - - df_list = [] - - -def calculate_sma(pairs, file_name="tickers"): - print("***Calculate SMA***") - resolution_sma = "1d" - int_per_sma = {resolution_sma: "3mo"} # define interval and corresponding period - - tickerStrings_sma = [] - - for pair in pairs: - tickerStrings_sma.append(pair.ticker1) - tickerStrings_sma.append(pair.ticker2) - - download_data(tickerStrings_sma, int_per_sma, file_name) - - alltickersfile_sma = file_name + "_" + resolution_sma.upper() + ".csv" - df_sma = pd.read_csv(alltickersfile_sma) - - for pair in pairs: - - df_sorted_sma = df_sma.set_index(["ticker", "time"]).sort_index() # set indexes - df1_sorted_sma = df_sorted_sma.xs(pair.ticker2) # the first ticker - df2_sorted_sma = df_sorted_sma.xs(pair.ticker1) # the second ticker - - df1_sma = pair.hedge * df1_sorted_sma - df_spread_sma = df2_sorted_sma.subtract(df1_sma).round(5) - - df_spread_sma["sma_20"], df_spread_sma["std"] = SMA(df_spread_sma.Close, 20) - - if resolution_sma.upper() == "1H": - df_spread_sma["sma_20d"], df_spread_sma["std"] = SMA( - df_spread_sma.Close, 20 * 7 - ) # add 20d sma for 1H only - - pair.sma = round(df_spread_sma.iloc[-1, :]["sma_20"], 5) - pair.std = round(df_spread_sma.iloc[-1, :]["std"], 5) - pair.update() - - -def calculate_price(pairs, file_name="tickers"): - print("***Calculate Price***") - resolution = "5m" - int_per = {resolution: "1d"} # define interval and corresponding period - - tickerStrings = [] - - for pair in pairs: - tickerStrings.append(pair.ticker1) - tickerStrings.append(pair.ticker2) - - download_data(tickerStrings, int_per, file_name) - - alltickersfile = file_name + "_" + resolution.upper() + ".csv" - df = pd.read_csv(alltickersfile) - - for pair in pairs: - - df_sorted = df.set_index(["ticker", "time"]).sort_index() # set indexes - df1_sorted = df_sorted.xs(pair.ticker1) # the first ticker - df2_sorted = df_sorted.xs(pair.ticker2) # the second ticker - - ticker1_price = df1_sorted.iloc[-1, :]["Close"] - ticker2_price = df2_sorted.iloc[-1, :]["Close"] - - pair.act_price = round(ticker1_price - ticker2_price * pair.hedge, 4) - pair.update() - - # print(pair.act_price) - - if pair.sma: - pair.sma_dist = round(pair.sma - pair.act_price, 4) - else: - pair.sma_dist = 0 - - pair.update() - - -def calculate_sma_distance(): - with app.app_context(): # being executed through a new thread outside the app context - - session_start = configs.get("EXCHANGE", "SESSION_START") - session_end = configs.get("EXCHANGE", "SESSION_END") - session_extension_min = int(configs.get("EXCHANGE", "SESSION_EXTENSION_MIN")) - exchange_timezone = configs.get("EXCHANGE", "EXCHANGE_TIMEZONE") - date_format = "%H:%M:%S" - - date_now = datetime.now(tz=pytz.timezone(exchange_timezone)) - date_now_formatted = date_now.strftime(date_format) # format as string - date_now_final = datetime.strptime( - date_now_formatted, date_format - ) # convert to timestamps - - weekday = date_now.isoweekday() - print("Day of the week: ", str(weekday)) - - session_start_final = datetime.strptime( - session_start, date_format - ) # convert to timestamps - session_end_final = datetime.strptime( - session_end, date_format - ) # convert to timestamps - - since_start = date_now_final - session_start_final - until_end = session_end_final - date_now_final - - print("Since start: ", since_start) - print("Until end: ", until_end) - - if ( - int(weekday) < 6 - and since_start > -timedelta(minutes=session_extension_min) - and until_end > -timedelta(minutes=session_extension_min) - ): - - active_pairs_sma = PairModel.get_active_pairs(str(20)) - - print("***Calculate Start***") - try: - calculate_sma(active_pairs_sma) - calculate_price(active_pairs_sma) - print("***Calculate End***") - except Exception as e: - print("***Calculate Err***") - print(e) - else: - print("***No Calculation***") - - -def calculate_watchlist(): - with app.app_context(): # being executed through a new thread outside the app context - - watchlist_pairs = PairModel.get_watchlist_pairs(str(40)) - - print("***Calculate Watchlist Start***") - try: - calculate_sma(watchlist_pairs, "watchlist") - calculate_price(watchlist_pairs, "watchlist") - print("***Calculate End***") - except Exception as e: - print("***Calculate Err***") - print(e) - - -# scheduler for email notifications and sma calculation below - -from apscheduler.schedulers.background import BackgroundScheduler + app.run() +# get user configuration variables +configs = configparser.ConfigParser() +configs.read("config.ini") # change to your config file name -@app.before_first_request -def init_scheduler(): +import config # global configuration +import security # security module +import routes # API resources +import handlers # loaders and error handlers - # more details: https://betterprogramming.pub/introduction-to-apscheduler-86337f3bb4a6 - scheduler = BackgroundScheduler() - # Check if email notifications are enabled for waiting/problematic orders - if configs.getboolean("EMAIL", "ENABLE_EMAIL_NOTIFICATIONS"): - import email_notifications +# initialize SQLAlchemy +from db import db - scheduler.add_job( - email_notifications.warning_email_context, - "interval", - seconds=int(configs.get("EMAIL", "MAIL_CHECK_PERIOD")), - ) - # Check if enabled to calculate pair price distance to SMA (20 days moving average) - if configs.getboolean("SMA", "ENABLE_SMA_CALC"): - scheduler.add_job( - calculate_sma_distance, - "interval", - minutes=int(configs.get("SMA", "SMA_CALC_PERIOD")), - ) +db.init_app(app) - scheduler.start() - # sched.shutdown() +import demo # front-end demo diff --git a/config.ini b/config.ini index 7cd35c1..3f7b08d 100644 --- a/config.ini +++ b/config.ini @@ -27,7 +27,7 @@ SMA_CALC_PERIOD = 20 # configuraton for the email notifications [EMAIL] -ENABLE_EMAIL_NOTIFICATIONS = True +ENABLE_EMAIL_NOTIFICATIONS = False # check for waiting/problematic orders in every x seconds MAIL_CHECK_PERIOD = 90 diff --git a/db.py b/db.py index 6627dc1..ac8fc77 100644 --- a/db.py +++ b/db.py @@ -1,8 +1,8 @@ from flask_sqlalchemy import SQLAlchemy # TODO: CHECK TIMEOUT -# Certain database backends may impose different inactive connection timeouts, which interferes with -# Flask-SQLAlchemy’s connection pooling. +# Certain database backends may impose different inactive connection timeouts, +# which interferes with Flask-SQLAlchemy’s connection pooling. # set sqlalchemy db and options, below doesn't work for sqlite3 # db = SQLAlchemy( engine_options={'connect_args': {'connect_timeout': 10}} ) diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..533fc74 --- /dev/null +++ b/demo.py @@ -0,0 +1,699 @@ +""" +Routes and functions used for the frontend demo +""" +import os +from app import app, configs +from flask import render_template, request, flash, redirect, url_for +from models.signals import SignalModel +from models.tickers import TickerModel +from models.account import AccountModel +from models.pairs import PairModel +from models.session import SessionModel +from datetime import datetime +from datetime import timedelta +import pytz +from pytz import timezone +import pandas as pd +import yfinance as yf + + +@app.route("/") +def home(): + return redirect("/dashboard") + + +@app.get("/dashboard") +def dashboard(): + # (flask-session-change) Flask sessions may not be persistent in Heroku + # consider disabling below for Heroku + + ## + # if session.get("token") is None: + # session["token"] = None + # + # if session["token"] == "yes_token": + # items = SignalModel.get_rows(str(50)) + # else: + # items = SignalModel.get_rows(str(5)) + # flash("Login to see more!", "login_for_more") + # + # signals = [item.json() for item in items] + ## + + # consider enabling below for Heroku: + # this method uses a simple custom session table created in the database + + access_token = request.cookies.get("access_token") + + SessionModel.delete_expired() # clean expired session data + + if access_token: + simplesession = SessionModel.find_by_value(access_token[-10:]) + else: + simplesession = None + + if simplesession: + items = SignalModel.get_rows(str(20)) + + else: + items = SignalModel.get_rows(str(5)) + flash("Login to see more rows!", "login_for_more") + + signals = [item.json() for item in items] + + return render_template( + "dash.html", + signals=signals, + title="DASHBOARD", + tab1="tab active", + tab2="tab", + tab3="tab", + tab4="tab", + ) + + +@app.get("/list") +def dashboard_list(): + # (flask-session-change) Flask sessions may not be persistent in Heroku + # consider disabling below for Heroku + + ## + # selected_ticker = request.args.get('ticker_webhook') + + # if session.get("token") is None: + # session["token"] = None + # + # if session["token"] == "yes_token": + # items = SignalModel.get_list_ticker(selected_ticker,"0") + # else: + # items = SignalModel.get_list_ticker(selected_ticker,"5") + # flash("Login to see more!", "login_for_more") + # + # signals = [item.json() for item in items] + ## + + # consider enabling below for Heroku: + # this method uses a simple custom session table created in the database + + # get form submission + selected_ticker = request.args.get("ticker_webhook") + selected_trade_type = request.args.get("tradetype") + start_date_selected = request.args.get("start_date") + end_date_selected = request.args.get("end_date") + + date_format = "%Y-%m-%d" + + try: + start_date = start_date_selected.split(".")[ + 0 + ] # clean the timezone info if necessary + start_date = datetime.strptime( + start_date, date_format + ) # convert string to timestamp + end_date = end_date_selected.split(".")[ + 0 + ] # clean the timezone info if necessary + end_date = datetime.strptime( + end_date, date_format + ) # convert string to timestamp + end_date = end_date + timedelta( + days=1 + ) # Add 1 day to "%Y-%m-%d" 00:00:00 to reach end of day + + except Exception as e: + print("Error occurred - ", e) + start_date = datetime.now(tz=pytz.utc) - timedelta(days=1) + date_now = datetime.now(tz=pytz.utc) + date_now_formatted = date_now.strftime(date_format) # format as string + start_date = datetime.strptime( + date_now_formatted, date_format + ) # convert to timestamp + end_date = start_date + timedelta(days=1) # Today end of day + + access_token = request.cookies.get("access_token") + + SessionModel.delete_expired() # clean expired session data + + if access_token: + simplesession = SessionModel.find_by_value(access_token[-10:]) + else: + simplesession = None + + if selected_ticker: + + slip_dic = SignalModel.get_avg_slip(selected_ticker, start_date, end_date) + + if simplesession: + items = SignalModel.get_list_ticker_dates( + selected_ticker, "0", start_date, end_date + ) + else: + items = SignalModel.get_list_ticker_dates( + selected_ticker, "5", start_date, end_date + ) + flash("Login to see more rows!", "login_for_more") + else: + return render_template( + "list.html", + title="LIST SIGNALS", + tab2="tab active", + tab1="tab", + tab3="tab", + tab4="tab", + ) + + signals = [item.json() for item in items] + + slip_buy = "?" + slip_sell = "?" + slip_avg = "?" + + if slip_dic["buy"]: + slip_buy = str(round(slip_dic["buy"], 5)) + if slip_dic["sell"]: + slip_sell = str(round(slip_dic["sell"], 5)) + if slip_dic["avg"]: + slip_avg = str(round(slip_dic["avg"], 5)) + + return render_template( + "list.html", + signals=signals, + title="LIST SIGNALS", + tab1="tab", + tab2="tab active", + tab3="tab", + tab4="tab", + start_date=start_date, + end_date=end_date - timedelta(days=1), + selected_ticker=selected_ticker, + selected_trade_type=selected_trade_type, + slip_buy=slip_buy, + slip_sell=slip_sell, + slip_avg=slip_avg, + ) + + +@app.get("/positions") +def positions(): + # (flask-session-change) Flask sessions may not be persistent in Heroku + # consider disabling below for Heroku + + ## + # if session.get("token") is None: + # session["token"] = None + # + # if session["token"] == "yes_token": + # items = SignalModel.get_rows(str(50)) + # else: + # items = SignalModel.get_rows(str(5)) + # flash("Login to see more!", "login_for_more") + # + # signals = [item.json() for item in items] + ## + + # consider enabling below for Heroku: + # this method uses a simple custom session table created in the database + + access_token = request.cookies.get("access_token") + + SessionModel.delete_expired() # clean expired session data + + if access_token: + simplesession = SessionModel.find_by_value(access_token[-10:]) + else: + simplesession = None + + pnl = { + "rowid": "NA", + } + + if simplesession: + active_tickers = TickerModel.get_active_tickers(str(20)) + active_pairs = PairModel.get_active_pairs(str(20)) + acc_pnl = AccountModel.get_rows(str(1)) + + # having index problems with heroku deployment with the following. + # len() doesn't work. + # if acc_pnl[0]: + # pnl = acc_pnl[0].json() + + # using this instead: + try: + pnl = acc_pnl[0].json() + except IndexError: + pass + + else: + active_tickers = TickerModel.get_active_tickers(str(3)) + active_pairs = PairModel.get_active_pairs(str(3)) + + flash("Login to see PNL details", "login_for_pnl") + flash("Login to see more positions!", "login_for_more") + + pair_pos_all = [] + sum_act_sma = 0 + + for pair in active_pairs: + pair_pos_all.append( + { + "pair": pair.json(), + "ticker1": TickerModel.find_by_symbol(pair.ticker1).json(), + "ticker2": TickerModel.find_by_symbol(pair.ticker2).json(), + } + ) + + if pair.status == 1 and pair.sma_dist: + act_pos = TickerModel.find_by_symbol(pair.ticker1).active_pos + sum_act_sma = sum_act_sma + pair.sma_dist * act_pos + + # pair_pairs_ticker = [] + # + # for item in active_pairs: + # pair_pairs_ticker.append(TickerModel.find_by_symbol(item.ticker1).json()) + # pair_pairs_ticker.append(TickerModel.find_by_symbol(item.ticker2).json()) + + other_pos = [item.json() for item in active_tickers] + + resolution = "5m" + tickersfile = "tickers_" + resolution.upper() + ".csv" + + # TODO: os.path changes depending on the server. use a better method + # if os.path.exists("app"): + # tickersfile = "app/tickers_" + resolution.upper() + ".csv" + + if os.path.exists(tickersfile): + prices = pd.read_csv(tickersfile, index_col="time") + last_price_update = prices.index[-1] + else: + last_price_update = "" + + print(last_price_update) + + # TODO: add ceiling to the account history + + return render_template( + "pos.html", + pair_pos_all=pair_pos_all, + other_pos=other_pos, + pnl=pnl, + sum_act_sma=sum_act_sma, + last_price_update=last_price_update, + title="POSITIONS", + tab1="tab", + tab2="tab", + tab3="tab active", + tab4="tab", + ) + + +@app.get("/watchlist") +def watchlist(): + access_token = request.cookies.get("access_token") + + SessionModel.delete_expired() # clean expired session data + + if access_token: + simplesession = SessionModel.find_by_value(access_token[-10:]) + else: + simplesession = None + + if simplesession: + # watchlist_tickers = TickerModel.get_watchlist_tickers(str(40)) + watchlist_pairs = PairModel.get_watchlist_pairs(str(40)) + + else: + # watchlist_tickers = TickerModel.get_watchlist_tickers(str(5)) + watchlist_pairs = PairModel.get_watchlist_pairs(str(5)) + + flash("Login to update!", "login_to_update") + flash("Login to see more!", "login_for_more") + + pair_pos_all = [] + + for pair in watchlist_pairs: + pair_pos_all.append( + { + "pair": pair.json(), + "ticker1": TickerModel.find_by_symbol(pair.ticker1).json(), + "ticker2": TickerModel.find_by_symbol(pair.ticker2).json(), + } + ) + + # other_pos = [item.json() for item in watchlist_tickers] + + resolution = "5m" + tickersfile = "watchlist_" + resolution.upper() + ".csv" + + # TODO: os.path changes depending on the server. use a better method + if os.path.exists(tickersfile): + prices = pd.read_csv(tickersfile, index_col="time") + last_price_update = prices.index[-1] + else: + last_price_update = "" + + print(last_price_update) + + return render_template( + "watch.html", + pair_pos_all=pair_pos_all, + last_price_update=last_price_update, + title="WATCHLIST", + tab1="tab", + tab2="tab", + tab3="tab", + tab4="tab active", + ) + + +@app.get("/setup") +def setup(): + # (flask-session-change) session may not be persistent in Heroku + # consider disabling below for Heroku + + ## + # if session.get("token") is None: + # session["token"] = None + # + # if session["token"] == "yes_token": + # return render_template("setup.html") + # else: + # # show login message and bo back to dashboard + # flash("Please login!","login") + # return redirect(url_for('dashboard')) + ## + + # consider enabling below for Heroku: + # this method does not confirm the session on the server side + # JWT tokens are still needed for API, so no need to worry + + access_token = request.cookies.get("access_token") + + if access_token: + return render_template("setup.html") + else: + # show login message and bo back to dashboard + flash("Please login!", "login") + return redirect(url_for("dashboard")) + + +@app.get("/sma") +def calculate_sma_dist(): + try: + calculate_sma_distance() + except Exception as e: + print("***Calculate Err***") + print(e) + + return redirect(url_for("positions")) + + +@app.get("/update_watchlist") +def update_watchlist(): + try: + calculate_watchlist() + except Exception as e: + print("***Calculate Err***") + print(e) + + return redirect(url_for("watchlist")) + + +# TEMPLATE FILTERS BELOW: + +# check if the date is today's date +@app.template_filter("iftoday") +def iftoday(value): + date_format = "%Y-%m-%d %H:%M:%S" + value = value.split(".")[0] # clean the timezone info if necessary + date_signal = datetime.strptime(value, date_format) # convert string to timestamp + + date_now = datetime.now(tz=pytz.utc) + date_now_formatted = date_now.strftime(date_format) # format as string + date_now_final = datetime.strptime( + date_now_formatted, date_format + ) # convert to timestamps + + if (date_now_final.day == date_signal.day) and ( + date_now_final.month == date_signal.month + ): + return True + else: + return False + + +# edit the timezone to display at the dashboard, default from UTC to PCT +@app.template_filter("pct_time") +def pct_time(value, fromzone="UTC"): + date_format = "%Y-%m-%d %H:%M:%S" + value = value.split(".")[0] # clean the timezone info if necessary + date_signal = datetime.strptime(value, date_format) # convert string to timestamp + if fromzone == "EST": + date_signal_utc = date_signal.replace( + tzinfo=pytz.timezone("US/Eastern") + ) # add tz info + else: + date_signal_utc = date_signal.replace(tzinfo=pytz.UTC) # add tz info + date_pct = date_signal_utc.astimezone(timezone("US/Pacific")) # change tz + date_final = date_pct.strftime(date_format) # convert to str + + if value is None: + return "" + return date_final + + +# calculate time difference in minutes, default is from UTC +@app.template_filter("timediff") +def timediff(value, fromzone="UTC"): + date_format = "%Y-%m-%d %H:%M:%S" + + try: + value = value.split(".")[0] # clean the timezone info if necessary + + date_signal = datetime.strptime( + value, date_format + ) # convert string to timestamp + + if fromzone == "EST": + date_now = datetime.now(tz=pytz.timezone("US/Eastern")) + else: + date_now = datetime.now(tz=pytz.utc) + + date_now_formatted = date_now.strftime(date_format) # format as string + date_now_final = datetime.strptime( + date_now_formatted, date_format + ) # convert to timestamp + + date_diff = (date_now_final - date_signal).total_seconds() / 60.0 + + except Exception as e: + print("Error occurred - ", e) + return "" + + return round(date_diff) + + +# SMA Calculation + + +def SMA(values, n): + sma = pd.Series(values).rolling(n).mean() + std = ( + pd.Series(values).rolling(n).std(ddof=1) + ) # default ddof=1, sample standard deviation, divide by (n-1) + return sma, std + + +def download_data(tickerStrings, int_per, file_name): + print(tickerStrings) + + df_list = list() + + for key in int_per: + for ticker in tickerStrings: + data = yf.download( + ticker, group_by="Ticker", period=int_per[key], interval=key + ) + data["ticker"] = ticker + data.index.names = ["time"] + + df_list.append(data) + + # combine all dataframes into a single dataframe + df_download = pd.concat(df_list) + + # save to csv + df_download.to_csv(file_name + "_" + key.upper() + ".csv") + + df_list = [] + + +def calculate_sma(pairs, file_name="tickers"): + print("***Calculate SMA***") + resolution_sma = "1d" + int_per_sma = {resolution_sma: "3mo"} # define interval and corresponding period + + tickerStrings_sma = [] + + for pair in pairs: + tickerStrings_sma.append(pair.ticker1) + tickerStrings_sma.append(pair.ticker2) + + download_data(tickerStrings_sma, int_per_sma, file_name) + + alltickersfile_sma = file_name + "_" + resolution_sma.upper() + ".csv" + df_sma = pd.read_csv(alltickersfile_sma) + + for pair in pairs: + + df_sorted_sma = df_sma.set_index(["ticker", "time"]).sort_index() # set indexes + df1_sorted_sma = df_sorted_sma.xs(pair.ticker2) # the first ticker + df2_sorted_sma = df_sorted_sma.xs(pair.ticker1) # the second ticker + + df1_sma = pair.hedge * df1_sorted_sma + df_spread_sma = df2_sorted_sma.subtract(df1_sma).round(5) + + df_spread_sma["sma_20"], df_spread_sma["std"] = SMA(df_spread_sma.Close, 20) + + if resolution_sma.upper() == "1H": + df_spread_sma["sma_20d"], df_spread_sma["std"] = SMA( + df_spread_sma.Close, 20 * 7 + ) # add 20d sma for 1H only + + pair.sma = round(df_spread_sma.iloc[-1, :]["sma_20"], 5) + pair.std = round(df_spread_sma.iloc[-1, :]["std"], 5) + pair.update() + + +def calculate_price(pairs, file_name="tickers"): + print("***Calculate Price***") + resolution = "5m" + int_per = {resolution: "1d"} # define interval and corresponding period + + tickerStrings = [] + + for pair in pairs: + tickerStrings.append(pair.ticker1) + tickerStrings.append(pair.ticker2) + + download_data(tickerStrings, int_per, file_name) + + alltickersfile = file_name + "_" + resolution.upper() + ".csv" + df = pd.read_csv(alltickersfile) + + for pair in pairs: + + df_sorted = df.set_index(["ticker", "time"]).sort_index() # set indexes + df1_sorted = df_sorted.xs(pair.ticker1) # the first ticker + df2_sorted = df_sorted.xs(pair.ticker2) # the second ticker + + ticker1_price = df1_sorted.iloc[-1, :]["Close"] + ticker2_price = df2_sorted.iloc[-1, :]["Close"] + + pair.act_price = round(ticker1_price - ticker2_price * pair.hedge, 4) + pair.update() + + # print(pair.act_price) + + if pair.sma: + pair.sma_dist = round(pair.sma - pair.act_price, 4) + else: + pair.sma_dist = 0 + + pair.update() + + +def calculate_sma_distance(): + with app.app_context(): # being executed outside the app context + + session_start = configs.get("EXCHANGE", "SESSION_START") + session_end = configs.get("EXCHANGE", "SESSION_END") + session_extension_min = int(configs.get("EXCHANGE", "SESSION_EXTENSION_MIN")) + exchange_timezone = configs.get("EXCHANGE", "EXCHANGE_TIMEZONE") + date_format = "%H:%M:%S" + + date_now = datetime.now(tz=pytz.timezone(exchange_timezone)) + date_now_formatted = date_now.strftime(date_format) # format as string + date_now_final = datetime.strptime( + date_now_formatted, date_format + ) # convert to timestamps + + weekday = date_now.isoweekday() + print("Day of the week: ", str(weekday)) + + session_start_final = datetime.strptime( + session_start, date_format + ) # convert to timestamps + session_end_final = datetime.strptime( + session_end, date_format + ) # convert to timestamps + + since_start = date_now_final - session_start_final + until_end = session_end_final - date_now_final + + print("Since start: ", since_start) + print("Until end: ", until_end) + + if ( + int(weekday) < 6 + and since_start > -timedelta(minutes=session_extension_min) + and until_end > -timedelta(minutes=session_extension_min) + ): + + active_pairs_sma = PairModel.get_active_pairs(str(20)) + + print("***Calculate Start***") + try: + calculate_sma(active_pairs_sma) + calculate_price(active_pairs_sma) + print("***Calculate End***") + except Exception as e: + print("***Calculate Err***") + print(e) + else: + print("***No Calculation***") + + +def calculate_watchlist(): + with app.app_context(): # being executed outside the app context + + watchlist_pairs = PairModel.get_watchlist_pairs(str(40)) + + print("***Calculate Watchlist Start***") + try: + calculate_sma(watchlist_pairs, "watchlist") + calculate_price(watchlist_pairs, "watchlist") + print("***Calculate End***") + except Exception as e: + print("***Calculate Err***") + print(e) + + +# scheduler for email notifications and sma calculation below + +from apscheduler.schedulers.background import BackgroundScheduler + + +@app.before_first_request +def init_scheduler(): + + # details: https://betterprogramming.pub/introduction-to-apscheduler-86337f3bb4a6 + scheduler = BackgroundScheduler() + # Check if email notifications are enabled for waiting/problematic orders + if configs.getboolean("EMAIL", "ENABLE_EMAIL_NOTIFICATIONS"): + import notify + + scheduler.add_job( + notify.warning_email_context, + "interval", + seconds=int(configs.get("EMAIL", "MAIL_CHECK_PERIOD")), + ) + # Check if enabled to calculate pair price distance to SMA (20 days moving average) + if configs.getboolean("SMA", "ENABLE_SMA_CALC"): + scheduler.add_job( + calculate_sma_distance, + "interval", + minutes=int(configs.get("SMA", "SMA_CALC_PERIOD")), + ) + + scheduler.start() + # sched.shutdown() diff --git a/handlers.py b/handlers.py new file mode 100644 index 0000000..60ca7ba --- /dev/null +++ b/handlers.py @@ -0,0 +1,80 @@ +""" +Loaders & Error Handlers +""" + +from config import jwt +from app import app +from jwt import ExpiredSignatureError + + +# Create tables and default users +@app.before_first_request +def create_tables(): + from app import db + from resources.users import UserRegister + + db.create_all() + UserRegister.default_users() + + +# If necessary to check admin rights, is_admin can be used +@jwt.additional_claims_loader +def add_claims_to_jwt(identity): + from app import configs + + admin_username = configs.get("SECRET", "ADMIN_USERNAME") + if identity == admin_username: # TODO: read from a config file + return {"is_admin": True} + return {"is_admin": False} + + +# This method will check if a token is blacklisted, +# and will be called automatically when blacklist is enabled +@jwt.token_in_blocklist_loader +def check_if_token_in_blacklist(jwt_header, jwt_payload): + from blacklist import BLACKLIST + + return jwt_payload["jti"] in BLACKLIST + + +@jwt.expired_token_loader +def my_expired_token_callback(*args): + return {"message": "The token has expired.", "error": "token_expired"}, 401 + + +# TODO: Check for another solution to invalid token returns 500 instead of 401 +# jwt.exceptions.ExpiredSignatureError: Signature has expired +# check the workaround: _handle_expired_signature +@jwt.invalid_token_loader +def my_invalid_token_callback(*args): + # (flask-session-change) enable if using sessions to end session: + # session["token"] = None + return {"message": "The token is invalid.", "error": "token_invalid"}, 401 + + +@jwt.unauthorized_loader +def my_missing_token_callback(error): + return ( + { + "message": "Request does not contain an access token.", + "error": "authorization_required", + }, + 401, + ) + + +@jwt.needs_fresh_token_loader +def my_token_not_fresh_callback(jwt_header, jwt_payload): + return {"message": "The token is not fresh.", "error": "fresh_token_required"}, 401 + + +@jwt.revoked_token_loader +def my_revoked_token_callback(jwt_header, jwt_payload): + return {"message": "The token has been revoked.", "error": "token_revoked"}, 401 + + +@app.errorhandler(ExpiredSignatureError) +def _handle_expired_signature(error): + # (flask-session-change) enable if using sessions to end session: + # session["token"] = None + return {"message": "The token is invalid.", "error": "token_invalid"}, 401 diff --git a/models/account.py b/models/account.py index 7ef476e..aaf554b 100644 --- a/models/account.py +++ b/models/account.py @@ -1,3 +1,4 @@ +"""Account Model""" import re from typing import Dict, List # for type hinting from db import db @@ -18,8 +19,7 @@ class AccountModel(db.Model): ) # using 'rowid' as the default key timestamp = db.Column( db.DateTime(timezone=False), - # server_default=func.timezone("UTC", func.current_timestamp()) # this can be problematic for sqlite3 - server_default=func.current_timestamp() # TODO: check for sqlite3 and postgres + server_default=func.current_timestamp() # db.DateTime(timezone=False), server_default = func.now() ) # DATETIME DEFAULT (CURRENT_TIMESTAMP) for sqlite3 AvailableFunds = db.Column(db.Float) @@ -106,8 +106,3 @@ def delete(self) -> None: db.session.delete(self) db.session.commit() - - # TODO: add a find_by_date function - # def find_by_date(cls, date) -> "AccountModel": - # - # return cls.query.filter_by(cls.timestamp == date).first() diff --git a/models/session.py b/models/session.py index 0b1e9d6..41731f4 100644 --- a/models/session.py +++ b/models/session.py @@ -1,4 +1,4 @@ -from typing import Dict, Union # for type hinting +from typing import Dict, List # for type hinting from db import db from sqlalchemy.sql import func from datetime import datetime @@ -32,6 +32,11 @@ def find_by_value(cls, value) -> "SessionModel": return cls.query.filter_by(value=value).first() + @classmethod + def get_all(cls) -> List: + + return cls.query.order_by(cls.rowid.desc()) + def insert(self) -> None: db.session.add(self) @@ -44,21 +49,14 @@ def delete(self) -> None: @staticmethod def delete_all() -> None: - try: - db.session.query(SessionModel).delete() - db.session.commit() - except: - db.session.rollback() + + db.session.query(SessionModel).delete() + db.session.commit() @classmethod def delete_expired(cls) -> None: date_now = datetime.now(tz=pytz.utc) - print(date_now) - - try: - cls.query.filter(cls.expiry < date_now).delete() - db.session.commit() - except: - db.session.rollback() + cls.query.filter(cls.expiry < date_now).delete() + db.session.commit() diff --git a/models/signals.py b/models/signals.py index 598d183..2cc1b8f 100644 --- a/models/signals.py +++ b/models/signals.py @@ -25,7 +25,6 @@ class SignalModel(db.Model): ) # using 'rowid' as the default key timestamp = db.Column( db.DateTime(timezone=False), - # server_default=func.timezone("UTC", func.current_timestamp()) # this can be problematic for sqlite3 server_default=func.current_timestamp() # TODO: check for sqlite3 and postgres # db.DateTime(timezone=False), server_default = func.now() ) # DATETIME DEFAULT (CURRENT_TIMESTAMP) for sqlite3 @@ -508,12 +507,12 @@ def check_ticker_status(self) -> bool: self.status_msg = "unknown ticker" return False - def splitticker( - self, - ) -> bool: + def splitticker(self,) -> bool: success_flag = True currency_match = True + ticker_pair1 = "" + ticker_pair2 = "" eq12 = self.ticker.split("-") # check if pair or single # print(eq12) # ['LNT', '1.25*NYSE:FTS'] @@ -543,15 +542,15 @@ def splitticker( item = TickerModel.find_by_symbol(eq1_ticker_almost) if item: + # print("item found") + + # refer to test cases if item.sectype == "CASH": + fx1 = eq1_ticker_almost[0:3] # get the first 3 char # USD fx2 = eq1_ticker_almost[-3:] # get the last 3 char # CAD ticker_pair1 = fx1 + "." + fx2 - # TODO: improve validity check - # check if valid fx pair - if len(ticker_pair1) != 7: # check if it is in USD.CAD format - success_flag = False # check for currency mismatch if fx2 != item.currency: currency_match = False @@ -567,7 +566,7 @@ def splitticker( # TODO: improve validity check # check if valid crypto pair, accepts only USD pairs - if cry2 != item.currency or cry2 != "USD": + if cry2 != item.currency: currency_match = False success_flag = False @@ -584,8 +583,9 @@ def splitticker( char for char in eq1_ticker_almost if char.isalnum() ) - if eq1_ticker_almost != ticker_pair1: - success_flag = False + else: + success_flag = False + self.status_msg = "unknown ticker!" # print("ticker_pair1: ", ticker_pair1) # LNT @@ -621,18 +621,16 @@ def splitticker( # print("eq2_ticker_almost: ", eq2_ticker_almost) # FTS # check if the ticker security type is CASH or CRYPTO - item = TickerModel.find_by_symbol(eq1_ticker_almost) + item = TickerModel.find_by_symbol(eq2_ticker_almost) if item: + # print("item found") + if item.sectype == "CASH": fx1 = eq2_ticker_almost[0:3] # get the first 3 char # USD fx2 = eq2_ticker_almost[-3:] # get the last 3 char # CAD ticker_pair2 = fx1 + "." + fx2 - # TODO: improve validity check - # check if valid fx pair - if len(ticker_pair2) != 7: # check if it is in USD.CAD format - success_flag = False # check for currency mismatch if fx2 != item.currency: currency_match = False @@ -648,10 +646,9 @@ def splitticker( # TODO: improve validity check # check if valid cryptopair, accepts only USD pairs - if cry2 != item.currency or cry2 != "USD": + if cry2 != item.currency: currency_match = False success_flag = False - else: if ( @@ -665,8 +662,9 @@ def splitticker( char for char in eq2_ticker_almost if char.isalnum() ) - if eq2_ticker_almost != ticker_pair2: - success_flag = False + else: + success_flag = False + self.status_msg = "unknown ticker!" # print("ticker_pair2: ", ticker_pair2) # FTS @@ -694,134 +692,6 @@ def splitticker( return success_flag - # to split stocks only - def splitticker_stocks( - self, - ) -> bool: - - # Split the received webhook equation into tickers and hedge parameters - # Tested with Tradingview webhooks and Interactive Brokers ticker format - # TESTED FOR THESE EQUATIONS: - # pair_equation = "TEST 123" - # pair_equation = "NYSE:LNT" - # pair_equation = "0.7*NYSE:BF.A" - # pair_equation = "NYSE:BF.A" - # pair_equation = "NYSE:LNT-NYSE:FTS*2.2" - # pair_equation = "NYSE:LNT*2-NYSE:FTS" - # pair_equation = "NYSE:LNT-NYSE:FTS/3" - # pair_equation = "1.3*NYSE:LNT-NYSE:FTS*2.2" - # pair_equation = "NYSE:LNT-1.25*NYSE:FTS" - # pair_equation = "LNT-1.25*NYSE:FTS" - # pair_equation = "NYSE:LNT-NYSE:FTS" - # pair_equation = "BF.A-0.7*NYSE:BF.B" - - success_flag = True - - eq12 = self.ticker.split("-") # check if pair or single - # print(eq12) # ['LNT', '1.25*NYSE:FTS'] - - if len(eq12) <= 2: - - eq1_hedge = re.findall( - r"[-+]?\d*\.\d+|\d+", eq12[0] - ) # hedge constant fot the 1st ticker - # print("eq1_hedge: ", eq1_hedge) # [] - - if len(eq1_hedge) > 0: - eq1 = eq12[0].replace(eq1_hedge[0], "") - else: - eq1 = eq12[0] # LNT - - eq1 = eq1.replace("*", "") - # print("eq1: ", eq1) # LNT - - eq1_split = eq1.rsplit(":", maxsplit=1) - eq1_ticker_almost = eq1_split[len(eq1_split) - 1] - - # print("eq1_split: ", eq1_split) # ['LNT'] - # print("eq1_ticker_almost: ", eq1_ticker_almost) # LNT - - if "." in eq1_ticker_almost: # For Class A,B type tickers EXP: BF.A BF.B - ticker_pair1 = eq1_ticker_almost.replace( - ".", " " - ) # convert Tradingview -> IB format - else: - ticker_pair1 = "".join( - char for char in eq1_ticker_almost if char.isalnum() - ) - - if eq1_ticker_almost != ticker_pair1: - success_flag = False - - # print("ticker_pair1: ", ticker_pair1) # LNT - - if len(eq1_hedge) != 0: - if eq1_hedge[0] != 1: - success_flag = False - - # print("problem_flag_first: ", success_flag) - - self.ticker_type = "single" - self.ticker1 = ticker_pair1 - - if len(eq12) == 2: - - eq2_hedge = re.findall( - r"[-+]?\d*\.\d+|\d+", eq12[1] - ) # hedge constant fot the 2nd ticker - # print("eq2_hedge: ", eq2_hedge) # ['1.25'] - - if len(eq2_hedge) > 0: - eq2 = eq12[1].replace(eq2_hedge[0], "") - else: - eq2 = eq12[1] # *NYSE:FTS - - eq2 = eq2.replace("*", "") - - # print("eq2: ", eq2) # NYSE:FTS - - eq2_split = eq2.rsplit(":", maxsplit=1) - eq2_ticker_almost = eq2_split[len(eq2_split) - 1] - - # print("eq2_split: ", eq2_split) # ['NYSE', 'FTS'] - # print("eq2_ticker_almost: ", eq2_ticker_almost) # FTS - - if "." in eq2_ticker_almost: # For Class A,B type tickers EXP: BF.A BF.B - ticker_pair2 = eq2_ticker_almost.replace( - ".", " " - ) # convert Tradingview -> IB format - else: - ticker_pair2 = "".join( - char for char in eq2_ticker_almost if char.isalnum() - ) - - if eq2_ticker_almost != ticker_pair2: - success_flag = False - - # print("ticker_pair2: ", ticker_pair2) # FTS - - if len(eq2_hedge) == 0: - hedge_const = 1 - else: - hedge_const = eq2_hedge[0] - - # print("hedge_const: ", hedge_const) # False - # print("problem_flag_final: ", success_flag) - # print("ticker_type: ", self.ticker_type) - - self.ticker_type = "pair" - self.ticker2 = ticker_pair2 - self.hedge_param = hedge_const - - if len(eq12) > 2: - success_flag = False - - if not success_flag: - self.order_status = "error" - self.status_msg = "problematic ticker!" - - return success_flag - @classmethod def get_avg_slip(cls, ticker_name, start_date, end_date) -> dict: @@ -877,7 +747,6 @@ def get_avg_slip(cls, ticker_name, start_date, end_date) -> dict: db.session.query(db.func.avg(cls.slip)) .filter( (cls.ticker1 == ticker1) - & (cls.ticker2 == ticker2) & (cls.timestamp <= end_date) & (cls.timestamp >= start_date) & (cls.order_action == "buy") @@ -890,7 +759,6 @@ def get_avg_slip(cls, ticker_name, start_date, end_date) -> dict: db.session.query(db.func.avg(cls.slip)) .filter( (cls.ticker1 == ticker1) - & (cls.ticker2 == ticker2) & (cls.timestamp <= end_date) & (cls.timestamp >= start_date) & (cls.order_action == "sell") @@ -903,7 +771,6 @@ def get_avg_slip(cls, ticker_name, start_date, end_date) -> dict: db.session.query(db.func.avg(cls.slip)) .filter( (cls.ticker1 == ticker1) - & (cls.ticker2 == ticker2) & (cls.timestamp <= end_date) & (cls.timestamp >= start_date) ) @@ -923,7 +790,7 @@ def find_by_orderid(cls, orderid) -> "SignalModel": ) # get the most recent order in case of a multiple order id situation @classmethod - # multiple order id situation happens a lot, better to double check the ticker + # multiple order id situation happens a lot, better to double-check the ticker def find_by_orderid_ticker(cls, orderid, ticker) -> "SignalModel": return ( cls.query.filter( @@ -935,7 +802,7 @@ def find_by_orderid_ticker(cls, orderid, ticker) -> "SignalModel": ) # get the most recent order in case of a multiple order id situation @classmethod - def check_timestamp(cls) -> "SignalModel": + def check_latest(cls) -> "SignalModel": return ( cls.query.filter( ( @@ -948,3 +815,131 @@ def check_timestamp(cls) -> "SignalModel": .order_by(cls.rowid.desc()) .first() ) + + # to split stocks only + # def splitticker_stocks( + # self, + # ) -> bool: + # + # # Split the received webhook equation into tickers and hedge parameters + # # Tested with Tradingview webhooks and Interactive Brokers ticker format + # # TESTED FOR THESE EQUATIONS: + # # pair_equation = "TEST 123" + # # pair_equation = "NYSE:LNT" + # # pair_equation = "0.7*NYSE:BF.A" + # # pair_equation = "NYSE:BF.A" + # # pair_equation = "NYSE:LNT-NYSE:FTS*2.2" + # # pair_equation = "NYSE:LNT*2-NYSE:FTS" + # # pair_equation = "NYSE:LNT-NYSE:FTS/3" + # # pair_equation = "1.3*NYSE:LNT-NYSE:FTS*2.2" + # # pair_equation = "NYSE:LNT-1.25*NYSE:FTS" + # # pair_equation = "LNT-1.25*NYSE:FTS" + # # pair_equation = "NYSE:LNT-NYSE:FTS" + # # pair_equation = "BF.A-0.7*NYSE:BF.B" + # + # success_flag = True + # + # eq12 = self.ticker.split("-") # check if pair or single + # # print(eq12) # ['LNT', '1.25*NYSE:FTS'] + # + # if len(eq12) <= 2: + # + # eq1_hedge = re.findall( + # r"[-+]?\d*\.\d+|\d+", eq12[0] + # ) # hedge constant fot the 1st ticker + # # print("eq1_hedge: ", eq1_hedge) # [] + # + # if len(eq1_hedge) > 0: + # eq1 = eq12[0].replace(eq1_hedge[0], "") + # else: + # eq1 = eq12[0] # LNT + # + # eq1 = eq1.replace("*", "") + # # print("eq1: ", eq1) # LNT + # + # eq1_split = eq1.rsplit(":", maxsplit=1) + # eq1_ticker_almost = eq1_split[len(eq1_split) - 1] + # + # # print("eq1_split: ", eq1_split) # ['LNT'] + # # print("eq1_ticker_almost: ", eq1_ticker_almost) # LNT + # + # if "." in eq1_ticker_almost: # For Class A,B type tickers EXP: BF.A BF.B + # ticker_pair1 = eq1_ticker_almost.replace( + # ".", " " + # ) # convert Tradingview -> IB format + # else: + # ticker_pair1 = "".join( + # char for char in eq1_ticker_almost if char.isalnum() + # ) + # + # if eq1_ticker_almost != ticker_pair1: + # success_flag = False + # + # # print("ticker_pair1: ", ticker_pair1) # LNT + # + # if len(eq1_hedge) != 0: + # if eq1_hedge[0] != 1: + # success_flag = False + # + # # print("problem_flag_first: ", success_flag) + # + # self.ticker_type = "single" + # self.ticker1 = ticker_pair1 + # + # if len(eq12) == 2: + # + # eq2_hedge = re.findall( + # r"[-+]?\d*\.\d+|\d+", eq12[1] + # ) # hedge constant fot the 2nd ticker + # # print("eq2_hedge: ", eq2_hedge) # ['1.25'] + # + # if len(eq2_hedge) > 0: + # eq2 = eq12[1].replace(eq2_hedge[0], "") + # else: + # eq2 = eq12[1] # *NYSE:FTS + # + # eq2 = eq2.replace("*", "") + # + # # print("eq2: ", eq2) # NYSE:FTS + # + # eq2_split = eq2.rsplit(":", maxsplit=1) + # eq2_ticker_almost = eq2_split[len(eq2_split) - 1] + # + # # print("eq2_split: ", eq2_split) # ['NYSE', 'FTS'] + # # print("eq2_ticker_almost: ", eq2_ticker_almost) # FTS + # + # if "." in eq2_ticker_almost: # For Class A,B type tickers EXP: BF.A BF.B + # ticker_pair2 = eq2_ticker_almost.replace( + # ".", " " + # ) # convert Tradingview -> IB format + # else: + # ticker_pair2 = "".join( + # char for char in eq2_ticker_almost if char.isalnum() + # ) + # + # if eq2_ticker_almost != ticker_pair2: + # success_flag = False + # + # # print("ticker_pair2: ", ticker_pair2) # FTS + # + # if len(eq2_hedge) == 0: + # hedge_const = 1 + # else: + # hedge_const = eq2_hedge[0] + # + # # print("hedge_const: ", hedge_const) # False + # # print("problem_flag_final: ", success_flag) + # # print("ticker_type: ", self.ticker_type) + # + # self.ticker_type = "pair" + # self.ticker2 = ticker_pair2 + # self.hedge_param = hedge_const + # + # if len(eq12) > 2: + # success_flag = False + # + # if not success_flag: + # self.order_status = "error" + # self.status_msg = "problematic ticker!" + # + # return success_flag diff --git a/models/users.py b/models/users.py index fa3bc64..e646ccc 100644 --- a/models/users.py +++ b/models/users.py @@ -143,10 +143,9 @@ def get_rows(cls, number_of_items) -> List: # # return items - @staticmethod - def delete(username) -> None: + def delete(self) -> None: - db.session.delete(username) + db.session.delete(self) db.session.commit() # KEEPING THE SQL CODE THAT FUNCTIONS THE SAME FOR COMPARISON PURPOSES: diff --git a/notify.py b/notify.py new file mode 100644 index 0000000..c5d0c6f --- /dev/null +++ b/notify.py @@ -0,0 +1,83 @@ +""" +Email Notifications +""" + +from flask_mail import Mail, Message +from app import app, configs +from demo import iftoday, timediff +from models.signals import SignalModel + +# Passphrase is required to register webhooks (& to update account positions & PNL) +PASSPHRASE = configs.get("SECRET", "WEBHOOK_PASSPHRASE") + +# configuration of email +app.config["MAIL_SERVER"] = configs.get("EMAIL", "MAIL_SERVER") +app.config["MAIL_PORT"] = int(configs.get("EMAIL", "MAIL_PORT")) +app.config["MAIL_USERNAME"] = configs.get("EMAIL", "MAIL_USERNAME") +app.config["MAIL_PASSWORD"] = configs.get("EMAIL", "MAIL_PASSWORD") +app.config["MAIL_USE_TLS"] = configs.getboolean("EMAIL", "MAIL_USE_TLS") +app.config["MAIL_USE_SSL"] = configs.getboolean("EMAIL", "MAIL_USE_SSL") + +email_subject = configs.get("EMAIL", "MAIL_SUBJECT") +email_body = configs.get("EMAIL", "MAIL_BODY") +email_sender = configs.get("EMAIL", "MAIL_SENDER") +email_recipient = configs.get("EMAIL", "MAIL_RECIPIENT") + +mail = Mail(app) # instantiate the mail class + + +def get_waiting_time(signal_to_check): + + send_email = False + warned = False + print("*** checking to warn ***") + + if signal_to_check: + if iftoday(str(signal_to_check.timestamp)): + diff = timediff(str(signal_to_check.timestamp)) + + if signal_to_check.error_msg: + if "(warned)" in signal_to_check.error_msg: + warned = True + + if int(diff) > 2 and not warned: + print("*** SENDING EMAIL ****") + send_email = True + + else: + print("*** nothing to warn ***") + + return send_email + + +def warning_email_context(): + + with app.app_context(): # being executed outside the app context + try: + + signal_to_check = SignalModel.check_latest() + + send_email = get_waiting_time(signal_to_check) + + if send_email: + msg = Message( + email_subject, sender=email_sender, recipients=[email_recipient] + ) + msg.body = email_body + mail.send(msg) + print("*** warning email sent...***") + + if signal_to_check.error_msg: + signal_to_check.error_msg = signal_to_check.error_msg + "(warned)" + else: + signal_to_check.error_msg = "(warned)" + + signal_to_check.update(signal_to_check.rowid) + + else: + + print("*** no email ***") + + except Exception as e: + print("*** Email Notification Error ***") + print(e) diff --git a/resources/account.py b/resources/account.py index 1316031..7f75387 100644 --- a/resources/account.py +++ b/resources/account.py @@ -3,6 +3,7 @@ from models.signals import SignalModel from flask_jwt_extended import get_jwt_identity, jwt_required, get_jwt from datetime import datetime +from . import status_codes as status EMPTY_ERR = "'{}' cannot be empty!" PASS_ERR = "incorrect passphrase." @@ -47,9 +48,8 @@ def post(): # format return message inline with flask_restful parser errors if SignalModel.passphrase_wrong(data["passphrase"]): - return_msg = {"message": {"passphrase": PASS_ERR}} - # return {"message": PASS_ERR}, 400 # return Bad Request - return return_msg, 400 # return Bad Request + return_msg = {"message": PASS_ERR} + return return_msg, status.HTTP_401_UNAUTHORIZED # return Unauthorized item = AccountModel( data["timestamp"], @@ -70,12 +70,12 @@ def post(): print("Error occurred - ", e) # better log the errors return ( {"message": INSERT_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return ( {"message": CREATE_OK.format("record")}, - 201, + status.HTTP_201_CREATED, ) # Return Successful Creation of Resource @staticmethod @@ -85,8 +85,7 @@ def put(): # format return message inline with flask_restful parser errors if SignalModel.passphrase_wrong(data["passphrase"]): return_msg = {"message": {"passphrase": PASS_ERR}} - # return {"message": PASS_ERR}, 400 # return Bad Request - return return_msg, 400 # return Bad Request + return return_msg, status.HTTP_401_UNAUTHORIZED # return Unauthotized if AccountModel.find_by_rowid(data["rowid"]): @@ -109,7 +108,7 @@ def put(): print("Error occurred - ", e) return ( {"message": UPDATE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error item.rowid = data["rowid"] @@ -117,7 +116,7 @@ def put(): return return_json - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found class PNLList(Resource): @@ -144,12 +143,12 @@ def get(number_of_items="0"): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # return {'signals': list(map(lambda x: x.json(), items))} # we can map the list of objects, return { - "signals": [item.json() for item in items], + "pnls": [item.json() for item in items], "notoken_limit": notoken_limit, } # this is more readable @@ -165,13 +164,13 @@ def get(rowid): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error if item: return item.json() - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found @staticmethod @jwt_required(fresh=True) # need fresh token @@ -179,9 +178,11 @@ def delete(rowid): claims = get_jwt() - # TODO: consider user to delete own data if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized try: item_to_delete = AccountModel.find_by_rowid(rowid) @@ -189,15 +190,13 @@ def delete(rowid): if item_to_delete: item_to_delete.delete() else: - return {"message": NOT_FOUND} + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND except Exception as e: print("Error occurred - ", e) return ( {"message": DELETE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error - return {"message": DELETE_OK.format("signal")} - - # TODO: add a get(date) function + return {"message": DELETE_OK.format("pnl")} diff --git a/resources/pairs.py b/resources/pairs.py index 0d08462..79c78f2 100644 --- a/resources/pairs.py +++ b/resources/pairs.py @@ -2,6 +2,7 @@ from models.pairs import PairModel from flask_jwt_extended import jwt_required, get_jwt from models.tickers import TickerModel +from . import status_codes as status EMPTY_ERR = "'{}' cannot be empty!" NAME_ERR = "'{}' with that name already exists." @@ -30,33 +31,23 @@ class PairRegister(Resource): "hedge", type=float, required=True, help=EMPTY_ERR.format("hedge") ) parser.add_argument( - "status", - type=int, - default=0, + "status", type=int, default=0, ) parser.add_argument("notes", type=str) parser.add_argument( "contracts", type=int, required=True, help=EMPTY_ERR.format("contracts") ) parser.add_argument( - "act_price", - type=float, - default=0, + "act_price", type=float, default=0, ) parser.add_argument( - "sma", - type=float, - default=0, + "sma", type=float, default=0, ) parser.add_argument( - "sma_dist", - type=float, - default=0, + "sma_dist", type=float, default=0, ) parser.add_argument( - "std", - type=float, - default=0, + "std", type=float, default=0, ) @staticmethod @@ -83,7 +74,7 @@ def post(): if TickerModel.find_active_ticker(item.ticker1, item.ticker2): return ( {"message": STK_ERR}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request try: @@ -94,26 +85,26 @@ def post(): if PairModel.find_by_name(item.name): return ( {"message": NAME_ERR.format("pair")}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request item.insert() else: return ( {"message": TICKER_ERR}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request except Exception as e: print("Error occurred - ", e) # better log the errors return ( {"message": INSERT_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return ( {"message": CREATE_OK.format("pair")}, - 201, + status.HTTP_201_CREATED, ) # Return Successful Creation of Resource @staticmethod @@ -140,7 +131,7 @@ def put(): if TickerModel.find_active_ticker(item.ticker1, item.ticker2): return ( {"message": STK_ERR}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request item_to_return = PairModel.find_by_name(data["name"]) @@ -153,12 +144,12 @@ def put(): print("Error occurred - ", e) return ( {"message": UPDATE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return item_to_return.json() - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found class PairList(Resource): @@ -171,7 +162,7 @@ def get(number_of_items="0"): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # return {'pairs': list(map(lambda x: x.json(), items))} # we can map the list of objects, @@ -191,13 +182,16 @@ def get(name): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error if item: return item.json() - return {"message": "Item not found"}, 404 # Return Not Found + return ( + {"message": "Item not found"}, + status.HTTP_404_NOT_FOUND, + ) # Return Not Found @staticmethod @jwt_required(fresh=True) # need fresh token @@ -207,7 +201,10 @@ def delete(name): # TODO: consider user to delete own data if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized try: item_to_delete = PairModel.find_by_name(name) @@ -215,13 +212,13 @@ def delete(name): if item_to_delete: item_to_delete.delete() else: - return {"message": NOT_FOUND} + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Not Found except Exception as e: print("Error occurred - ", e) return ( {"message": DELETE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return {"message": DELETE_OK.format("pair")} diff --git a/resources/signals.py b/resources/signals.py index bbe2352..9cd517d 100644 --- a/resources/signals.py +++ b/resources/signals.py @@ -3,6 +3,7 @@ from flask_jwt_extended import get_jwt_identity, jwt_required, get_jwt from datetime import datetime import math +from . import status_codes as status EMPTY_ERR = "'{}' cannot be empty!" PASS_ERR = "incorrect passphrase." @@ -26,9 +27,7 @@ class SignalUpdateOrder(Resource): "passphrase", type=str, required=True, help=EMPTY_ERR.format("passphrase") ) parser.add_argument( - "order_id", - type=int, - required=True, + "order_id", type=int, required=True, ) parser.add_argument("symbol", type=str, required=True) parser.add_argument("price", type=float, required=True) @@ -47,8 +46,7 @@ def put(): # format return message inline with flask_restful parser errors if SignalModel.passphrase_wrong(data["passphrase"]): return_msg = {"message": {"passphrase": PASS_ERR}} - # return {"message": PASS_ERR}, 400 # return Bad Request - return return_msg, 400 # return Bad Request + return return_msg, status.HTTP_401_UNAUTHORIZED # Unauthorized # get signal with rowid item = SignalModel.find_by_orderid_ticker(data["order_id"], data["symbol"]) @@ -71,7 +69,10 @@ def put(): order_contracts_old - item.order_contracts ) else: - return {"message": PART_ERR} # return Bad Request + return ( + {"message": PART_ERR}, + status.HTTP_400_BAD_REQUEST, + ) # return Bad Request else: if item.order_status != "filled": @@ -166,7 +167,7 @@ def put(): print("Error occurred - ", e) return ( {"message": UPDATE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return_json = item.json() @@ -175,7 +176,7 @@ def put(): return return_json - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found class SignalWebhook(Resource): @@ -194,10 +195,7 @@ class SignalWebhook(Resource): "ticker", type=str, required=True, help=EMPTY_ERR.format("ticker") ) parser.add_argument( - "order_action", - type=str, - required=True, - help=EMPTY_ERR.format("order_action"), + "order_action", type=str, required=True, help=EMPTY_ERR.format("order_action"), ) parser.add_argument( "order_contracts", @@ -206,24 +204,19 @@ class SignalWebhook(Resource): help=EMPTY_ERR.format("order_contracts"), ) parser.add_argument( - "order_price", - type=float, + "order_price", type=float, ) parser.add_argument( - "mar_pos", - type=str, + "mar_pos", type=str, ) parser.add_argument( - "mar_pos_size", - type=int, + "mar_pos_size", type=int, ) parser.add_argument( - "pre_mar_pos", - type=str, + "pre_mar_pos", type=str, ) parser.add_argument( - "pre_mar_pos_size", - type=int, + "pre_mar_pos_size", type=int, ) parser.add_argument("order_comment", type=str, default="") parser.add_argument("order_status", type=str, default="waiting") @@ -249,9 +242,8 @@ def post(): # format return message inline with flask_restful parser errors if SignalModel.passphrase_wrong(data["passphrase"]): - return_msg = {"message": {"passphrase": PASS_ERR}} - # return {"message": PASS_ERR}, 400 # Old return Bad Request - return return_msg, 400 # Old return Bad Request + return_msg = {"message": PASS_ERR} + return return_msg, status.HTTP_401_UNAUTHORIZED # Return Unauthorized item = SignalModel( data["timestamp"], @@ -293,23 +285,29 @@ def post(): if not ticker_ok: # keep the ticker record in the database, but change the message # do not return bad request - return {"message": TICKER_ERR}, 201 # Created but need attention + return ( + {"message": TICKER_ERR}, + status.HTTP_201_CREATED, + ) # Created but need attention if not active_ok: # keep the ticker record in the database, but change the message # do not return bad request - return {"message": ACTIVE_ERR}, 201 # Created but need attention + return ( + {"message": ACTIVE_ERR}, + status.HTTP_201_CREATED, + ) # Created but need attention except Exception as e: print("Error occurred - ", e) # better log the errors return ( {"message": INSERT_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return ( {"message": CREATE_OK.format("signal")}, - 201, + 200, ) # Return Successful Creation of Resource @staticmethod @@ -319,8 +317,7 @@ def put(): # format return message inline with flask_restful parser errors if SignalModel.passphrase_wrong(data["passphrase"]): return_msg = {"message": {"passphrase": PASS_ERR}} - # return {"message": PASS_ERR}, 400 # Old return Bad Request - return return_msg, 400 # Old return Bad Request + return return_msg, status.HTTP_401_UNAUTHORIZED # Return Unauthorized if SignalModel.find_by_rowid(data["rowid"]): @@ -354,9 +351,7 @@ def put(): item.splitticker() # check webhook ticker validity # if you need to bypass active ticker status check - if data["bypass_ticker_status"]: - pass - else: + if not data["bypass_ticker_status"]: item.check_ticker_status() item.update(data["rowid"]) @@ -365,7 +360,7 @@ def put(): print("Error occurred - ", e) return ( {"message": UPDATE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error item.rowid = data["rowid"] @@ -375,7 +370,7 @@ def put(): return return_json - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found class SignalList(Resource): @@ -402,7 +397,7 @@ def get(number_of_items="0"): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # return {'signals': list(map(lambda x: x.json(), items))} # we can map the list of objects, @@ -436,7 +431,7 @@ def get(ticker_name, number_of_items="0"): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # return {'signals': list(map(lambda x: x.json(), items))} # we can map the list of objects, @@ -472,7 +467,7 @@ def get(order_status, number_of_items="0"): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # return {'signals': list(map(lambda x: x.json(), items))} # we can map the list of objects, @@ -492,13 +487,13 @@ def get(rowid): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error if item: return item.json() - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found @staticmethod @jwt_required(fresh=True) # need fresh token @@ -508,7 +503,10 @@ def delete(rowid): # TODO: consider user to delete own data if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized try: item_to_delete = SignalModel.find_by_rowid(rowid) @@ -516,13 +514,13 @@ def delete(rowid): if item_to_delete: item_to_delete.delete() else: - return {"message": NOT_FOUND} + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Not Found except Exception as e: print("Error occurred - ", e) return ( {"message": DELETE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return {"message": DELETE_OK.format("signal")} diff --git a/resources/tickers.py b/resources/tickers.py index 600017f..f02c8f6 100644 --- a/resources/tickers.py +++ b/resources/tickers.py @@ -3,6 +3,7 @@ from flask_jwt_extended import jwt_required, get_jwt from models.pairs import PairModel from models.signals import SignalModel +from . import status_codes as status EMPTY_ERR = "'{}' cannot be empty!" NAME_ERR = "'{}' with that name already exists." @@ -36,9 +37,8 @@ def put(): # format return message inline with flask_restful parser errors if SignalModel.passphrase_wrong(data["passphrase"]): - return_msg = {"message": {"passphrase": PASS_ERR}} - # return {"message": PASS_ERR}, 400 # return Bad Request - return return_msg, 400 # return Bad Request + return_msg = {"message": PASS_ERR} + return return_msg, status.HTTP_400_BAD_REQUEST # return Bad Request # get ticker with symbol item = TickerModel.find_by_symbol(data["symbol"]) @@ -56,12 +56,12 @@ def put(): print("Error occurred - ", e) return ( {"message": UPDATE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return item.json() - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found class TickerRegister(Resource): @@ -89,7 +89,7 @@ def post(): if TickerModel.find_by_symbol(data["symbol"]): return ( {"message": NAME_ERR.format("ticker")}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request item = TickerModel( @@ -110,7 +110,7 @@ def post(): if PairModel.find_active_ticker(item.symbol): return ( {"message": TICKR_ERR}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request try: @@ -120,12 +120,12 @@ def post(): print("Error occurred - ", e) # better log the errors return ( {"message": INSERT_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return ( {"message": CREATE_OK.format("ticker")}, - 201, + status.HTTP_201_CREATED, ) # Return Successful Creation of Resource @staticmethod @@ -151,7 +151,7 @@ def put(): if PairModel.find_active_ticker(item.symbol): return ( {"message": TICKR_ERR}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request item_to_update = TickerModel.find_by_symbol(data["symbol"]) @@ -165,7 +165,7 @@ def put(): print("Error occurred - ", e) return ( {"message": UPDATE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return item_to_update.json() @@ -177,12 +177,12 @@ def put(): print("Error occurred - ", e) return ( {"message": INSERT_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return ( {"message": CREATE_OK.format("ticker")}, - 201, + status.HTTP_201_CREATED, ) # Return Successful Creation of Resource @@ -196,7 +196,7 @@ def get(number_of_items="0"): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # return {'tickers': list(map(lambda x: x.json(), items))} # we can map the list of objects, @@ -216,13 +216,13 @@ def get(symbol): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error if item: return item.json() - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found @staticmethod @jwt_required(fresh=True) # need fresh token @@ -232,7 +232,10 @@ def delete(symbol): # TODO: consider user to delete own data if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized try: item_to_delete = TickerModel.find_by_symbol(symbol) @@ -240,13 +243,16 @@ def delete(symbol): if item_to_delete: item_to_delete.delete() else: - return {"message": NOT_FOUND} + return ( + {"message": NOT_FOUND}, + status.HTTP_404_NOT_FOUND, + ) # Return Not Found except Exception as e: print("Error occurred - ", e) return ( {"message": DELETE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return {"message": DELETE_OK.format("ticker")} diff --git a/resources/users.py b/resources/users.py index 6c8d2a6..f81d08c 100644 --- a/resources/users.py +++ b/resources/users.py @@ -3,6 +3,7 @@ from flask_restful import Resource, reqparse from datetime import datetime import pytz +from . import status_codes as status # enable if using sessions: # from flask import session @@ -67,14 +68,17 @@ def post(): claims = get_jwt() if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized data = _parser.parse_args() if UserModel.find_by_username(data["username"]): return ( {"message": USERNAME_ERR.format("username")}, - 400, + status.HTTP_400_BAD_REQUEST, ) # Return Bad Request item = UserModel(data["username"], data["password"]) @@ -86,12 +90,12 @@ def post(): print("Error occurred - ", e) # better log the errors return ( {"message": INSERT_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return ( {"message": CREATE_OK.format("user")}, - 201, + status.HTTP_201_CREATED, ) # Return Successful Creation of Resource @staticmethod @@ -101,7 +105,10 @@ def put(): claims = get_jwt() if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized data = _parser.parse_args() @@ -115,7 +122,7 @@ def put(): print("Error occurred - ", e) return ( {"message": UPDATE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return item.json() @@ -127,12 +134,12 @@ def put(): print("Error occurred - ", e) return ( {"message": INSERT_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return ( {"message": CREATE_OK.format("user")}, - 201, + status.HTTP_201_CREATED, ) # Return Successful Creation of Resource @@ -144,7 +151,10 @@ def get(number_of_users="0"): claims = get_jwt() if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized try: users = UserModel.get_rows(number_of_users) @@ -153,7 +163,7 @@ def get(number_of_users="0"): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # return {'users': list(map(lambda x: x.json(), users))} # we can map the list of objects, @@ -170,7 +180,10 @@ def get(username): claims = get_jwt() if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized try: item = UserModel.find_by_username(username) @@ -179,13 +192,13 @@ def get(username): print("Error occurred - ", e) return ( {"message": GET_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error if item: return item.json() - return {"message": NOT_FOUND}, 404 # Return Not Found + return {"message": NOT_FOUND}, status.HTTP_404_NOT_FOUND # Return Not Found @staticmethod @jwt_required(fresh=True) # need fresh token @@ -194,16 +207,27 @@ def delete(username): claims = get_jwt() if not claims["is_admin"]: - return {"message": PRIV_ERR.format("admin")}, 401 # Return Unauthorized + return ( + {"message": PRIV_ERR.format("admin")}, + status.HTTP_401_UNAUTHORIZED, + ) # Return Unauthorized try: - UserModel.delete(username) + item_to_delete = UserModel.find_by_username(username) + + if item_to_delete: + UserModel.delete(item_to_delete) + else: + return ( + {"message": NOT_FOUND}, + status.HTTP_404_NOT_FOUND, + ) # Return Not Found except Exception as e: print("Error occurred - ", e) return ( {"message": DELETE_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error return {"message": DELETE_OK.format("user")} @@ -236,24 +260,21 @@ def post(): print("Error occurred - ", e) return ( {"message": SESSION_ERR}, - 500, + status.HTTP_500_INTERNAL_SERVER_ERROR, ) # Return Interval Server Error # (flask-session-change)enable if using flask sessions: # session["token"] = "yes_token" # store token, use it as a dict - return ( - { - # 'access_token': access_token.decode('utf-8'), # token needs to be JSON serializable - # 'refresh_token': refresh_token.decode('utf-8'), # for earlier versions of pyjwt - "access_token": access_token, - "refresh_token": refresh_token, - "expire": data["expire"], - }, - 200, - ) + return { + # 'access_token': access_token.decode('utf-8'), # token needs to be JSON serializable + # 'refresh_token': refresh_token.decode('utf-8'), # for earlier versions of pyjwt + "access_token": access_token, + "refresh_token": refresh_token, + "expire": data["expire"], + } - return {"message": INVAL_ERR}, 401 + return {"message": INVAL_ERR}, status.HTTP_401_UNAUTHORIZED class UserLogout(Resource): @@ -271,7 +292,7 @@ def post(): # (flask-session-change) enable if using flask sessions to end session: # session["token"] = None - return {"message": LOGOUT_OK}, 200 + return {"message": LOGOUT_OK} class TokenRefresh(Resource): @@ -284,4 +305,4 @@ def post(): ) # Create not fresh Token if fresh=False refresh_token = create_refresh_token(current_user) - return {"access_token": new_token, "refresh_token": refresh_token}, 200 + return {"access_token": new_token, "refresh_token": refresh_token} diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..c945fdd --- /dev/null +++ b/routes.py @@ -0,0 +1,60 @@ +""" +Resource Definitions for API +""" +from app import app +from flask_restful import Api +from resources.pairs import PairRegister, PairList, Pair +from resources.signals import ( + SignalWebhook, + SignalUpdateOrder, + SignalList, + SignalListTicker, + SignalListStatus, + Signal, +) +from resources.tickers import TickerRegister, TickerUpdatePNL, TickerList, Ticker +from resources.users import ( + UserRegister, + UserList, + User, + UserLogin, + UserLogout, + TokenRefresh, +) +from resources.account import PNLRegister, PNLList, PNL + + +api = Api(app) + + +api.add_resource(SignalWebhook, "/v4/webhook") +api.add_resource(SignalUpdateOrder, "/v4/signal/order") +api.add_resource(SignalList, "/v4/signals/") +api.add_resource( + SignalListStatus, + "/v4/signals/status//", +) +api.add_resource( + SignalListTicker, "/v4/signals/ticker//" +) +api.add_resource(Signal, "/v4/signal/") + +api.add_resource(PairRegister, "/v4/pair") +api.add_resource(PairList, "/v4/pairs/") +api.add_resource(Pair, "/v4/pair/") + +api.add_resource(TickerRegister, "/v4/ticker") +api.add_resource(TickerUpdatePNL, "/v4/ticker/pnl") +api.add_resource(TickerList, "/v4/tickers/") +api.add_resource(Ticker, "/v4/ticker/") + +api.add_resource(UserRegister, "/v4/user") +api.add_resource(UserList, "/v4/users/") +api.add_resource(User, "/v4/user/") +api.add_resource(UserLogin, "/v4/login") +api.add_resource(UserLogout, "/v4/logout") +api.add_resource(TokenRefresh, "/v4/refresh") + +api.add_resource(PNLRegister, "/v4/pnl") +api.add_resource(PNLList, "/v4/pnls/") +api.add_resource(PNL, "/v4/pnl/") diff --git a/security.py b/security.py new file mode 100644 index 0000000..1ce93cf --- /dev/null +++ b/security.py @@ -0,0 +1,38 @@ +""" +This module applies the security policy to the API and the frontend demo +""" + +# SeaSurf is preventing cross-site request forgery (CSRF) +# Talisman handles setting HTTP headers to protect against common security issues +# flask_cors handles Cross Origin Resource Sharing (CORS), making cross-origin AJAX possible +from flask_seasurf import SeaSurf +from flask_talisman import Talisman +from flask_cors import CORS +from app import app + +# define a content security policy for the script, styles & font sources used for the demo +SELF = "'self'" +csp = { + "default-src": SELF, + "img-src": "*", + "script-src": [SELF, "ajax.googleapis.com"], + "style-src": [SELF,], + "font-src": [SELF,], +} + +nonce_list = ["default-src", "script-src"] + +csrf = SeaSurf(app) + +# add csrf exception routes. +# these routes are to be reached from external sources with a passphrase +csrf._exempt_urls = ( + "/v4/webhook", + "/v4/signal/order", + "/v4/ticker/pnl", +) + +talisman = Talisman( + app, content_security_policy=csp, content_security_policy_nonce_in=nonce_list +) +CORS(app) diff --git a/static/pairs.js b/static/pairs.js index 46ef392..fe472d9 100644 --- a/static/pairs.js +++ b/static/pairs.js @@ -4,7 +4,7 @@ // define api constants for the pairs: const api_url_get_pair= server_url +'v4/pair/'; const api_url_get_all_pairs= server_url +'v4/pairs/0';// "0" for all pairs. -const api_url_post_put_pair= server_url +'v4/regpair'; +const api_url_post_put_pair= server_url +'v4/pair'; // define other api constants (defining as a separate constant to be used as a standalone script): const api_url_get_all_tickers= server_url +'v4/tickers/0'; @@ -20,6 +20,16 @@ form_pairs.addEventListener('submit', handleFormSubmit_pairs); const form_update_pairs = document.querySelector('.pairs-update'); form_update_pairs.addEventListener('submit', handleFormSubmit_pairs_update); +// create event listener for dynamic list content +function dynamiclistener_pair(id) { + document.getElementById(id).addEventListener('click', dynamicHandler_pair); +} + +function dynamicHandler_pair(event) { + Update_pair(this) +} + + // dynamic objects // TO-DO: define all var ticker1 = document.getElementById("ticker1"); @@ -140,14 +150,11 @@ function handleFormSubmit_pairs(event) { const results = document.querySelector('.results-pairs pre'); document.getElementById("jsontext-pairs").style.display = 'block'; - - // Uppercase JSON string - no need - // uppercase_json = JSON.parse(JSON.stringify(formJSON_pairs, function(a, b) { - // return typeof b === "string" ? b.toUpperCase() : b - // })); // show JSON string before sending - results.innerText = JSON.stringify(formJSON_pairs, null, 2); + var formJSON_pairs_show = JSON.parse(JSON.stringify(formJSON_pairs)); //new json object here + delete formJSON_pairs_show['_csrf_token']; + results.innerText = JSON.stringify(formJSON_pairs_show, null, 2); // create post & save button createButton_pairs(); @@ -179,14 +186,11 @@ function handleFormSubmit_pairs_update(event) { formJSON_update_pairs = Object.fromEntries(data.entries()); document.getElementById("jsontext-pairs-update").style.display = 'block'; - - // Uppercase JSON string - no need - // uppercase_json = JSON.parse(JSON.stringify(formJSON_update_pairs, function(a, b) { - // return typeof b === "string" ? b.toUpperCase() : b - // })); - // show JSON string before sending - results.innerText = JSON.stringify(formJSON_update_pairs, null, 2); + // show JSON string before sending + var formJSON_update_pairs_show = JSON.parse(JSON.stringify(formJSON_update_pairs)); //new json object here + delete formJSON_update_pairs_show['_csrf_token']; + results.innerText = JSON.stringify(formJSON_update_pairs_show, null, 2); // create put & update button createUpdateButton_pairs(); @@ -285,18 +289,20 @@ function postSave_pairs() { // create span element according to pair status if (jsonResponse.status) { - li.innerHTML = ""; + li.innerHTML = ""; } else if (pairs_data.pairs[key].status==0) { - li.innerHTML = ""; + li.innerHTML = ""; } else { - li.innerHTML = ""; + li.innerHTML = ""; } li.setAttribute('id', pair_text); li.appendChild(document.createTextNode(pair_text)); - li.setAttribute("onclick", "Update_pair(this)"); pairlist.insertBefore(li, pairlist.firstChild); + // add onclick event listener for each list element + dynamiclistener_signal(pair_text) + createPages_pairs(); } else { @@ -434,17 +440,18 @@ async function listPairs() { // create span element according to pair status if (pairs_data.pairs[key].status==1) { - li.innerHTML = ""; + li.innerHTML = ""; } else if (pairs_data.pairs[key].status==0) { - li.innerHTML = ""; + li.innerHTML = ""; } else { - li.innerHTML = ""; + li.innerHTML = ""; } - + //create list elements li.setAttribute('id', str); li.appendChild(document.createTextNode(str)); - li.setAttribute("onclick", "Update_pair(this)"); pairlist.appendChild(li); + // add onclick event listener for each list element + dynamiclistener_pair(str) } } @@ -534,16 +541,16 @@ function Update_pair(currentEl){ getPair(pairname); } -function alertBefore_pairs() { +function alertBefore_pairs(csrf_token) { if (confirm("Do you want to delete selected pair?") == true) { - deletePair() + deletePair(csrf_token) } else { return } } -function deletePair() { +function deletePair(csrf_token) { // check token status if (!localStorage.access_token) { @@ -557,16 +564,21 @@ function deletePair() { return } - + alert("Sending DELETE request for: " + pair_update.value); var api_url_delete_pair = api_url_get_pair + pair_update.value + body_msg = {_csrf_token: csrf_token } + fetch(api_url_delete_pair, { method: "DELETE", headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.access_token, }, + body: JSON.stringify(body_msg) + }).then(response => { if (response.status >= 200 && response.status <= 401) { return_code = response.status; diff --git a/static/signals.js b/static/signals.js index bb7ab5c..d17bbb8 100644 --- a/static/signals.js +++ b/static/signals.js @@ -28,6 +28,15 @@ for(const radioButton of radioButtons){ radioButton.addEventListener('change', checkTradeType); } +// create event listener for dynamic list content +function dynamiclistener_signal(id) { + document.getElementById(id).addEventListener('click', dynamicHandler_signal); +} + +function dynamicHandler_signal(event) { + Update_signal(this) +} + // define dynamic objects // TO-DO: define all @@ -642,25 +651,25 @@ async function listSignals() { str = str.toUpperCase() if (signals_data.signals[key].order_status.includes("waiting") || signals_data.signals[key].order_status.includes("rerouted")) { - li.innerHTML = ""+ signals_data.signals[key].rowid +""; + li.innerHTML = ""+ signals_data.signals[key].rowid +""; } else if (signals_data.signals[key].order_status.includes("err")) { // show error messages in a tip box - li.innerHTML = ""+ signals_data.signals[key].rowid +"" + signals_data.signals[key].error_msg + ""; + li.innerHTML = ""+ signals_data.signals[key].rowid +"" + signals_data.signals[key].error_msg + ""; } else if (signals_data.signals[key].order_status.includes("cancel")) { - li.innerHTML = ""+ signals_data.signals[key].rowid +"" + signals_data.signals[key].error_msg + ""; + li.innerHTML = ""+ signals_data.signals[key].rowid +"" + signals_data.signals[key].error_msg + ""; } else if (signals_data.signals[key].order_status.includes("filled")) { - li.innerHTML = ""+ signals_data.signals[key].rowid +""; + li.innerHTML = ""+ signals_data.signals[key].rowid +""; } else if (signals_data.signals[key].order_status.includes("created")) { - li.innerHTML = ""+ signals_data.signals[key].rowid +"" + signals_data.signals[key].error_msg + ""; + li.innerHTML = ""+ signals_data.signals[key].rowid +"" + signals_data.signals[key].error_msg + ""; }else { li.innerHTML = ""+ signals_data.signals[key].rowid +""; } - - - li.setAttribute('id', signals_data.signals[key].rowid); + //create list elements + li.setAttribute('id', signals_data.signals[key].rowid); li.appendChild(document.createTextNode(str)); - li.setAttribute("onclick", "Update_signal(this)"); signallist.appendChild(li); + // add onclick event listener for each list element + dynamiclistener_signal(signals_data.signals[key].rowid) } } @@ -680,6 +689,7 @@ async function listSignals() { } + function Update_signal(currentEl){ // get signal text @@ -743,16 +753,16 @@ async function getSignal(rowid) { } -function alertBefore_signals() { +function alertBefore_signals(csrf_token) { if (confirm("Do you want to delete selected signal?") == true) { - deleteSignal() + deleteSignal(csrf_token) } else { return } } -function deleteSignal() { +function deleteSignal(csrf_token) { // check token status if (!localStorage.access_token) { @@ -771,11 +781,17 @@ function deleteSignal() { var api_url_delete_signal = api_url_get_signal + rowid_update.value + body_msg = {_csrf_token: csrf_token } + + alert(JSON.stringify(body_msg)) fetch(api_url_delete_signal, { method: "DELETE", headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.access_token, }, + body: JSON.stringify(body_msg) + }).then(response => { if (response.status >= 200 && response.status <= 401) { return_code = response.status; diff --git a/static/style.css b/static/style.css index 406ad34..efd4232 100644 --- a/static/style.css +++ b/static/style.css @@ -156,12 +156,11 @@ input { display: table-cell; } font-size: 90%; } - - .ticker { border-radius: 5%; box-sizing: border-box; width: 90px; + text-transform: uppercase; } .sectype { @@ -502,21 +501,30 @@ button:hover { .settingsbutton { + height: 30px; float: right; right: 80px; - font-size: 80%; margin-right: 0.5%; - padding: 4px; + padding: 0px; color:black; - border:2px solid black; + border:0px solid black; border-radius:5px; - box-shadow: 0 0 5px -1px rgba(0,0,0,0.2); cursor:default; - vertical-align:middle; + vertical-align:middletop; text-align: center; letter-spacing:0.5px; +} +.settingicon { + font-size: 180%; + font-weight: bold; + color:black; + left: 0px; + top: -8px; + position: relative; } + + .loginmsg { float: right; @@ -567,6 +575,7 @@ ul.no-bullets { list-style-type: none; /* Remove bullets */ padding: 0; /* Remove padding */ margin: 0; /* Remove margins */ + cursor: pointer; } .column li:hover{ @@ -579,6 +588,11 @@ ul.no-bullets { display: none; } +.poslegend { + font-size: 150%; + text-align: center; +} + .round { height: 10px; width: 10px; @@ -587,10 +601,165 @@ ul.no-bullets { display: inline-block; border:1px solid grey; margin: 2px; + vertical-align:middle + +} + +.roundgreen { + height: 10px; + width: 10px; + background-color: yellowgreen; + border-radius: 50%; + display: inline-block; + border:1px solid grey; + margin: 2px; + vertical-align:text-top; + +} + +.roundred { + height: 10px; + width: 10px; + background-color: lightcoral; + border-radius: 50%; + display: inline-block; + border:1px solid grey; + margin: 2px; vertical-align:text-top; } +.roundyellow { + height: 10px; + width: 10px; + background-color: yellow; + border-radius: 50%; + display: inline-block; + border:1px solid grey; + margin: 2px; + vertical-align:text-top; + +} + +.tdorange { + background-color:orange; + text-align: center; +} + +.tdblue { + background-color:lightblue; +} + +.tdbluegroove { + background-color:lightblue; + border-right-style: groove; +} + +.tdbluegroove2 { + color:darkblue; + font-weight:bold; + border-right-style: groove; +} + +.tdpeachgroove { + background-color:peachpuff; + border-right-style: groove; +} + + +.tdyellow { + background-color:lightgoldenrodyellow; +} + +.tdlightyellow { + background-color:lightyellow; + text-align: center; +} + +.tdwhite{ + background-color:whitesmoke; +} + +.tdgreen1 { + background-color:greenyellow; +} + +.tdgreen2 { + background-color:lightgreen; +} + +.tdgreen3 { + background-color:palegreen; +} + +.tdpink { + background-color:lightpink; +} + +.tdtoday { + background-color:#dcf5f4; +} +.tdredbold { + color:darkred; + font-weight:bold; + +} +.tdredboldgroove { + color:darkred; + font-weight:bold; + border-right-style: groove; +} + +.tdgreenbold { + color:darkred; + font-weight:bold; +} + +.tdgreenboldgroove { + color:darkred; + font-weight:bold; + border-right-style: groove; +} +.tdblueyellowgroove { + color:darkblue; + font-weight:bold; + background-color:lightyellow; + border-right-style: groove; +} + +.tdblueredgroove { + color:darkblue; + font-weight:bold; + background-color:orangered; + border-right-style: groove; +} + +.tdblueorangegroove { + color:darkblue; + font-weight:bold; + background-color:orange; + border-right-style: groove; +} + +.tdblueyellowgroove { + color:darkblue; + font-weight:bold; + background-color:lightyellow; + border-right-style: groove; +} + +.tdgroove { + border-right-style: groove; +} + +.tdcenter { + text-align: center; +} + +.tdleft { + text-align: left; +} + .numberCircle { height: 14px; width: 18px; @@ -604,6 +773,142 @@ ul.no-bullets { font-size: 75%; } +.numberCircleWhite { + height: 14px; + width: 18px; + background-color: whitesmoke; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.numberCircleOrange { + height: 14px; + width: 18px; + background-color: orange; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.numberCircleYellow { + height: 14px; + width: 18px; + background-color: lightgoldenrodyellow; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.numberCircleYellow2 { + height: 14px; + width: 18px; + background-color: lightyellow; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.numberCircleBlue{ + height: 14px; + width: 18px; + background-color: lightblue; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.numberCircleGreen { + height: 14px; + width: 18px; + background-color: lightgreen; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.numberCircleGreen2 { + height: 14px; + width: 18px; + background-color: palegreen; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.numberCirclePink { + height: 14px; + width: 18px; + background-color: lightpink; + border-radius: 20%; + display: inline-block; + border:1px solid grey; + margin: 1.5px; + vertical-align:text-top; + text-align: center; + font-size: 75%; +} + +.divlist1 { + width: 100%; + font-size: medium; +} + +.divlist2 { + width: 50%; + height: 100px; + float: left; +} + +.divlist3 { + margin-left: 90%; + height: 100px; + border-style: groove; + text-align: left; + font-size: medium; +} + +.setupdiv1 { + border-bottom:1px solid darkgrey; +} + +.floatleftnodisplay { + float: left; display: none; +} + +.floatleft { + float: left; +} + + .pages { display: none; font-size: 90%; @@ -617,8 +922,40 @@ ul.no-bullets { cursor:default; text-align: center; letter-spacing:0.5px; +} +.pagesleft { + display: none; + font-size: 90%; + margin: 3px; + padding: 1px; + color:black; + border:1px solid black; + border-radius:1px; + background: whitesmoke; + box-shadow: 0 0 5px -1px rgba(0,0,0,0.2); + cursor:default; + text-align: center; + letter-spacing:0.5px; + float: left; +} + +.pagesright { + display: none; + font-size: 90%; + margin: 3px; + padding: 1px; + color:black; + border:1px solid black; + border-radius:1px; + background: whitesmoke; + box-shadow: 0 0 5px -1px rgba(0,0,0,0.2); + cursor:default; + text-align: center; + letter-spacing:0.5px; + float: right; } + .legend { float: center; font-size: 80%; diff --git a/static/tickers.js b/static/tickers.js index e80b7d0..bb4617e 100644 --- a/static/tickers.js +++ b/static/tickers.js @@ -4,7 +4,7 @@ // define api constants for the tickers: const api_url_get= server_url +'v4/ticker/'; const api_url_get_all= server_url +'v4/tickers/0';// "0" for all tickers -const api_url_post_put= server_url +'v4/regticker'; +const api_url_post_put= server_url +'v4/ticker'; // form data to be collected in these variables: var formJSON; @@ -17,6 +17,16 @@ form.addEventListener('submit', handleFormSubmit); const form_update = document.querySelector('.tickers-update'); form_update.addEventListener('submit', handleFormSubmit_update); +// create event listener for dynamic list content +function dynamiclistener_ticker(id) { + document.getElementById(id).addEventListener('click', dynamicHandler_ticker); +} + +function dynamicHandler_ticker(event) { + Update(this) +} + + // dynamic objects // TO-DO: define all var ticker = document.getElementById("ticker"); @@ -112,13 +122,23 @@ function handleFormSubmit(event) { document.getElementById("jsontext").style.display = 'block'; + // copy token to add later + csrf_token_copy = formJSON['_csrf_token']; + // Uppercase JSON string - uppercase_json = JSON.parse(JSON.stringify(formJSON, function(a, b) { + var uppercase_json = JSON.parse(JSON.stringify(formJSON, function(a, b) { return typeof b === "string" ? b.toUpperCase() : b })); + // show text without token + delete uppercase_json['_csrf_token']; results.innerText = JSON.stringify(uppercase_json, null, 2); + // add token + uppercase_json['_csrf_token'] = csrf_token_copy; + + formJSON = uppercase_json + // create post & save button createButton(); } @@ -137,6 +157,9 @@ function handleFormSubmit_update(event) { const results = document.querySelector('.results-update pre'); document.getElementById("jsontext-update").style.display = 'block'; + + // copy token to add later + csrf_token_copy = formJSON_update['_csrf_token']; // Uppercase JSON string uppercase_json = JSON.parse(JSON.stringify(formJSON_update, function(a, b) { @@ -144,8 +167,15 @@ function handleFormSubmit_update(event) { })); // show JSON string before sending + delete uppercase_json['_csrf_token']; results.innerText = JSON.stringify(uppercase_json, null, 2); + // add token + uppercase_json['_csrf_token'] = csrf_token_copy; + + formJSON_update = uppercase_json + alert(JSON.stringify(formJSON_update)) + // create put & update button createUpdateButton(); } @@ -209,17 +239,13 @@ function postSave() { ticker_text = (formJSON.symbol + "-" + formJSON.xch + "-" + formJSON.prixch).toUpperCase(); alert("Sending POST request for: " + ticker_text); - var body_msg = JSON.stringify(formJSON, function(a, b) { - return typeof b === "string" ? b.toUpperCase() : b - }); - fetch(api_url_post_put, { method: "POST", headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.access_token, }, - body: body_msg + body: JSON.stringify(formJSON) }).then(response => { if (response.status >= 200 && response.status <= 401) { return_code = response.status; @@ -244,9 +270,9 @@ function postSave() { // create span element according to ticker status if (jsonResponse.active) { - li.innerHTML = ""; + li.innerHTML = ""; } else { - li.innerHTML = ""; + li.innerHTML = ""; } li.setAttribute('id', ticker.value.toUpperCase()); @@ -295,17 +321,13 @@ function putUpdate() { alert("Sending PUT request for: " + ticker_text); - var body_msg = JSON.stringify(formJSON_update, function(a, b) { - return typeof b === "string" ? b.toUpperCase() : b - }); - fetch(api_url_post_put, { method: "PUT", headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.access_token, }, - body: body_msg + body: JSON.stringify(formJSON_update) }).then(response => { if (response.status >= 200 && response.status <= 401) { return_code = response.status; @@ -359,17 +381,19 @@ async function getTickers() { // create span element according to ticker status if (tickers_data.tickers[key].active) { - li.innerHTML = ""; + li.innerHTML = ""; } else { - li.innerHTML = ""; + li.innerHTML = ""; } str = tickers_data.tickers[key].symbol li.setAttribute('id', str); li.appendChild(document.createTextNode(str)); - li.setAttribute("onclick", "Update(this)"); stklist.appendChild(li); + // add onclick event listener for each list element + dynamiclistener_ticker(str) + } } @@ -456,16 +480,16 @@ function Update(currentEl){ getTicker(symbol); } -function alertBefore() { +function alertBefore(csrf_token) { if (confirm("Do you want to delete selected ticker?") == true) { - deleteTicker() + deleteTicker(csrf_token) } else { return } } -function deleteTicker() { +function deleteTicker(csrf_token) { // check token status if (!localStorage.access_token) { @@ -484,11 +508,16 @@ function deleteTicker() { var api_url_delete_ticker = api_url_get + ticker_update.value + body_msg = {_csrf_token: csrf_token } + fetch(api_url_delete_ticker, { method: "DELETE", headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + localStorage.access_token, }, + body: JSON.stringify(body_msg) + }).then(response => { if (response.status >= 200 && response.status <= 401) { return_code = response.status; diff --git a/static/users.js b/static/users.js index 3c0506a..5366c59 100644 --- a/static/users.js +++ b/static/users.js @@ -43,7 +43,9 @@ async function getToken() { fetch(api_url_post_login, { method: "POST", - headers: {'Content-Type': 'application/json'}, + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify(userpassJSON) }).then(response => { if (response.status == 200) { diff --git a/templates/base.html b/templates/base.html index c8cdab1..4dee28c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,8 +4,7 @@ {{ title }} - - + @@ -57,22 +56,14 @@

Signals

{% endblock %} - - + {% block listscript %} {% endblock %} diff --git a/templates/dash.html b/templates/dash.html index 1116cd4..0ca8972 100644 --- a/templates/dash.html +++ b/templates/dash.html @@ -17,7 +17,7 @@ {{ signal.rowid }} {% if signal.timestamp|iftoday %} - {{ signal.timestamp|pct_time("UTC")}} + {{ signal.timestamp|pct_time("UTC")}} {% else %} {{ signal.timestamp|pct_time("UTC")}} {% endif %} @@ -27,17 +27,17 @@ {{ "{:,}".format(signal.order_contracts) }} {% if signal.order_price is number %}{{ "{:,.3f}".format(signal.order_price) }}{% endif %} {% if "err" in signal.order_status%} - {{ signal.order_status }}{{ signal.status_msg }} + {{ signal.order_status }}{{ signal.status_msg }} {% elif "created" in signal.order_status %} - {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} + {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} {% elif "cancel" in signal.order_status %} - {{ signal.order_status }}{{ signal.status_msg }} + {{ signal.order_status }}{{ signal.status_msg }} {% elif "waiting" in signal.order_status or "rerouted" in signal.order_status %} - {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} + {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} {% elif "part." in signal.order_status %} - {{ signal.order_status }}{{ signal.status_msg }} + {{ signal.order_status }}{{ signal.status_msg }} {% elif "filled" in signal.order_status %} - {{ signal.order_status }}{{ signal.status_msg }} + {{ signal.order_status }}{{ signal.status_msg }} {% else %} {{ signal.order_status }} @@ -67,10 +67,10 @@ {% block legenddash %}
-   waiting -   canceled -   error -   created -   filled +   waiting +   canceled +   error +   created +   filled
{% endblock %} diff --git a/templates/list.html b/templates/list.html index 214d92c..15fba0c 100644 --- a/templates/list.html +++ b/templates/list.html @@ -1,8 +1,8 @@ {% extends 'base.html' %} {% block submitlist %} -
-
+
+
pair @@ -15,7 +15,7 @@   - +

@@ -24,7 +24,7 @@
-
+
slippage:

buy: {{slip_buy}}
sell: {{slip_sell}}
@@ -53,7 +53,7 @@ {{ signal.rowid }} {% if signal.timestamp|iftoday %} - {{ signal.timestamp|pct_time("UTC")}} + {{ signal.timestamp|pct_time("UTC")}} {% else %} {{ signal.timestamp|pct_time("UTC")}} {% endif %} @@ -63,15 +63,15 @@ {{ "{:,}".format(signal.order_contracts) }} {{ "{:,.2f}".format(signal.order_price) }} {% if "err" in signal.order_status%} - {{ signal.order_status }}{{ signal.status_msg }} + {{ signal.order_status }}{{ signal.status_msg }} {% elif "created" in signal.order_status %} - {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} + {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} {% elif "cancel" in signal.order_status %} - {{ signal.order_status }}{{ signal.status_msg }} + {{ signal.order_status }}{{ signal.status_msg }} {% elif "waiting" in signal.order_status or "rerouted" in signal.order_status %} - {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} + {{ signal.order_status }}({{ (signal.timestamp|timediff) }} min){{ signal.status_msg }} {% elif "filled" in signal.order_status %} - {{ signal.order_status }}{{ signal.status_msg }} + {{ signal.order_status }}{{ signal.status_msg }} {% else %} {{ signal.order_status }} {% endif %} @@ -99,24 +99,24 @@ {% endblock %} {% block listscript %} - - + {% endblock %} {% block legendlist %}
-   active -   passive -   watch +   active +   passive +   watch
-   waiting -   canceled -   error -   created -   filled +   waiting +   canceled +   error +   created +   filled
{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index a09a6d5..a1c82eb 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,13 +1,21 @@
-   waiting -   canceled -   error -   created -   filled +   waiting +   canceled +   error +   created +   filled
@@ -457,6 +494,7 @@

Add Pair

+



@@ -476,20 +514,41 @@

List of Pairs

- - + + + +
-
+

Update Pair

-
 DELETE 
+
 DELETE 
+

@@ -531,6 +590,7 @@

Update Pair

watch +

@@ -554,8 +614,8 @@
-   active -   passive +   active +   passive
@@ -567,7 +627,7 @@

Add Ticker

-

@@ -630,6 +690,7 @@

Add Ticker

+

@@ -647,20 +708,41 @@

List of Tickers

- - + + + +
-
+

Update Ticker

-
 DELETE 
+
 DELETE 
+

@@ -735,6 +817,7 @@

Update Ticker

passive +



@@ -754,31 +837,24 @@
-   active -   passive +   active +   passive
- - - - - + + + + diff --git a/templates/watch.html b/templates/watch.html index cc8c6c8..670f6ca 100644 --- a/templates/watch.html +++ b/templates/watch.html @@ -1,11 +1,11 @@ {% extends 'base.html' %} {% block watchlist %} - + -
+ +
+