Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/strategy performance page v2 #184

Merged
merged 18 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
/.github
/.vscode
# Ignore Git and version control files
.git
.gitignore
.dockerignore
_pycache_

# Ignore cache and temporary files
__pycache__
*.pyc
*.pyo
*.pyd

# Ignore editor and IDE configurations
.vscode
.idea
*.swp
*~

# Ignore environment and dependency files
*.env
*.venv
*.DS_Store
*.log

# Ignore specific folders that are not needed in the container
.github
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ LABEL date=${BUILD_DATE}
# Set ENV variables
ENV COMMIT_SHA=${COMMIT}
ENV COMMIT_BRANCH=${BRANCH}
ENV BUILD_DATE=${DATE}
ENV BUILD_DATE=${BUILD_DATE}

# Install system dependencies
RUN apt-get update && \
Expand Down
59 changes: 58 additions & 1 deletion backend/services/backend_api_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Optional
from typing import Any, Dict, List, Optional

import pandas as pd
import requests
Expand Down Expand Up @@ -306,3 +306,60 @@ def get_account_state_history(self):
"""Get account state history."""
endpoint = "account-state-history"
return self.get(endpoint)

def get_performance_results(self, executors: List[Dict[str, Any]]):
if not isinstance(executors, list) or len(executors) == 0:
raise ValueError("Executors must be a non-empty list of dictionaries")
# Check if all elements in executors are dictionaries
if not all(isinstance(executor, dict) for executor in executors):
raise ValueError("All elements in executors must be dictionaries")
endpoint = "get-performance-results"
payload = {
"executors": executors,
}

performance_results = self.post(endpoint, payload=payload)
if "error" in performance_results:
raise Exception(performance_results["error"])
if "detail" in performance_results:
raise Exception(performance_results["detail"])
if "processed_data" not in performance_results:
data = None
else:
data = pd.DataFrame(performance_results["processed_data"])
if "executors" not in performance_results:
executors = []
else:
executors = [ExecutorInfo(**executor) for executor in performance_results["executors"]]
return {
"processed_data": data,
"executors": executors,
"results": performance_results["results"]
}

def list_databases(self):
"""Get databases list."""
endpoint = "list-databases"
return self.post(endpoint)

def read_databases(self, db_paths: List[str]):
"""Read databases."""
endpoint = "read-databases"
return self.post(endpoint, payload=db_paths)

def create_checkpoint(self, db_names: List[str]):
"""Create a checkpoint."""
endpoint = "create-checkpoint"
return self.post(endpoint, payload=db_names)

def list_checkpoints(self, full_path: bool):
"""List checkpoints."""
endpoint = "list-checkpoints"
params = {"full_path": full_path}
return self.post(endpoint, params=params)

def load_checkpoint(self, checkpoint_path: str):
"""Load a checkpoint."""
endpoint = "load-checkpoint"
params = {"checkpoint_path": checkpoint_path}
return self.post(endpoint, params=params)
197 changes: 197 additions & 0 deletions backend/utils/performance_data_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import json
from typing import Any, Dict, List

import numpy as np
import pandas as pd
from hummingbot.core.data_type.common import TradeType
from hummingbot.strategy_v2.models.base import RunnableStatus
from hummingbot.strategy_v2.models.executors import CloseType
from hummingbot.strategy_v2.models.executors_info import ExecutorInfo


class PerformanceDataSource:
def __init__(self,
checkpoint_data: Dict[str, Any]):
self.checkpoint_data = checkpoint_data
self.executors_dict = self.checkpoint_data["executors"].copy()
self.orders = self.load_orders()
self.controllers_df = self.load_controllers()
self.executors_with_orders = self.get_executors_with_orders(self.get_executors_df(), self.orders)

def load_orders(self):
"""
Load the orders data from the checkpoint.
"""
orders = self.checkpoint_data["orders"].copy()
orders = pd.DataFrame(orders)
return orders

def load_trade_fill(self):
trade_fill = self.checkpoint_data["trade_fill"].copy()
trade_fill = pd.DataFrame(trade_fill)
trade_fill["timestamp"] = trade_fill["timestamp"].apply(lambda x: self.ensure_timestamp_in_seconds(x))
trade_fill["datetime"] = pd.to_datetime(trade_fill.timestamp, unit="s")
return trade_fill

def load_controllers(self):
controllers = self.checkpoint_data["controllers"].copy()
controllers = pd.DataFrame(controllers)
controllers["config"] = controllers["config"].apply(lambda x: json.loads(x))
controllers["datetime"] = pd.to_datetime(controllers.timestamp, unit="s")
return controllers

@property
def controllers_dict(self):
return {controller["id"]: controller["config"] for controller in self.controllers_df.to_dict(orient="records")}

def get_executors_df(self, executors_filter: Dict[str, Any] = None, apply_executor_data_types: bool = False):
executors_df = pd.DataFrame(self.executors_dict)
executors_df["custom_info"] = executors_df["custom_info"].apply(
lambda x: json.loads(x) if isinstance(x, str) else x
)
executors_df["config"] = executors_df["config"].apply(lambda x: json.loads(x) if isinstance(x, str) else x)
executors_df["timestamp"] = executors_df["timestamp"].apply(lambda x: self.ensure_timestamp_in_seconds(x))
executors_df["close_timestamp"] = executors_df["close_timestamp"].apply(
lambda x: self.ensure_timestamp_in_seconds(x)
)
executors_df.sort_values("close_timestamp", inplace=True)
executors_df["trading_pair"] = executors_df["config"].apply(lambda x: x["trading_pair"])
executors_df["exchange"] = executors_df["config"].apply(lambda x: x["connector_name"])
executors_df["status"] = executors_df["status"].astype(int)
executors_df["level_id"] = executors_df["config"].apply(lambda x: x.get("level_id"))
executors_df["bep"] = executors_df["custom_info"].apply(lambda x: x["current_position_average_price"])
executors_df["order_ids"] = executors_df["custom_info"].apply(lambda x: x.get("order_ids"))
executors_df["close_price"] = executors_df["custom_info"].apply(
lambda x: x.get("close_price", x["current_position_average_price"]))
executors_df["sl"] = executors_df["config"].apply(lambda x: x.get("stop_loss")).fillna(0)
executors_df["tp"] = executors_df["config"].apply(lambda x: x.get("take_profit")).fillna(0)
executors_df["tl"] = executors_df["config"].apply(lambda x: x.get("time_limit")).fillna(0)
executors_df["close_type_name"] = executors_df["close_type"].apply(lambda x: self.get_enum_by_value(CloseType, x).name)

controllers = self.controllers_df.copy()
controllers.drop(columns=["controller_id"], inplace=True)
controllers.rename(columns={
"config": "controller_config",
"type": "controller_type",
"id": "controller_id"
}, inplace=True)

executors_df = executors_df.merge(controllers[["controller_id", "controller_type", "controller_config"]],
on="controller_id", how="left")
if apply_executor_data_types:
executors_df = self.apply_executor_data_types(executors_df)
if executors_filter is not None:
executors_df = self.filter_executors(executors_df, executors_filter)
return executors_df

def apply_executor_data_types(self, executors):
executors["status"] = executors["status"].apply(lambda x: self.get_enum_by_value(RunnableStatus, int(x)))
executors["side"] = executors["config"].apply(lambda x: self.get_enum_by_value(TradeType, int(x["side"])))
executors["close_type"] = executors["close_type"].apply(lambda x: self.get_enum_by_value(CloseType, int(x)))
executors["datetime"] = pd.to_datetime(executors.timestamp, unit="s")
executors["close_datetime"] = pd.to_datetime(executors["close_timestamp"], unit="s")
return executors

@staticmethod
def remove_executor_data_types(executors):
executors["status"] = executors["status"].apply(lambda x: x.value)
executors["side"] = executors["side"].apply(lambda x: x.value)
executors["close_type"] = executors["close_type"].apply(lambda x: x.value)
executors.drop(columns=["datetime", "close_datetime"], inplace=True)
return executors

@staticmethod
def get_executors_with_orders(executors_df: pd.DataFrame, orders: pd.DataFrame):
df = (executors_df[["id", "order_ids"]]
.rename(columns={"id": "executor_id", "order_ids": "order_id"})
.explode("order_id"))
exec_with_orders = df.merge(orders, left_on="order_id", right_on="client_order_id", how="inner")
exec_with_orders = exec_with_orders[exec_with_orders["last_status"].isin(["SellOrderCompleted",
"BuyOrderCompleted"])]
return exec_with_orders[["executor_id", "order_id", "last_status", "last_update_timestamp",
"price", "amount", "position"]]

def get_executor_info_list(self,
executors_filter: Dict[str, Any] = None) -> List[ExecutorInfo]:
required_columns = [
"id", "timestamp", "type", "close_timestamp", "close_type", "status", "controller_type",
"net_pnl_pct", "net_pnl_quote", "cum_fees_quote", "filled_amount_quote",
"is_active", "is_trading", "controller_id", "side", "config", "custom_info", "exchange", "trading_pair"
]
executors_df = self.get_executors_df(executors_filter=executors_filter,
apply_executor_data_types=True
)[required_columns].copy()
executors_df = executors_df[executors_df["net_pnl_quote"] != 0]
executor_info_list = executors_df.apply(lambda row: ExecutorInfo(**row.to_dict()), axis=1).tolist()
return executor_info_list

def get_executor_dict(self,
executors_filter: Dict[str, Any] = None,
apply_executor_data_types: bool = False,
remove_special_fields: bool = False) -> List[dict]:
executors_df = self.get_executors_df(executors_filter,
apply_executor_data_types=apply_executor_data_types).copy()
if remove_special_fields:
executors_df = self.remove_executor_data_types(executors_df)
return executors_df.to_dict(orient="records")

def get_executors_by_controller_type(self,
executors_filter: Dict[str, Any] = None) -> Dict[str, pd.DataFrame]:
executors_by_controller_type = {}
executors_df = self.get_executors_df(executors_filter).copy()
for controller_type in executors_df["controller_type"].unique():
executors_by_controller_type[controller_type] = executors_df[
executors_df["controller_type"] == controller_type
]
return executors_by_controller_type

@staticmethod
def filter_executors(executors_df: pd.DataFrame,
filters: Dict[str, List[Any]]):
filter_condition = np.array([True] * len(executors_df))
for key, value in filters.items():
if isinstance(value, list) and len(value) > 0:
filter_condition &= np.array(executors_df[key].isin(value))
elif key == "start_time":
filter_condition &= np.array(executors_df["timestamp"] >= value - 60)
elif key == "close_type_name":
filter_condition &= np.array(executors_df["close_type_name"] == value)
elif key == "end_time":
filter_condition &= np.array(executors_df["close_timestamp"] <= value + 60)

return executors_df[filter_condition]

@staticmethod
def get_enum_by_value(enum_class, value):
for member in enum_class:
if member.value == value:
return member
raise ValueError(f"No enum member with value {value}")

@staticmethod
def ensure_timestamp_in_seconds(timestamp: float) -> float:
"""
Ensure the given timestamp is in seconds.

Args:
- timestamp (int): The input timestamp which could be in seconds, milliseconds, or microseconds.

Returns:
- int: The timestamp in seconds.

Raises:
- ValueError: If the timestamp is not in a recognized format.
"""
timestamp_int = int(float(timestamp))
if timestamp_int >= 1e18: # Nanoseconds
return timestamp_int / 1e9
elif timestamp_int >= 1e15: # Microseconds
return timestamp_int / 1e6
elif timestamp_int >= 1e12: # Milliseconds
return timestamp_int / 1e3
elif timestamp_int >= 1e9: # Seconds
return timestamp_int
else:
raise ValueError(
"Timestamp is not in a recognized format. Must be in seconds, milliseconds, microseconds or "
"nanoseconds.")
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ services:
- BACKEND_API_USERNAME=admin
- BACKEND_API_PASSWORD=admin
volumes:
- .:/home/dashboard
- .:/home/dashboard
Empty file.
5 changes: 5 additions & 0 deletions frontend/pages/performance/bot_performance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
This page helps you analize database files of several Hummingbot strategies and measure performance.

#### Support

For any inquiries, feedback, or assistance, please contact @drupman on Hummingbot's [Discord](https://discord.com/invite/hummingbot).
Empty file.
43 changes: 43 additions & 0 deletions frontend/pages/performance/bot_performance/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import asyncio

import streamlit as st

from backend.utils.performance_data_source import PerformanceDataSource
from frontend.st_utils import get_backend_api_client, initialize_st_page
from frontend.visualization.bot_performance import (
display_execution_analysis,
display_global_results,
display_performance_summary_table,
display_tables_section,
)
from frontend.visualization.performance_etl import display_etl_section


async def main():
initialize_st_page(title="Bot Performance", icon="🚀", initial_sidebar_state="collapsed")
st.session_state["default_config"] = {}
backend_api = get_backend_api_client()

st.subheader("🔫 DATA SOURCE")
checkpoint_data = display_etl_section(backend_api)
data_source = PerformanceDataSource(checkpoint_data)
st.divider()

st.subheader("📊 OVERVIEW")
display_performance_summary_table(data_source.get_executors_df(), data_source.executors_with_orders)
st.divider()

st.subheader("🌎 GLOBAL RESULTS")
display_global_results(data_source)
st.divider()

st.subheader("🧨 EXECUTION")
display_execution_analysis(data_source)
st.divider()

st.subheader("💾 EXPORT")
display_tables_section(data_source)


if __name__ == "__main__":
asyncio.run(main())
2 changes: 2 additions & 0 deletions frontend/pages/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def public_pages():
Page("frontend/pages/config/xemm_controller/app.py", "XEMM Controller", "⚡️"),
Section("Data", "💾"),
Page("frontend/pages/data/download_candles/app.py", "Download Candles", "💹"),
Section("Performance Pages", "🚀"),
Page("frontend/pages/performance/bot_performance/app.py", "Strategy Performance", "📈"),
Section("Community Pages", "👨‍👩‍👧‍👦"),
Page("frontend/pages/data/token_spreads/app.py", "Token Spreads", "🧙"),
Page("frontend/pages/data/tvl_vs_mcap/app.py", "TVL vs Market Cap", "🦉"),
Expand Down
4 changes: 2 additions & 2 deletions frontend/visualization/backtesting_metrics.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import streamlit as st


def render_backtesting_metrics(summary_results):
def render_backtesting_metrics(summary_results, title="Backtesting Metrics"):
net_pnl = summary_results.get('net_pnl', 0)
net_pnl_quote = summary_results.get('net_pnl_quote', 0)
total_volume = summary_results.get('total_volume', 0)
Expand All @@ -13,7 +13,7 @@ def render_backtesting_metrics(summary_results):
profit_factor = summary_results.get('profit_factor', 0)

# Displaying KPIs in Streamlit
st.write("### Backtesting Metrics")
st.write(f"### {title}")
col1, col2, col3, col4, col5, col6 = st.columns(6)
col1.metric(label="Net PNL (Quote)", value=f"{net_pnl_quote:.2f}", delta=f"{net_pnl:.2%}")
col2.metric(label="Max Drawdown (USD)", value=f"{max_drawdown_usd:.2f}", delta=f"{max_drawdown_pct:.2%}")
Expand Down
Loading