diff --git a/.streamlit/config.toml b/.streamlit/config.toml index f6e43646..9a427db6 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,5 +1,5 @@ [theme] -base="dark" +base="light" font="monospace" [server] port=8501 diff --git a/environment_conda.yml b/environment_conda.yml index ba07074e..fcb3c610 100644 --- a/environment_conda.yml +++ b/environment_conda.yml @@ -1,4 +1,4 @@ -name: streamlit-apps +name: dashboard channels: - conda-forge dependencies: diff --git "a/pages/2_\360\237\232\200_Strategy_Performance.py" "b/pages/2_\360\237\232\200_Strategy_Performance.py" index 489c8456..529d6dd2 100644 --- "a/pages/2_\360\237\232\200_Strategy_Performance.py" +++ "b/pages/2_\360\237\232\200_Strategy_Performance.py" @@ -1,14 +1,12 @@ import os import sqlite3 -from datetime import datetime import ccxt -import numpy as np import pandas as pd import streamlit as st -from utils.data_manipulation import BotData +from utils.database_manager import DatabaseManager from utils.graphs import CandlesGraph st.set_page_config( @@ -20,118 +18,121 @@ st.title("πŸš€ Strategy Performance") intervals = { - "1m": 60, + # "1m": 60, "3m": 60 * 3, "30m": 60 * 30, "1h": 60 * 60, "6h": 60 * 60 * 6, "1d": 60 * 60 * 24, - } - @st.cache_resource -def get_data(db_name: str): - path = os.path.join("data", db_name) - conn = sqlite3.connect(path) - order_df = pd.read_sql_query("SELECT * FROM 'Order'", conn) - order_status_df = pd.read_sql_query("SELECT * FROM OrderStatus", conn) - trade_fill_df = pd.read_sql_query("SELECT * FROM TradeFill", conn) - order_df['creation_timestamp'] = pd.to_datetime(order_df['creation_timestamp'], unit="ms") - order_df['last_update_timestamp'] = pd.to_datetime(order_df['last_update_timestamp'], unit="ms") - trade_fill_df["timestamp"] = pd.to_datetime(trade_fill_df["timestamp"], unit="ms") - # TODO: GitHub issue #8 - trade_fill_df["price"] = trade_fill_df["price"] / 1000000 - trade_fill_df["amount"] = trade_fill_df["amount"] / 1000000 - conn.close() - return BotData(order_df, order_status_df, trade_fill_df) +def get_database(db_name: str): + db_manager = DatabaseManager(db_name) + return db_manager @st.cache_data(ttl=60) def get_ohlc(trading_pair: str, exchange: str, interval: str, start_timestamp: int, end_timestamp: int): - exchange = eval("ccxt." + exchange + "()") - limit = (end_timestamp - start_timestamp) / intervals[interval] - bars = exchange.fetch_ohlcv(trading_pair, timeframe=interval, since=start_timestamp * 1000, limit=int(limit)) + connector = getattr(ccxt, exchange)() + limit = max(int((end_timestamp - start_timestamp) / intervals[interval]), 10) + bars = connector.fetch_ohlcv(trading_pair.replace("-", ""), timeframe=interval, since=start_timestamp * 1000, limit=limit) df = pd.DataFrame(bars, columns=["timestamp", "open", "high", "low", "close", "volume"]) df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms") return df -col11, col12 = st.columns(2) -with col11: - db_names = [db_name for db_name in os.listdir("data") if db_name.endswith(".sqlite")] - selected_db_name = st.selectbox("Select a database to use:", db_names) - all_bots_data = get_data(selected_db_name) -with col12: - selected_config_file = st.selectbox("Select a config file to analyze:", - all_bots_data.trade_fill["config_file_path"].unique()) - if selected_config_file is not None: - strategy_data = all_bots_data.get_strategy_data( - selected_config_file) - -exchange_name = strategy_data.market -trading_pair = strategy_data.symbol.replace("-", "") - -c1, c2 = st.columns([1, 5]) -with c1: - interval = st.selectbox("Candles Interval:", intervals.keys(), index=3) - -date_array = pd.date_range(start=strategy_data.start_time, end=strategy_data.end_time, periods=60) -ohlc_extra_time = 60 -with st.spinner("Loading candles..."): - candles_df = get_ohlc(trading_pair, exchange_name, interval, int(strategy_data.start_time.timestamp() - ohlc_extra_time), - int(strategy_data.end_time.timestamp() + ohlc_extra_time)) -start_time, end_time = st.select_slider("Select a time range to analyze", options=date_array.tolist(), value=(date_array[0], date_array[-1])) -candles_df_filtered = candles_df[(candles_df["timestamp"] >= int(start_time.timestamp() * 1000)) & (candles_df["timestamp"] <= int(end_time.timestamp() * 1000))] -strategy_data_filtered = strategy_data.get_filtered_strategy_data(start_time, end_time) - -row = st.container() -col11, col12, col13 = st.columns([1, 2, 3]) -with row: - with col11: - st.header(f"🏦 Market") - st.metric(label="Exchange", value=strategy_data_filtered.market.capitalize()) - st.metric(label="Trading pair", value=strategy_data_filtered.symbol) - with col12: - st.header("πŸ“‹ General stats") - col121, col122 = st.columns(2) - with col121: - st.metric(label='Start date', value=strategy_data_filtered.start_time.strftime("%Y-%m-%d %H:%M")) - st.metric(label='End date', value=strategy_data_filtered.end_time.strftime("%Y-%m-%d %H:%M")) - st.metric(label='Duration (Hours)', value=round(strategy_data_filtered.duration_seconds / 3600, 2)) - with col122: - st.metric(label='Start Price', value=round(strategy_data_filtered.start_price, 4)) - st.metric(label='End Price', value=round(strategy_data_filtered.end_price, 4)) - st.metric(label='Price change', value=f"{round(strategy_data_filtered.price_change * 100, 2)} %") - with col13: - st.header("πŸ“ˆ Performance") - col131, col132, col133 = st.columns(3) - with col131: - st.metric(label='Total Trades', value=strategy_data_filtered.total_orders) - st.metric(label='Total Buy Trades', value=strategy_data_filtered.total_buy_trades) - st.metric(label='Total Sell Trades', value=strategy_data_filtered.total_sell_trades) - with col132: - st.metric(label='Inventory change in Base asset', value=round(strategy_data_filtered.inventory_change_base_asset, 4)) - st.metric(label='Total Buy Trades Amount', value=strategy_data_filtered.total_buy_amount) - st.metric(label='Total Sell Trades Amount', value=strategy_data_filtered.total_sell_amount) - with col133: - st.metric(label='Trade PNL USD', value=round(strategy_data_filtered.trade_pnl_usd, 2)) - st.metric(label='Average Buy Price', value=round(strategy_data_filtered.average_buy_price, 4)) - st.metric(label='Average Sell Price', value=round(strategy_data_filtered.average_sell_price, 4)) - -cg = CandlesGraph(candles_df_filtered, show_volume=True, extra_rows=2) -cg.add_buy_trades(strategy_data_filtered.buys) -cg.add_sell_trades(strategy_data_filtered.sells) -cg.add_base_inventory_change(strategy_data_filtered) -cg.add_trade_pnl(strategy_data_filtered) -fig = cg.figure() -st.plotly_chart(fig, use_container_width=True) - -st.subheader("πŸ’΅Trades") -st.write(strategy_data_filtered.trade_fill) - -st.subheader("πŸ“© Orders") -st.write(strategy_data_filtered.orders) - -st.subheader("βŒ• Order Status") -st.write(strategy_data_filtered.order_status) \ No newline at end of file +with st.container(): + col1, col2 = st.columns(2) + with col1: + db_names = [db_name for db_name in os.listdir("data") if db_name.endswith(".sqlite")] + selected_db_name = st.selectbox("Select a database to use:", + db_names if len(db_names) > 0 else ["No databases found"]) + if selected_db_name == "No databases found": + st.warning("No databases available to analyze. Please run a backtesting first.") + else: + db_manager = get_database(selected_db_name) + with col2: + selected_config_file = st.selectbox("Select a config file to analyze:", db_manager.get_config_files()) + if selected_config_file is not None: + exchanges_trading_pairs = db_manager.get_exchanges_trading_pairs_by_config_file(selected_config_file) + strategy_data = db_manager.get_strategy_data(selected_config_file) + + with st.container(): + col1, col2, col3 = st.columns(3) + with col1: + selected_exchange = st.selectbox("Select an exchange:", list(exchanges_trading_pairs.keys())) + with col2: + selected_trading_pair = st.selectbox("Select a trading pair:", exchanges_trading_pairs[selected_exchange]) + with col3: + interval = st.selectbox("Candles Interval:", intervals.keys(), index=0) + + if selected_exchange and selected_trading_pair: + single_market_strategy_data = strategy_data.get_single_market_strategy_data(selected_exchange, selected_trading_pair) + date_array = pd.date_range(start=strategy_data.start_time, end=strategy_data.end_time, periods=60) + ohlc_extra_time = 60 + with st.spinner("Loading candles..."): + candles_df = get_ohlc(single_market_strategy_data.trading_pair, single_market_strategy_data.exchange, interval, + int(strategy_data.start_time.timestamp() - ohlc_extra_time), + int(strategy_data.end_time.timestamp() + ohlc_extra_time)) + start_time, end_time = st.select_slider("Select a time range to analyze", options=date_array.tolist(), + value=(date_array[0], date_array[-1])) + candles_df_filtered = candles_df[(candles_df["timestamp"] >= int(start_time.timestamp() * 1000)) & ( + candles_df["timestamp"] <= int(end_time.timestamp() * 1000))] + strategy_data_filtered = single_market_strategy_data.get_filtered_strategy_data(start_time, end_time) + + row = st.container() + col11, col12, col13 = st.columns([1, 2, 3]) + with row: + with col11: + st.header(f"🏦 Market") + st.metric(label="Exchange", value=strategy_data_filtered.exchange.capitalize()) + st.metric(label="Trading pair", value=strategy_data_filtered.trading_pair.upper()) + with col12: + st.header("πŸ“‹ General stats") + col121, col122 = st.columns(2) + with col121: + st.metric(label='Duration (Hours)', value=round(strategy_data_filtered.duration_seconds / 3600, 2)) + st.metric(label='Start date', value=strategy_data_filtered.start_time.strftime("%Y-%m-%d %H:%M")) + st.metric(label='End date', value=strategy_data_filtered.end_time.strftime("%Y-%m-%d %H:%M")) + with col122: + st.metric(label='Price change', value=f"{round(strategy_data_filtered.price_change * 100, 2)} %") + with col13: + st.header("πŸ“ˆ Performance") + col131, col132, col133, col134 = st.columns(4) + with col131: + st.metric(label=f'Net PNL {strategy_data_filtered.quote_asset}', value=round(strategy_data_filtered.net_pnl_quote, 2)) + st.metric(label=f'Trade PNL {strategy_data_filtered.quote_asset}', value=round(strategy_data_filtered.trade_pnl_quote, 2)) + st.metric(label=f'Fees {strategy_data_filtered.quote_asset}', value=round(strategy_data_filtered.cum_fees_in_quote, 2)) + with col132: + st.metric(label='Total Trades', value=strategy_data_filtered.total_orders) + st.metric(label='Total Buy Trades', value=strategy_data_filtered.total_buy_trades) + st.metric(label='Total Sell Trades', value=strategy_data_filtered.total_sell_trades) + with col133: + st.metric(label='Inventory change in Base asset', + value=round(strategy_data_filtered.inventory_change_base_asset, 4)) + st.metric(label='Total Buy Trades Amount', value=round(strategy_data_filtered.total_buy_amount, 2)) + st.metric(label='Total Sell Trades Amount', value=round(strategy_data_filtered.total_sell_amount, 2)) + with col134: + st.metric(label='End Price', value=round(strategy_data_filtered.end_price, 4)) + st.metric(label='Average Buy Price', value=round(strategy_data_filtered.average_buy_price, 4)) + st.metric(label='Average Sell Price', value=round(strategy_data_filtered.average_sell_price, 4)) + + cg = CandlesGraph(candles_df_filtered, show_volume=True, extra_rows=4) + cg.add_buy_trades(strategy_data_filtered.buys) + cg.add_sell_trades(strategy_data_filtered.sells) + cg.add_base_inventory_change(strategy_data_filtered) + cg.add_net_pnl(strategy_data_filtered) + cg.add_trade_pnl(strategy_data_filtered) + cg.add_trade_fee(strategy_data_filtered) + fig = cg.figure() + st.plotly_chart(fig, use_container_width=True) + + st.subheader("πŸ’΅Trades") + st.write(strategy_data_filtered.trade_fill) + + st.subheader("πŸ“© Orders") + st.write(strategy_data_filtered.orders) + + st.subheader("βŒ• Order Status") + st.write(strategy_data_filtered.order_status) diff --git a/utils/data_manipulation.py b/utils/data_manipulation.py index aa5705da..3171cc2e 100644 --- a/utils/data_manipulation.py +++ b/utils/data_manipulation.py @@ -8,34 +8,87 @@ class StrategyData: orders: pd.DataFrame order_status: pd.DataFrame trade_fill: pd.DataFrame - config_file_name: str - - def __post_init__(self): - self.trade_fill.loc[:, "net_amount"] = self.trade_fill['amount'] * self.trade_fill['trade_type'].apply(lambda x: 1 if x == 'BUY' else -1) - self.trade_fill.loc[:, "net_amount_quote"] = self.trade_fill['net_amount'] * self.trade_fill['price'] - self.trade_fill.loc[:, "cum_net_amount"] = self.trade_fill["net_amount"].cumsum() - self.trade_fill.loc[:, "unrealized_trade_pnl"] = -1 * self.trade_fill["net_amount_quote"].cumsum() - self.trade_fill.loc[:, "inventory_cost"] = self.trade_fill["cum_net_amount"] * self.trade_fill["price"] - self.trade_fill.loc[:, "realized_trade_pnl"] = self.trade_fill["unrealized_trade_pnl"] + self.trade_fill["inventory_cost"] - - def get_filtered_strategy_data(self, start_time: datetime.datetime, end_time: datetime.datetime): - orders = self.orders[(self.orders["creation_timestamp"] >= start_time) & (self.orders["creation_timestamp"] <= end_time)].copy() + + def get_single_market_strategy_data(self, exchange: str, trading_pair: str): + orders = self.orders[(self.orders["market"] == exchange) & (self.orders["symbol"] == trading_pair)].copy() + trade_fill = self.trade_fill[self.trade_fill["order_id"].isin(orders["id"])].copy() + order_status = self.order_status[self.order_status["order_id"].isin(orders["id"])].copy() + return SingleMarketStrategyData( + exchange=exchange, + trading_pair=trading_pair, + orders=orders, + order_status=order_status, + trade_fill=trade_fill, + ) + + @property + def exchanges(self): + return self.trade_fill["market"].unique() + + @property + def trading_pairs(self): + return self.trade_fill["symbol"].unique() + + @property + def start_time(self): + return self.orders["creation_timestamp"].min() + + @property + def end_time(self): + return self.orders["last_update_timestamp"].max() + + @property + def duration_seconds(self): + return (self.end_time - self.start_time).total_seconds() + + @property + def buys(self): + return self.trade_fill[self.trade_fill["trade_type"] == "BUY"] + + @property + def sells(self): + return self.trade_fill[self.trade_fill["trade_type"] == "SELL"] + + @property + def total_buy_trades(self): + return self.buys["amount"].count() + + @property + def total_sell_trades(self): + return self.sells["amount"].count() + + @property + def total_orders(self): + return self.total_buy_trades + self.total_sell_trades + + +@dataclass +class SingleMarketStrategyData: + exchange: str + trading_pair: str + orders: pd.DataFrame + order_status: pd.DataFrame + trade_fill: pd.DataFrame + + def get_filtered_strategy_data(self, start_date: datetime.datetime, end_date: datetime.datetime): + orders = self.orders[(self.orders["creation_timestamp"] >= start_date) & (self.orders["creation_timestamp"] <= end_date)].copy() trade_fill = self.trade_fill[self.trade_fill["order_id"].isin(orders["id"])].copy() order_status = self.order_status[self.order_status["order_id"].isin(orders["id"])].copy() - return StrategyData( + return SingleMarketStrategyData( + exchange=self.exchange, + trading_pair=self.trading_pair, orders=orders, order_status=order_status, trade_fill=trade_fill, - config_file_name=self.config_file_name ) @property - def market(self): - return self.trade_fill["market"].unique()[0].split("_")[0] + def base_asset(self): + return self.trading_pair.split("-")[0] @property - def symbol(self): - return self.trade_fill["symbol"].unique()[0] + def quote_asset(self): + return self.trading_pair.split("-")[1] @property def start_time(self): @@ -81,19 +134,6 @@ def total_buy_trades(self): def total_sell_trades(self): return self.sells["amount"].count() - - @property - def trade_pnl_usd(self): - # TODO: Review logic - buy_volume = self.buys["amount"].sum() * self.average_buy_price - sell_volume = self.sells["amount"].sum() * self.average_sell_price - inventory_change_volume = self.inventory_change_base_asset * self.end_price - return sell_volume - buy_volume + inventory_change_volume - - @property - def inventory_change_base_asset(self): - return self.total_buy_amount - self.total_sell_amount - @property def total_orders(self): return self.total_buy_trades + self.total_sell_trades @@ -112,28 +152,21 @@ def average_sell_price(self): def price_change(self): return (self.end_price - self.start_price) / self.start_price - -@dataclass -class BotData: - orders: pd.DataFrame - order_status: pd.DataFrame - trade_fill: pd.DataFrame - - def get_strategy_data(self, config_file_name: str): - orders_filtered = self.orders[self.orders["config_file_path"] == config_file_name].copy() - order_status_filtered = self.order_status[ - self.order_status["order_id"].isin(orders_filtered["id"])].copy() - trade_fill_filtered = self.trade_fill[self.trade_fill["config_file_path"] == config_file_name].copy() - return StrategyData(orders_filtered, order_status_filtered, trade_fill_filtered, config_file_name) + @property + def trade_pnl_quote(self): + buy_volume = self.buys["amount"].sum() * self.average_buy_price + sell_volume = self.sells["amount"].sum() * self.average_sell_price + inventory_change_volume = self.inventory_change_base_asset * self.end_price + return sell_volume - buy_volume + inventory_change_volume @property - def start_time(self): - return self.orders["creation_timestamp"].min() + def cum_fees_in_quote(self): + return self.trade_fill["trade_fee_in_quote"].sum() @property - def end_time(self): - return self.orders["last_update_timestamp"].max() + def net_pnl_quote(self): + return self.trade_pnl_quote - self.cum_fees_in_quote @property - def duration_minutes(self): - return (self.end_time - self.start_time).seconds / 60 \ No newline at end of file + def inventory_change_base_asset(self): + return self.total_buy_amount - self.total_sell_amount diff --git a/utils/database_manager.py b/utils/database_manager.py new file mode 100644 index 00000000..ba9dade8 --- /dev/null +++ b/utils/database_manager.py @@ -0,0 +1,117 @@ +import os + +import pandas as pd +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from utils.data_manipulation import StrategyData + + +class DatabaseManager: + def __init__(self, db_name): + self.db_name = db_name + # TODO: Create db path for all types of db + self.db_path = f'sqlite:///{os.path.join("data", db_name)}' + self.engine = create_engine(self.db_path, connect_args={'check_same_thread': False}) + self.session_maker = sessionmaker(bind=self.engine) + + def get_config_files(self): + with self.session_maker() as session: + query = 'SELECT DISTINCT config_file_path FROM TradeFill' + config_files = pd.read_sql_query(query, session.connection()) + return config_files['config_file_path'].tolist() + + def get_exchanges_trading_pairs_by_config_file(self, config_file_path): + with self.session_maker() as session: + query = f"SELECT DISTINCT market, symbol FROM TradeFill WHERE config_file_path = '{config_file_path}'" + exchanges_trading_pairs = pd.read_sql_query(query, session.connection()) + exchanges_trading_pairs["market"] = exchanges_trading_pairs["market"].apply( + lambda x: x.lower().replace("_papertrade", "")) + exchanges_trading_pairs = exchanges_trading_pairs.groupby("market")["symbol"].apply(list).to_dict() + return exchanges_trading_pairs + + @staticmethod + def _get_orders_query(config_file_path=None, start_date=None, end_date=None): + query = "SELECT * FROM 'Order'" + conditions = [] + if config_file_path: + conditions.append(f"config_file_path = '{config_file_path}'") + if start_date: + conditions.append(f"created_at >= '{start_date}'") + if end_date: + conditions.append(f"created_at <= '{end_date}'") + if conditions: + query += f" WHERE {' AND '.join(conditions)}" + return query + + @staticmethod + def _get_order_status_query(order_ids=None, start_date=None, end_date=None): + query = "SELECT * FROM OrderStatus" + conditions = [] + if order_ids: + order_ids_string = ",".join(f"'{order_id}'" for order_id in order_ids) + conditions.append(f"order_id IN ({order_ids_string})") + if start_date: + conditions.append(f"created_at >= '{start_date}'") + if end_date: + conditions.append(f"created_at <= '{end_date}'") + if conditions: + query += f" WHERE {' AND '.join(conditions)}" + return query + + @staticmethod + def _get_trade_fills_query(config_file_path=None, start_date=None, end_date=None): + query = "SELECT * FROM TradeFill" + conditions = [] + if config_file_path: + conditions.append(f"config_file_path = '{config_file_path}'") + if start_date: + conditions.append(f"created_at >= '{start_date}'") + if end_date: + conditions.append(f"created_at <= '{end_date}'") + if conditions: + query += f" WHERE {' AND '.join(conditions)}" + return query + + def get_orders(self, config_file_path=None, start_date=None, end_date=None): + with self.session_maker() as session: + query = self._get_orders_query(config_file_path, start_date, end_date) + orders = pd.read_sql_query(query, session.connection()) + orders["market"] = orders["market"].apply(lambda x: x.lower().replace("_papertrade", "")) + return orders + + def get_trade_fills(self, config_file_path=None, start_date=None, end_date=None): + with self.session_maker() as session: + query = self._get_trade_fills_query(config_file_path, start_date, end_date) + trade_fills = pd.read_sql_query(query, session.connection()) + trade_fills["amount"] = trade_fills["amount"] / 1e6 + trade_fills["price"] = trade_fills["price"] / 1e6 + trade_fills["trade_fee_in_quote"] = trade_fills["trade_fee_in_quote"] / 1e6 + trade_fills["cum_fees_in_quote"] = trade_fills["trade_fee_in_quote"].cumsum() + trade_fills.loc[:, "net_amount"] = trade_fills['amount'] * trade_fills['trade_type'].apply( + lambda x: 1 if x == 'BUY' else -1) + trade_fills.loc[:, "net_amount_quote"] = trade_fills['net_amount'] * trade_fills['price'] + trade_fills.loc[:, "cum_net_amount"] = trade_fills["net_amount"].cumsum() + trade_fills.loc[:, "unrealized_trade_pnl"] = -1 * trade_fills["net_amount_quote"].cumsum() + trade_fills.loc[:, "inventory_cost"] = trade_fills["cum_net_amount"] * trade_fills["price"] + trade_fills.loc[:, "realized_trade_pnl"] = trade_fills["unrealized_trade_pnl"] + trade_fills["inventory_cost"] + trade_fills.loc[:, "net_realized_pnl"] = trade_fills["realized_trade_pnl"] - trade_fills["cum_fees_in_quote"] + trade_fills["market"] = trade_fills["market"].apply(lambda x: x.lower().replace("_papertrade", "")) + + return trade_fills + + def get_order_status(self, order_ids=None, start_date=None, end_date=None): + with self.session_maker() as session: + query = self._get_order_status_query(order_ids, start_date, end_date) + order_status = pd.read_sql_query(query, session.connection()) + return order_status + + def get_strategy_data(self, config_file_path=None, start_date=None, end_date=None): + orders = self.get_orders(config_file_path, start_date, end_date) + trade_fills = self.get_trade_fills(config_file_path, start_date, end_date) + order_status = self.get_order_status(orders['id'].tolist(), start_date, end_date) + orders['creation_timestamp'] = pd.to_datetime(orders['creation_timestamp'], unit="ms") + orders['last_update_timestamp'] = pd.to_datetime(orders['last_update_timestamp'], unit="ms") + trade_fills["timestamp"] = pd.to_datetime(trade_fills["timestamp"], unit="ms") + strategy_data = StrategyData(orders, order_status, trade_fills) + return strategy_data diff --git a/utils/graphs.py b/utils/graphs.py index 0348b261..faf2fbd4 100644 --- a/utils/graphs.py +++ b/utils/graphs.py @@ -14,7 +14,8 @@ def __init__(self, candles_df: pd.DataFrame, show_volume=True, extra_rows=1): rows, heights = self.get_n_rows_and_heights(extra_rows) self.rows = rows specs = [[{"secondary_y": True}]] * rows - self.base_figure = make_subplots(rows=rows, cols=1, shared_xaxes=True, vertical_spacing=0.05, row_heights=heights, specs=specs) + self.base_figure = make_subplots(rows=rows, cols=1, shared_xaxes=True, vertical_spacing=0.005, + row_heights=heights, specs=specs) self.add_candles_graph() if self.show_volume: self.add_volume() @@ -22,9 +23,9 @@ def __init__(self, candles_df: pd.DataFrame, show_volume=True, extra_rows=1): def get_n_rows_and_heights(self, extra_rows): rows = 1 + extra_rows + self.show_volume - row_heights = [0.3] * (extra_rows) + row_heights = [0.4] * (extra_rows) if self.show_volume: - row_heights.insert(0, 0.2) + row_heights.insert(0, 0.05) row_heights.insert(0, 0.8) return rows, row_heights @@ -72,7 +73,7 @@ def add_sell_trades(self, orders_data: pd.DataFrame): color='red', size=12, line=dict(color='black', width=1), - opacity=0.7,)), + opacity=0.7, )), row=1, col=1, ) @@ -117,7 +118,8 @@ def add_volume(self): y=self.candles_df['volume'], name="Volume", opacity=0.5, - marker=dict(color='lightgreen') + marker=dict(color='lightgreen'), + ), row=2, col=1, ) @@ -146,47 +148,83 @@ def add_base_inventory_change(self, strategy_data: StrategyData, row=3): y=strategy_data.trade_fill["net_amount"], name="Base Inventory Change", opacity=0.5, - marker=dict(color=["lightgreen" if amount > 0 else "indianred" for amount in strategy_data.trade_fill["net_amount"]]) + marker=dict(color=["lightgreen" if amount > 0 else "indianred" for amount in + strategy_data.trade_fill["net_amount"]]) ), row=row, col=1, ) # TODO: Review impact in different subgraphs - merged_df = pd.merge_asof(self.candles_df, strategy_data.trade_fill, left_on="datetime", right_on="timestamp", direction="forward") + merged_df = self.get_merged_df(strategy_data) self.base_figure.add_trace( go.Scatter( x=merged_df.datetime, y=merged_df["cum_net_amount"], name="Cumulative Base Inventory Change", - mode='lines', - line=dict(color='black', width=1)), - row=row, col=1, secondary_y=True - ) - self.base_figure.update_yaxes(title_text='Cum Base Inventory Change', row=3, col=1, secondary_y=True) - self.base_figure.update_yaxes(title_text='Base Inventory Change', row=3, col=1) + mode="lines+markers+text", + marker=dict(color="black", size=6), + line=dict(color="royalblue", width=2), + text=merged_df["cum_net_amount"], + textposition="top center", + texttemplate="%{text:.2f}" + ), + row=row, col=1 + ) + self.base_figure.update_yaxes(title_text='Base Inventory Change', row=row, col=1) - def add_trade_pnl(self, strategy_data: StrategyData, row=4): - merged_df = pd.merge_asof(self.candles_df, strategy_data.trade_fill, left_on="datetime", right_on="timestamp", direction="nearest") - merged_df["trade_pnl_continuos"] = merged_df["unrealized_trade_pnl"] + merged_df["cum_net_amount"] * merged_df["close"] + def add_net_pnl(self, strategy_data: StrategyData, row=4): + merged_df = self.get_merged_df(strategy_data) + self.base_figure.add_trace( + go.Scatter( + x=merged_df.datetime, + y=merged_df["net_pnl_continuos"], + name="Cumulative Net PnL", + mode="lines+markers+text", + marker=dict(color="black", size=6), + line=dict(color="mediumpurple", width=2), + text=merged_df["net_pnl_continuos"], + textposition="top center", + texttemplate="%{text:.1f}" + ), + row=row, col=1 + ) + self.base_figure.update_yaxes(title_text='Cum Net PnL', row=row, col=1) + def add_trade_pnl(self, strategy_data: StrategyData, row=5): + merged_df = self.get_merged_df(strategy_data) self.base_figure.add_trace( go.Scatter( x=merged_df.datetime, y=merged_df["trade_pnl_continuos"], - name="Cumulative Trade PnL Continuos", - mode='lines', - line=dict(color='chocolate', width=2)), + name="Cumulative Trade PnL", + mode="lines+markers+text", + marker=dict(color="black", size=6), + line=dict(color="crimson", width=2), + text=merged_df["trade_pnl_continuos"], + textposition="top center", + texttemplate="%{text:.1f}" + ), row=row, col=1 - ) + ) + self.base_figure.update_yaxes(title_text='Cum Trade PnL', row=row, col=1) + + def add_trade_fee(self, strategy_data: StrategyData, row=6): + merged_df = self.get_merged_df(strategy_data) + self.base_figure.add_trace( go.Scatter( x=merged_df.datetime, - y=merged_df["realized_trade_pnl"], - name="Cumulative Trade PnL by Trade", - mode='lines', - line=dict(color='cornflowerblue', width=2)), + y=merged_df["cum_fees_in_quote"], + name="Cumulative Fees", + mode="lines+markers+text", + marker=dict(color="black", size=6), + line=dict(color="seagreen", width=2), + text=merged_df["cum_fees_in_quote"], + textposition="top center", + texttemplate="%{text:.1f}" + ), row=row, col=1 ) - self.base_figure.update_yaxes(title_text='Cum Trade PnL', row=4, col=1) + self.base_figure.update_yaxes(title_text='Cum Trade Fees', row=row, col=1) def update_layout(self): self.base_figure.update_layout( @@ -204,7 +242,7 @@ def update_layout(self): xanchor="right", x=1 ), - height=1000, + height=1500, xaxis_rangeslider_visible=False, hovermode='x unified' ) @@ -213,6 +251,12 @@ def update_layout(self): self.base_figure.update_yaxes(title_text="Volume", row=2, col=1) self.base_figure.update_xaxes(title_text="Time", row=self.rows, col=1) + def get_merged_df(self, strategy_data: StrategyData): + merged_df = pd.merge_asof(self.candles_df, strategy_data.trade_fill, left_on="datetime", right_on="timestamp", direction="forward") + merged_df["trade_pnl_continuos"] = merged_df["unrealized_trade_pnl"] + merged_df["cum_net_amount"] * merged_df["close"] + merged_df["net_pnl_continuos"] = merged_df["trade_pnl_continuos"] - merged_df["cum_fees_in_quote"] + return merged_df + def get_bar_plot_volume_of_trades(strategy_data: StrategyData): grouped_df = strategy_data.trade_fill.groupby("trade_type").agg({"amount": "sum", "order_id": "count"}) @@ -255,4 +299,4 @@ def get_bar_plot_quantity_of_trades(strategy_data: StrategyData): )) fig.update_layout(template="plotly_white", title="Excution Analysis", height=300, xaxis_title="Quantity of orders") - return fig \ No newline at end of file + return fig