update,
This commit is contained in:
5
hkdse_maths_tutor/meta.md
Normal file
5
hkdse_maths_tutor/meta.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
tags:
|
||||
---
|
||||
|
||||
# notes
|
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
import dash
|
||||
from dash import dcc, html
|
||||
from utils.helpers import load_data
|
||||
from layouts.main_layout import create_layout
|
||||
from callbacks.backtest_callbacks import register_backtest_callbacks
|
||||
from callbacks.strategy_callbacks import register_strategy_callbacks
|
||||
|
||||
# Create the Dash app
|
||||
app = dash.Dash(__name__,
|
||||
prevent_initial_callbacks=True,
|
||||
suppress_callback_exceptions=True)
|
||||
|
||||
# Load data
|
||||
try:
|
||||
full_data = load_data()
|
||||
except Exception as e:
|
||||
raise Exception(f"Error: {str(e)}")
|
||||
|
||||
# Set up the layout
|
||||
app.layout = create_layout(full_data)
|
||||
|
||||
# Register callbacks
|
||||
register_backtest_callbacks(app)
|
||||
register_strategy_callbacks(app)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run_server(debug=True)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,178 @@
|
||||
from dash import Input, Output, State
|
||||
from datetime import timedelta
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import plotly.graph_objects as go
|
||||
from plotly.subplots import make_subplots
|
||||
from models.backtest import backtest
|
||||
|
||||
def register_backtest_callbacks(app):
|
||||
@app.callback(
|
||||
[Output('backtest-results', 'data'),
|
||||
Output('combined-heatmap', 'figure'),
|
||||
Output('forwardtest-results', 'data')],
|
||||
Input('run-backtest', 'n_clicks'),
|
||||
[State('bt-end-date', 'date'),
|
||||
State('window-start', 'value'),
|
||||
State('window-end', 'value'),
|
||||
State('window-step', 'value'),
|
||||
State('threshold-start', 'value'),
|
||||
State('threshold-end', 'value'),
|
||||
State('threshold-step', 'value'),
|
||||
State('time-factor', 'value'),
|
||||
State('commission', 'value'),
|
||||
State('heatmap-title', 'value')], # Add new input for title
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def run_backtest_and_create_heatmaps(n_clicks, bt_end_date, window_start, window_end, window_step,
|
||||
threshold_start, threshold_end, threshold_step, time_factor,
|
||||
commission, heatmap_title):
|
||||
if n_clicks == 0:
|
||||
return {}, {}, {}
|
||||
|
||||
try:
|
||||
# Get data from the global scope
|
||||
from app import full_data
|
||||
|
||||
# Parse inputs
|
||||
bt_end_date = pd.to_datetime(bt_end_date)
|
||||
ft_start_date = bt_end_date + timedelta(days=1)
|
||||
commission = commission / 100
|
||||
|
||||
# Create window and threshold lists
|
||||
window_list = list(range(window_start, window_end, window_step))
|
||||
threshold_steps = int((threshold_end - threshold_start) / threshold_step)
|
||||
threshold_list = [round(threshold_start + i * threshold_step, 2)
|
||||
for i in range(threshold_steps)]
|
||||
|
||||
# Split data
|
||||
bt_df = full_data[:bt_end_date]
|
||||
ft_df = full_data[ft_start_date:]
|
||||
|
||||
# Run backtests
|
||||
bt_results = [backtest(bt_df, w, t, time_factor, commission, 'heatmap')
|
||||
for w in window_list for t in threshold_list]
|
||||
ft_results = [backtest(ft_df, w, t, time_factor, commission, 'heatmap')
|
||||
for w in window_list for t in threshold_list]
|
||||
|
||||
# Reverse threshold list for display
|
||||
threshold_list.reverse()
|
||||
|
||||
# Process results
|
||||
bt_results_df = pd.DataFrame(bt_results)
|
||||
ft_results_df = pd.DataFrame(ft_results)
|
||||
|
||||
bt_data_table = bt_results_df.pivot(index='threshold', columns='window', values='sharpe')
|
||||
ft_data_table = ft_results_df.pivot(index='threshold', columns='window', values='sharpe')
|
||||
|
||||
# Sort tables
|
||||
bt_data_table = bt_data_table.sort_index(ascending=False)
|
||||
ft_data_table = ft_data_table.sort_index(ascending=False)
|
||||
|
||||
# Use custom title or default if none provided
|
||||
bt_title = f"{heatmap_title}_BT_sharpe" if heatmap_title else "Backtest Sharpe Ratio"
|
||||
ft_title = f"{heatmap_title}_FT_sharpe" if heatmap_title else "Forward Test Sharpe Ratio"
|
||||
|
||||
# Create heatmap figure
|
||||
fig = make_subplots(
|
||||
rows=1, cols=2,
|
||||
subplot_titles=(bt_title, ft_title),
|
||||
horizontal_spacing=0.17,
|
||||
column_widths=[0.5, 0.5]
|
||||
)
|
||||
|
||||
# Custom colorscale
|
||||
custom_colorscale = [
|
||||
[0.0, '#FFFFBF'], # Light yellow
|
||||
[0.2, '#E0F3F8'], # Pale blue
|
||||
[0.4, '#ABD9E9'], # Very light blue
|
||||
[0.6, '#74ADD1'], # Light blue
|
||||
[0.8, '#4575B4'], # Blue
|
||||
[1.0, '#313695'] # Deep blue
|
||||
]
|
||||
|
||||
# Add backtest heatmap
|
||||
fig.add_trace(
|
||||
go.Heatmap(
|
||||
z=bt_data_table.values,
|
||||
x=bt_data_table.columns,
|
||||
y=np.round(bt_data_table.index, 2),
|
||||
text=np.round(bt_data_table.values, 2),
|
||||
texttemplate='%{text}',
|
||||
textfont={"size": 9},
|
||||
colorscale=custom_colorscale,
|
||||
showscale=True,
|
||||
colorbar=dict(
|
||||
x=0.45,
|
||||
thickness=15,
|
||||
title="Sharpe Ratio",
|
||||
titleside="right",
|
||||
len=0.9
|
||||
)
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Add forward test heatmap
|
||||
fig.add_trace(
|
||||
go.Heatmap(
|
||||
z=ft_data_table.values,
|
||||
x=ft_data_table.columns,
|
||||
y=np.round(ft_data_table.index, 2),
|
||||
text=np.round(ft_data_table.values, 2),
|
||||
texttemplate='%{text}',
|
||||
textfont={"size": 9},
|
||||
colorscale=custom_colorscale,
|
||||
showscale=True,
|
||||
colorbar=dict(
|
||||
x=1.02,
|
||||
thickness=15,
|
||||
title="Sharpe Ratio",
|
||||
titleside="right",
|
||||
len=0.9
|
||||
)
|
||||
),
|
||||
row=1, col=2
|
||||
)
|
||||
|
||||
# Calculate dimensions
|
||||
cell_height = 25
|
||||
cell_width = 15
|
||||
height = max(cell_height * len(threshold_list), 500)
|
||||
width = max(cell_width * len(window_list) * 3.2, 1800)
|
||||
|
||||
# Update layout
|
||||
fig.update_layout(
|
||||
height=height,
|
||||
width=width,
|
||||
showlegend=False,
|
||||
title_x=0.5,
|
||||
margin=dict(l=50, r=120, t=50, b=50),
|
||||
paper_bgcolor='white',
|
||||
plot_bgcolor='white',
|
||||
font=dict(size=12)
|
||||
)
|
||||
|
||||
# Update axes
|
||||
for i in range(1, 3):
|
||||
fig.update_xaxes(
|
||||
title_text="Window",
|
||||
tickmode='array',
|
||||
tickvals=window_list,
|
||||
ticktext=window_list,
|
||||
tickangle=45,
|
||||
row=1, col=i
|
||||
)
|
||||
fig.update_yaxes(
|
||||
title_text="Threshold",
|
||||
tickmode='array',
|
||||
tickvals=threshold_list,
|
||||
ticktext=[f"{x:.2f}" for x in threshold_list],
|
||||
row=1, col=i
|
||||
)
|
||||
|
||||
return bt_results_df.to_dict('records'), fig, ft_results_df.to_dict('records')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in heatmap generation: {str(e)}")
|
||||
return {}, {}, {}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,369 @@
|
||||
import plotly.graph_objects as go
|
||||
import pandas as pd
|
||||
from dash import html
|
||||
from layouts.components import create_metric_card
|
||||
|
||||
def create_equity_figure(result_dict):
|
||||
"""
|
||||
Creates an equity curve figure comparing strategy performance against benchmark.
|
||||
"""
|
||||
fig = go.Figure()
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=pd.to_datetime(result_dict['Dates']),
|
||||
y=result_dict['Equity Curve'],
|
||||
name='Strategy',
|
||||
line=dict(color='#2C7FB8', width=2)
|
||||
))
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=pd.to_datetime(result_dict['Dates']),
|
||||
y=result_dict['Benchmark'],
|
||||
name='Benchmark',
|
||||
line=dict(color='#FC8D59', width=2)
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
title={
|
||||
'text': "Equity Curve",
|
||||
'y': 0.95,
|
||||
'x': 0.5,
|
||||
'xanchor': 'center',
|
||||
'yanchor': 'top',
|
||||
'font': dict(size=16)
|
||||
},
|
||||
xaxis=dict(
|
||||
type="date",
|
||||
showgrid=True
|
||||
),
|
||||
yaxis_title="Cumulative Return",
|
||||
template='plotly_white',
|
||||
height=500,
|
||||
hovermode='x unified',
|
||||
legend=dict(
|
||||
yanchor="top",
|
||||
y=0.99,
|
||||
xanchor="left",
|
||||
x=0.01,
|
||||
bgcolor='rgba(255, 255, 255, 0.8)'
|
||||
),
|
||||
margin=dict(l=50, r=50, t=50, b=50)
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def create_underwater_figure(result_dict):
|
||||
"""
|
||||
Creates an underwater (drawdown) plot.
|
||||
"""
|
||||
underwater = pd.Series(result_dict['Equity Curve']) - pd.Series(result_dict['Equity Curve']).cummax()
|
||||
fig = go.Figure()
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=pd.to_datetime(result_dict['Dates']),
|
||||
y=underwater,
|
||||
fill='tozeroy',
|
||||
name='Drawdown',
|
||||
line=dict(color='#2C7FB8', width=2),
|
||||
fillcolor='rgba(44, 127, 184, 0.3)'
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
title={
|
||||
'text': "Drawdown",
|
||||
'y': 0.95,
|
||||
'x': 0.5,
|
||||
'xanchor': 'center',
|
||||
'yanchor': 'top',
|
||||
'font': dict(size=16)
|
||||
},
|
||||
xaxis=dict(
|
||||
type="date",
|
||||
showgrid=True
|
||||
),
|
||||
yaxis_title="Drawdown",
|
||||
template='plotly_white',
|
||||
height=300,
|
||||
hovermode='x unified',
|
||||
showlegend=False,
|
||||
margin=dict(l=50, r=50, t=50, b=50)
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def create_yearly_returns_figure(result_dict):
|
||||
"""
|
||||
Creates a yearly returns comparison bar plot.
|
||||
"""
|
||||
try:
|
||||
dates = pd.to_datetime(result_dict['Dates'])
|
||||
daily_returns = result_dict.get('Returns', None)
|
||||
|
||||
if daily_returns is None:
|
||||
daily_returns = pd.Series(result_dict['Equity Curve'], index=dates).diff()
|
||||
|
||||
daily_returns = pd.DataFrame({
|
||||
'equity': daily_returns,
|
||||
'benchmark': pd.Series(result_dict['Benchmark'], index=dates).diff()
|
||||
})
|
||||
|
||||
yearly_returns = daily_returns.resample('Y').sum()
|
||||
|
||||
fig = go.Figure()
|
||||
|
||||
fig.add_trace(go.Bar(
|
||||
x=yearly_returns.index.year,
|
||||
y=yearly_returns['equity'],
|
||||
name='Strategy',
|
||||
marker_color='#2C7FB8'
|
||||
))
|
||||
|
||||
fig.add_trace(go.Bar(
|
||||
x=yearly_returns.index.year,
|
||||
y=yearly_returns['benchmark'],
|
||||
name='Benchmark',
|
||||
marker_color='#FC8D59'
|
||||
))
|
||||
|
||||
fig.add_hline(y=0, line_dash="dash", line_color="red", opacity=0.5)
|
||||
|
||||
fig.update_layout(
|
||||
title={
|
||||
'text': "Yearly Returns",
|
||||
'y': 0.95,
|
||||
'x': 0.5,
|
||||
'xanchor': 'center',
|
||||
'yanchor': 'top',
|
||||
'font': dict(size=16)
|
||||
},
|
||||
yaxis_title="Returns",
|
||||
barmode='group',
|
||||
template='plotly_white',
|
||||
showlegend=True,
|
||||
legend=dict(
|
||||
orientation="h",
|
||||
y=1.1,
|
||||
x=0.5,
|
||||
xanchor="center",
|
||||
yanchor="bottom"
|
||||
),
|
||||
height=300,
|
||||
margin=dict(l=50, r=50, t=80, b=50),
|
||||
bargap=0.2,
|
||||
bargroupgap=0.1
|
||||
)
|
||||
|
||||
fig.update_xaxes(
|
||||
tickangle=0,
|
||||
dtick=1,
|
||||
tickfont=dict(size=12),
|
||||
range=[yearly_returns.index.year.min() - 0.5, yearly_returns.index.year.max() + 0.5]
|
||||
)
|
||||
|
||||
fig.update_yaxes(
|
||||
title_font=dict(size=12),
|
||||
tickfont=dict(size=12),
|
||||
zeroline=True,
|
||||
zerolinewidth=1,
|
||||
zerolinecolor='red'
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in create_yearly_returns_plot: {str(e)}")
|
||||
return go.Figure()
|
||||
|
||||
def create_metric_cards(result_dict):
|
||||
"""
|
||||
Creates metric cards displaying strategy performance metrics.
|
||||
"""
|
||||
metrics_data = [
|
||||
('Return (Ann.)', f"{result_dict['Return (Ann.) [%]']:.2f}%", '#e3f2fd'),
|
||||
('Sharpe Ratio', f"{result_dict['Sharpe Ratio']:.2f}", '#e3f2fd'),
|
||||
('Calmar Ratio', f"{result_dict['Calmar Ratio']:.2f}", '#e3f2fd'),
|
||||
('Max Drawdown', f"{result_dict['Max. Drawdown [%]']:.2f}%", '#e3f2fd'),
|
||||
('Beta', f"{result_dict['Beta']:.3f}", '#e3f2fd')
|
||||
]
|
||||
|
||||
if 'Win Rate [%]' in result_dict:
|
||||
trading_metrics = [
|
||||
('Win Rate', f"{result_dict['Win Rate [%]']:.2f}%", '#f1f8e9'),
|
||||
('Avg Win', f"{result_dict['Avg Win [%]']:.2f}%", '#f1f8e9'),
|
||||
('Avg Loss', f"{result_dict['Avg Loss [%]']:.2f}%", '#f1f8e9'),
|
||||
('Number of Trades', f"{result_dict['Number of Trades']}", '#f1f8e9'),
|
||||
('Long Duration', f"{result_dict['Long Duration [%]']:.2f}%", '#fff3e0'),
|
||||
('Short Duration', f"{result_dict['Short Duration [%]']:.2f}%", '#fff3e0'),
|
||||
('Holding Time [%]', f"{result_dict['Turnover [%]']:.2f}%", '#fff3e0'),
|
||||
('Holding Time', f"{result_dict['Turnover Period']:.2f}", '#fff3e0')
|
||||
]
|
||||
metrics_data.extend(trading_metrics)
|
||||
|
||||
metrics_style = {
|
||||
'padding': '12px',
|
||||
'backgroundColor': 'white',
|
||||
'borderRadius': '8px',
|
||||
'boxShadow': '0 1px 3px rgba(0,0,0,0.1)',
|
||||
'textAlign': 'center',
|
||||
'minWidth': '150px',
|
||||
'transition': 'transform 0.2s ease-in-out'
|
||||
}
|
||||
|
||||
metric_cards = []
|
||||
for label, value, bg_color in metrics_data:
|
||||
current_style = metrics_style.copy()
|
||||
current_style['backgroundColor'] = bg_color
|
||||
metric_cards.append(
|
||||
html.Div([
|
||||
html.Div(label, style={
|
||||
'fontSize': '13px',
|
||||
'color': '#555',
|
||||
'fontWeight': '500',
|
||||
'marginBottom': '4px',
|
||||
'whiteSpace': 'nowrap'
|
||||
}),
|
||||
html.Div(value, style={
|
||||
'fontSize': '15px',
|
||||
'fontWeight': 'bold',
|
||||
'color': '#2c3e50'
|
||||
})
|
||||
], style=current_style)
|
||||
)
|
||||
|
||||
return metric_cards
|
||||
|
||||
def create_strategy_header(mode, current_selection, start_date, end_date, click_data=None):
|
||||
"""
|
||||
Creates the strategy header with parameters and period information.
|
||||
"""
|
||||
if mode == 'aggregative' and current_selection:
|
||||
strategy_buttons = [
|
||||
html.Div([
|
||||
html.Span(
|
||||
f"({s['window']}, {s['threshold']:.2f})",
|
||||
style={'marginRight': '4px'}
|
||||
),
|
||||
html.Button(
|
||||
"×",
|
||||
id={'type': 'remove-strategy', 'index': i},
|
||||
n_clicks=0,
|
||||
style={
|
||||
'border': 'none',
|
||||
'background': 'transparent',
|
||||
'color': '#666',
|
||||
'fontSize': '18px',
|
||||
'fontWeight': 'bold',
|
||||
'cursor': 'pointer',
|
||||
'padding': '2px 8px',
|
||||
'verticalAlign': 'middle',
|
||||
'lineHeight': '1',
|
||||
'transition': 'color 0.2s',
|
||||
'margin': '0 2px',
|
||||
'borderRadius': '50%',
|
||||
}
|
||||
)
|
||||
], style={
|
||||
'display': 'inline-flex',
|
||||
'alignItems': 'center',
|
||||
'margin': '2px 4px',
|
||||
'padding': '4px 8px',
|
||||
'backgroundColor': '#f0f0f0',
|
||||
'border': '1px solid #ddd',
|
||||
'borderRadius': '16px',
|
||||
'fontSize': '14px'
|
||||
}) for i, s in enumerate(current_selection)
|
||||
]
|
||||
|
||||
return html.Div([
|
||||
html.Div("Combined Strategy Details",
|
||||
style={'fontSize': '18px', 'fontWeight': 'bold', 'marginBottom': '8px'}),
|
||||
html.Div([
|
||||
html.Span("Parameters: ", style={'marginRight': '8px'}),
|
||||
html.Div(strategy_buttons, style={'display': 'inline-block'})
|
||||
], style={'marginBottom': '8px'}),
|
||||
html.Div(
|
||||
f"Period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
|
||||
if start_date and end_date else ""
|
||||
)
|
||||
])
|
||||
|
||||
elif click_data:
|
||||
window = click_data['points'][0]['x']
|
||||
threshold = click_data['points'][0]['y']
|
||||
return html.Div([
|
||||
html.Div("Strategy Details",
|
||||
style={'fontSize': '18px', 'fontWeight': 'bold', 'marginBottom': '8px'}),
|
||||
html.Div([
|
||||
html.Span("Parameters: ", style={'marginRight': '8px'}),
|
||||
html.Span(f"({window}, {threshold:.2f})")
|
||||
], style={'marginBottom': '8px'}),
|
||||
html.Div(
|
||||
f"Period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
|
||||
if start_date and end_date else ""
|
||||
)
|
||||
])
|
||||
|
||||
return html.Div("Click on heatmap to see strategy details")
|
||||
|
||||
def create_error_message(error_type, start_date=None, end_date=None, error_message=None):
|
||||
"""
|
||||
Creates error message displays.
|
||||
"""
|
||||
if error_type == "no_trade":
|
||||
return html.Div([
|
||||
html.Div("No Trade Data",
|
||||
style={'fontSize': '18px', 'fontWeight': 'bold', 'marginBottom': '8px'}),
|
||||
html.Div(
|
||||
f"There is no trade from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
|
||||
)
|
||||
])
|
||||
else:
|
||||
return html.Div([
|
||||
html.Div("Error",
|
||||
style={'fontSize': '18px', 'fontWeight': 'bold', 'marginBottom': '8px'}),
|
||||
html.Div(f"An error occurred: {error_message}")
|
||||
])
|
||||
|
||||
def create_metrics_component(result_dict):
|
||||
"""Create metrics component with grid layout using create_metric_card."""
|
||||
if result_dict is None or not isinstance(result_dict, dict):
|
||||
return html.Div() # Just return an empty div without styling
|
||||
|
||||
try:
|
||||
# Define metrics in a grid-friendly format
|
||||
metrics_grid = [
|
||||
[
|
||||
('Return (Ann.)', f"{result_dict.get('Return (Ann.) [%]', 0):.2f}%"),
|
||||
('Sharpe Ratio', f"{result_dict.get('Sharpe Ratio', 0):.2f}")
|
||||
],
|
||||
[
|
||||
('Calmar Ratio', f"{result_dict.get('Calmar Ratio', 0):.2f}"),
|
||||
('Max Drawdown', f"{result_dict.get('Max. Drawdown [%]', 0):.2f}%")
|
||||
],
|
||||
[
|
||||
('Beta', f"{result_dict.get('Beta', 0):.3f}"),
|
||||
(None, None)
|
||||
]
|
||||
]
|
||||
|
||||
# Create grid layout
|
||||
grid_rows = []
|
||||
for row in metrics_grid:
|
||||
row_cells = []
|
||||
for label, value in row:
|
||||
if label is not None:
|
||||
cell = create_metric_card(label, value, '#e3f2fd')
|
||||
row_cells.append(cell)
|
||||
|
||||
grid_rows.append(html.Div(row_cells, style={
|
||||
'display': 'flex',
|
||||
'justifyContent': 'flex-start',
|
||||
'gap': '10px',
|
||||
'marginBottom': '10px'
|
||||
}))
|
||||
|
||||
return html.Div(grid_rows) # Remove the container styling
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in create_metrics_component: {str(e)}")
|
||||
return html.Div() # Return empty div without styling
|
@@ -0,0 +1,26 @@
|
||||
from dash import Output
|
||||
|
||||
STRATEGY_OUTPUTS = [
|
||||
Output('strategy-metrics', 'children', allow_duplicate=True),
|
||||
Output('equity-curve', 'figure', allow_duplicate=True),
|
||||
Output('underwater-plot', 'figure', allow_duplicate=True),
|
||||
Output('yearly-returns-plot', 'figure', allow_duplicate=True),
|
||||
Output('strategy-header', 'children', allow_duplicate=True),
|
||||
Output('date-range-picker', 'start_date', allow_duplicate=True),
|
||||
Output('date-range-picker', 'end_date', allow_duplicate=True),
|
||||
Output('selected-strategies', 'data', allow_duplicate=True),
|
||||
Output('strategy-clear-selection', 'style', allow_duplicate=True),
|
||||
Output('selected-count', 'children', allow_duplicate=True)
|
||||
]
|
||||
|
||||
def get_base_style(mode):
|
||||
return {
|
||||
'marginLeft': '10px',
|
||||
'padding': '5px 10px',
|
||||
'backgroundColor': '#dc3545',
|
||||
'color': 'white',
|
||||
'border': 'none',
|
||||
'borderRadius': '4px',
|
||||
'cursor': 'pointer',
|
||||
'display': 'block' if mode == 'aggregative' else 'none'
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
# callbacks/strategy/helpers.py
|
||||
from dash import html
|
||||
import pandas as pd
|
||||
from models.backtest import backtest, combine_strategies
|
||||
from utils.helpers import get_default_date_range
|
||||
from .constants import get_base_style
|
||||
from .components import (
|
||||
create_equity_figure,
|
||||
create_underwater_figure,
|
||||
create_yearly_returns_figure,
|
||||
create_metric_cards,
|
||||
create_strategy_header,
|
||||
create_error_message
|
||||
)
|
||||
|
||||
def get_default_outputs():
|
||||
"""Return default outputs when no interaction has occurred."""
|
||||
return [], {}, {}, {}, html.Div("Click on heatmap to see strategy details"), None, None, [], {'display': 'none'}, ''
|
||||
|
||||
def handle_mode_change(trigger_id, mode, current_selection):
|
||||
"""Handle mode changes and clear selection."""
|
||||
if trigger_id == 'strategy-clear-selection' or mode == 'single':
|
||||
current_selection = []
|
||||
|
||||
selected_count = f'Selected strategies: {len(current_selection)}' if mode == 'aggregative' else ''
|
||||
from app import full_data
|
||||
default_start, default_end = get_default_date_range(full_data)
|
||||
|
||||
return [], {}, {}, {}, html.Div("Click on heatmap to see strategy details"), \
|
||||
default_start, default_end, current_selection, get_base_style(mode), selected_count
|
||||
|
||||
def get_date_range(trigger_id, start_date, end_date):
|
||||
"""Handle date range reset and processing."""
|
||||
if trigger_id == 'reset-date-range':
|
||||
from app import full_data
|
||||
start_date, end_date = get_default_date_range(full_data)
|
||||
|
||||
if start_date and end_date:
|
||||
start_date = pd.to_datetime(start_date)
|
||||
end_date = pd.to_datetime(end_date)
|
||||
|
||||
return start_date, end_date
|
||||
|
||||
def get_filtered_data(start_date, end_date):
|
||||
"""Get data filtered by date range."""
|
||||
from app import full_data
|
||||
if start_date and end_date:
|
||||
return full_data[start_date:end_date]
|
||||
return full_data
|
||||
|
||||
def update_strategy_selection(mode, trigger_id, click_data, current_selection):
|
||||
"""Update strategy selection based on mode and user interaction."""
|
||||
if mode == 'aggregative' and trigger_id == 'combined-heatmap' and click_data:
|
||||
new_strategy = {
|
||||
'window': click_data['points'][0]['x'],
|
||||
'threshold': click_data['points'][0]['y']
|
||||
}
|
||||
if new_strategy not in current_selection:
|
||||
return current_selection + [new_strategy]
|
||||
elif mode == 'single':
|
||||
return []
|
||||
return current_selection
|
||||
|
||||
def get_backtest_results(mode, current_selection, data, click_data, time_factor, commission):
|
||||
"""Get backtest results based on mode and selected strategies."""
|
||||
if mode == 'single' or not current_selection:
|
||||
window = click_data['points'][0]['x']
|
||||
threshold = click_data['points'][0]['y']
|
||||
return backtest(data, window, threshold, time_factor, commission, 'full_report')
|
||||
return combine_strategies(data, current_selection, time_factor, commission)
|
||||
|
||||
def create_output_components(result_dict, mode, current_selection, start_date, end_date, click_data):
|
||||
"""Create all visual components."""
|
||||
metric_cards = create_metric_cards(result_dict)
|
||||
equity_fig = create_equity_figure(result_dict)
|
||||
underwater_fig = create_underwater_figure(result_dict)
|
||||
yearly_returns_fig = create_yearly_returns_figure(result_dict)
|
||||
header_text = create_strategy_header(mode, current_selection, start_date, end_date, click_data)
|
||||
|
||||
base_style = get_base_style(mode)
|
||||
selected_count = f'Selected strategies: {len(current_selection)}' if mode == 'aggregative' else ''
|
||||
|
||||
return (metric_cards, equity_fig, underwater_fig, yearly_returns_fig,
|
||||
header_text, start_date, end_date, current_selection,
|
||||
base_style, selected_count)
|
||||
|
||||
def handle_error(error_type, start_date, end_date, current_selection, mode, error_message=None):
|
||||
"""Handle various error cases."""
|
||||
base_style = get_base_style(mode)
|
||||
selected_count = f'Selected strategies: {len(current_selection)}' if mode == 'aggregative' else ''
|
||||
|
||||
if error_type == "zero_division":
|
||||
error_msg = create_error_message("no_trade", start_date, end_date)
|
||||
else:
|
||||
error_msg = create_error_message("error", error_message=error_message)
|
||||
|
||||
return [], {}, {}, {}, error_msg, start_date, end_date, current_selection, base_style, selected_count
|
@@ -0,0 +1,187 @@
|
||||
from dash import Input, Output, State, html, ALL, callback_context
|
||||
import dash
|
||||
from .constants import STRATEGY_OUTPUTS
|
||||
from .helpers import (
|
||||
get_filtered_data,
|
||||
get_backtest_results,
|
||||
create_output_components
|
||||
)
|
||||
import pandas as pd
|
||||
from .components import (
|
||||
create_equity_figure,
|
||||
create_underwater_figure,
|
||||
create_yearly_returns_figure,
|
||||
create_metrics_component
|
||||
)
|
||||
|
||||
def register_remove_callback(app):
|
||||
@app.callback(
|
||||
[Output('selected-strategies', 'data'),
|
||||
Output('selected-count', 'children'),
|
||||
Output('equity-curve', 'figure'),
|
||||
Output('underwater-plot', 'figure'),
|
||||
Output('yearly-returns-plot', 'figure'),
|
||||
Output('strategy-header', 'children'),
|
||||
Output('strategy-metrics', 'children')],
|
||||
[Input({'type': 'remove-strategy', 'index': ALL}, 'n_clicks')],
|
||||
[State('selected-strategies', 'data'),
|
||||
State('backtest-results', 'data'),
|
||||
State('time-factor', 'value'),
|
||||
State('commission', 'value'),
|
||||
State('date-range-picker', 'start_date'),
|
||||
State('date-range-picker', 'end_date'),
|
||||
State('dashboard-mode', 'value')],
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def remove_strategy(n_clicks_list, current_selection, backtest_results,
|
||||
time_factor, commission, start_date, end_date, mode):
|
||||
if not callback_context.triggered:
|
||||
raise dash.exceptions.PreventUpdate
|
||||
|
||||
trigger = callback_context.triggered[0]
|
||||
if trigger['value'] is None or trigger['value'] == 0:
|
||||
raise dash.exceptions.PreventUpdate
|
||||
|
||||
button_id = eval(trigger['prop_id'].split('.')[0])
|
||||
index_to_remove = button_id['index']
|
||||
|
||||
# Remove the strategy at the specified index
|
||||
updated_selection = [
|
||||
strategy for i, strategy in enumerate(current_selection)
|
||||
if i != index_to_remove
|
||||
]
|
||||
|
||||
# Convert string dates to datetime objects
|
||||
start_date = pd.to_datetime(start_date).date() if start_date else None
|
||||
end_date = pd.to_datetime(end_date).date() if end_date else None
|
||||
|
||||
try:
|
||||
# Get filtered data
|
||||
data = get_filtered_data(start_date, end_date)
|
||||
|
||||
# If there are still strategies left
|
||||
if updated_selection:
|
||||
# Recalculate backtest results for remaining strategies
|
||||
result_dict = get_backtest_results(
|
||||
mode='combined',
|
||||
current_selection=updated_selection,
|
||||
data=data,
|
||||
click_data=None,
|
||||
time_factor=float(time_factor),
|
||||
commission=float(commission) / 100
|
||||
)
|
||||
|
||||
# Create header with improved styling
|
||||
header = html.Div([
|
||||
html.Div("Combined Strategy Details",
|
||||
style={'fontSize': '18px', 'fontWeight': 'bold', 'marginBottom': '8px'}),
|
||||
html.Div([
|
||||
html.Span("Parameters: ", style={'marginRight': '8px'}),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Span(f"({s['window']}, {s['threshold']:.2f})",
|
||||
style={'marginRight': '4px'}),
|
||||
html.Button(
|
||||
"×",
|
||||
id={'type': 'remove-strategy', 'index': i},
|
||||
n_clicks=0,
|
||||
style={
|
||||
'border': 'none',
|
||||
'background': 'transparent',
|
||||
'color': '#666',
|
||||
'fontSize': '18px',
|
||||
'fontWeight': 'bold',
|
||||
'cursor': 'pointer',
|
||||
'padding': '2px 8px',
|
||||
'verticalAlign': 'middle',
|
||||
'lineHeight': '1',
|
||||
'transition': 'color 0.2s',
|
||||
'margin': '0 2px',
|
||||
'borderRadius': '50%'
|
||||
}
|
||||
)
|
||||
], style={
|
||||
'display': 'inline-flex',
|
||||
'alignItems': 'center',
|
||||
'margin': '2px 4px',
|
||||
'padding': '4px 8px',
|
||||
'backgroundColor': '#f0f0f0',
|
||||
'border': '1px solid #ddd',
|
||||
'borderRadius': '16px',
|
||||
'fontSize': '14px'
|
||||
}) for i, s in enumerate(updated_selection)
|
||||
], style={'display': 'inline-block'})
|
||||
], style={'marginBottom': '8px'}),
|
||||
html.Div(f"Period: {start_date} to {end_date}")
|
||||
])
|
||||
|
||||
# Create figures
|
||||
equity_fig = create_equity_figure(result_dict)
|
||||
underwater_fig = create_underwater_figure(result_dict)
|
||||
yearly_returns_fig = create_yearly_returns_figure(result_dict)
|
||||
metrics = create_metrics_component(result_dict)
|
||||
|
||||
else:
|
||||
# If no strategies left, return empty state
|
||||
empty_fig = {
|
||||
'data': [],
|
||||
'layout': {
|
||||
'title': {
|
||||
'text': 'No data to display',
|
||||
'y': 0.95,
|
||||
'x': 0.5,
|
||||
'xanchor': 'center',
|
||||
'yanchor': 'top',
|
||||
'font': dict(size=16)
|
||||
},
|
||||
'showlegend': False,
|
||||
'height': 300,
|
||||
'template': 'plotly_white',
|
||||
'xaxis': {'showgrid': False},
|
||||
'yaxis': {'showgrid': False},
|
||||
'margin': dict(l=50, r=50, t=50, b=50)
|
||||
}
|
||||
}
|
||||
|
||||
header = html.Div([
|
||||
html.Div("Strategy Parameters",
|
||||
style={'fontSize': '18px', 'fontWeight': 'bold', 'marginBottom': '8px'}),
|
||||
html.Div("No strategies selected. Click on the heatmap to select strategies.")
|
||||
])
|
||||
|
||||
equity_fig = underwater_fig = yearly_returns_fig = empty_fig
|
||||
metrics = create_metrics_component(None) # Will return empty white container
|
||||
|
||||
return (
|
||||
updated_selection,
|
||||
f"Selected strategies: {len(updated_selection)}",
|
||||
equity_fig,
|
||||
underwater_fig,
|
||||
yearly_returns_fig,
|
||||
header,
|
||||
metrics
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in remove_strategy callback: {str(e)}")
|
||||
empty_fig = {
|
||||
'data': [],
|
||||
'layout': {
|
||||
'title': 'Error occurred',
|
||||
'showlegend': False
|
||||
}
|
||||
}
|
||||
error_header = html.Div([
|
||||
html.Div("Error",
|
||||
style={'fontSize': '18px', 'fontWeight': 'bold', 'marginBottom': '8px'}),
|
||||
html.Div(f"An error occurred: {str(e)}")
|
||||
])
|
||||
return (
|
||||
updated_selection,
|
||||
f"Selected strategies: {len(updated_selection)}",
|
||||
empty_fig,
|
||||
empty_fig,
|
||||
empty_fig,
|
||||
error_header,
|
||||
create_metrics_component(None) # Will return empty white container
|
||||
)
|
@@ -0,0 +1,64 @@
|
||||
# callbacks/strategy/update_callback.py
|
||||
from dash import Input, Output, State
|
||||
import dash
|
||||
from .constants import STRATEGY_OUTPUTS
|
||||
from .helpers import (
|
||||
get_default_outputs,
|
||||
handle_mode_change,
|
||||
get_date_range,
|
||||
get_filtered_data,
|
||||
update_strategy_selection,
|
||||
get_backtest_results,
|
||||
create_output_components,
|
||||
handle_error
|
||||
)
|
||||
|
||||
def register_update_callback(app):
|
||||
@app.callback(
|
||||
STRATEGY_OUTPUTS,
|
||||
[Input('combined-heatmap', 'clickData'),
|
||||
Input('apply-date-range', 'n_clicks'),
|
||||
Input('reset-date-range', 'n_clicks'),
|
||||
Input('dashboard-mode', 'value'),
|
||||
Input('strategy-clear-selection', 'n_clicks')],
|
||||
[State('bt-end-date', 'date'),
|
||||
State('time-factor', 'value'),
|
||||
State('commission', 'value'),
|
||||
State('date-range-picker', 'start_date'),
|
||||
State('date-range-picker', 'end_date'),
|
||||
State('selected-strategies', 'data')],
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def update_all_outputs(click_data, apply_clicks, reset_clicks, mode, clear_clicks,
|
||||
bt_end_date, time_factor, commission,
|
||||
start_date, end_date, current_selection):
|
||||
"""Main callback function that updates all dashboard components."""
|
||||
# Get trigger context
|
||||
ctx = dash.callback_context
|
||||
if not ctx.triggered:
|
||||
return get_default_outputs()
|
||||
|
||||
trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
||||
|
||||
# Handle mode changes and clear selection
|
||||
if trigger_id in ['dashboard-mode', 'strategy-clear-selection']:
|
||||
return handle_mode_change(trigger_id, mode, current_selection)
|
||||
|
||||
try:
|
||||
# Process date range and data
|
||||
start_date, end_date = get_date_range(trigger_id, start_date, end_date)
|
||||
data = get_filtered_data(start_date, end_date)
|
||||
commission = commission / 100
|
||||
|
||||
# Update strategy selection and get results
|
||||
current_selection = update_strategy_selection(mode, trigger_id, click_data, current_selection)
|
||||
result_dict = get_backtest_results(mode, current_selection, data, click_data, time_factor, commission)
|
||||
|
||||
# Create and return components
|
||||
return create_output_components(result_dict, mode, current_selection, start_date, end_date, click_data)
|
||||
|
||||
except ZeroDivisionError:
|
||||
return handle_error("zero_division", start_date, end_date, current_selection, mode)
|
||||
except Exception as e:
|
||||
print(f"Error in strategy details update: {str(e)}")
|
||||
return handle_error("general", start_date, end_date, current_selection, mode, str(e))
|
@@ -0,0 +1,7 @@
|
||||
# callbacks/strategy_callbacks.py
|
||||
from .strategy.update_callback import register_update_callback
|
||||
# from .strategy.remove_callback import register_remove_callback
|
||||
|
||||
def register_strategy_callbacks(app):
|
||||
register_update_callback(app)
|
||||
# register_remove_callback(app)
|
@@ -0,0 +1,5 @@
|
||||
# Configuration settings
|
||||
DEFAULT_TIME_FACTOR = 8760
|
||||
DEFAULT_COMMISSION = 0.06
|
||||
DEFAULT_WINDOW_RANGE = (100, 1000, 100)
|
||||
DEFAULT_THRESHOLD_RANGE = (0.0, 2.0, 0.2)
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
from dash import html, dcc
|
||||
|
||||
def create_metric_card(label, value, bg_color):
|
||||
return html.Div([
|
||||
html.Div(label, style={
|
||||
'fontSize': '13px',
|
||||
'color': '#555',
|
||||
'fontWeight': '500',
|
||||
'marginBottom': '4px',
|
||||
'whiteSpace': 'nowrap'
|
||||
}),
|
||||
html.Div(value, style={
|
||||
'fontSize': '15px',
|
||||
'fontWeight': 'bold',
|
||||
'color': '#2c3e50'
|
||||
})
|
||||
], style={
|
||||
'padding': '12px',
|
||||
'backgroundColor': bg_color,
|
||||
'borderRadius': '8px',
|
||||
'boxShadow': '0 1px 3px rgba(0,0,0,0.1)',
|
||||
'textAlign': 'center',
|
||||
'minWidth': '150px'
|
||||
})
|
||||
|
||||
def date_range_picker():
|
||||
return html.Div([
|
||||
html.Label("Select Date Range:", style={'marginRight': '10px'}),
|
||||
dcc.DatePickerRange(
|
||||
id='date-range-picker',
|
||||
start_date=None,
|
||||
end_date=None
|
||||
),
|
||||
html.Button('Apply', id='apply-date-range', n_clicks=0,
|
||||
style={'marginLeft': '10px', 'backgroundColor': '#1f77b4', 'color': 'white', 'border': 'none', 'padding': '5px 15px', 'borderRadius': '4px'}),
|
||||
html.Button('Reset', id='reset-date-range', n_clicks=0,
|
||||
style={'marginLeft': '10px', 'backgroundColor': '#7f7f7f', 'color': 'white', 'border': 'none', 'padding': '5px 15px', 'borderRadius': '4px'})
|
||||
], style={'marginBottom': '20px'})
|
@@ -0,0 +1,432 @@
|
||||
from dash import html, dcc
|
||||
from layouts.components import create_metric_card
|
||||
|
||||
def create_layout(full_data):
|
||||
return html.Div([
|
||||
# Keep your existing stores
|
||||
dcc.Store(id='backtest-results', storage_type='memory'),
|
||||
dcc.Store(id='forwardtest-results', storage_type='memory'),
|
||||
dcc.Store(id='current-params', storage_type='memory'),
|
||||
# Add new store for selected strategies
|
||||
dcc.Store(id='selected-strategies', data=[], storage_type='memory'),
|
||||
|
||||
|
||||
html.Div([
|
||||
html.H1("Backtest Strategy Dashboard",
|
||||
style={'textAlign': 'center', 'marginBottom': '20px'}),
|
||||
|
||||
# Add the mode switch section here
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label("Dashboard Mode:", style={
|
||||
'fontWeight': '500',
|
||||
'color': '#2c3e50',
|
||||
'marginRight': '10px',
|
||||
'fontSize': '16px'
|
||||
}),
|
||||
dcc.RadioItems(
|
||||
id='dashboard-mode',
|
||||
options=[
|
||||
{'label': 'Single Strategy Mode', 'value': 'single'},
|
||||
{'label': 'Aggregative Mode', 'value': 'aggregative'}
|
||||
],
|
||||
value='single',
|
||||
style={'display': 'inline-block'},
|
||||
inputStyle={'marginRight': '5px'},
|
||||
labelStyle={'marginRight': '20px', 'cursor': 'pointer'}
|
||||
),
|
||||
# Changed ID for the second clear selection button
|
||||
html.Button(
|
||||
'Clear Selection',
|
||||
id='strategy-clear-selection',
|
||||
style={
|
||||
'marginLeft': '10px',
|
||||
'padding': '5px 10px',
|
||||
'backgroundColor': '#dc3545',
|
||||
'color': 'white',
|
||||
'border': 'none',
|
||||
'borderRadius': '4px',
|
||||
'cursor': 'pointer',
|
||||
'display': 'none' # Initially hidden
|
||||
}
|
||||
),
|
||||
html.Div(
|
||||
id='selected-count',
|
||||
style={
|
||||
'marginLeft': '15px',
|
||||
'display': 'inline-block',
|
||||
'color': '#2c3e50'
|
||||
}
|
||||
)
|
||||
], style={'display': 'flex', 'alignItems': 'center'})
|
||||
], style={
|
||||
'marginBottom': '20px',
|
||||
'padding': '15px',
|
||||
'backgroundColor': '#f8f9fa',
|
||||
'borderRadius': '8px',
|
||||
'border': '1px solid #e9ecef'
|
||||
}),
|
||||
|
||||
# Parameters input section
|
||||
html.Div([
|
||||
# First row of inputs
|
||||
# First row of inputs
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label("Backtest End Date:", style={
|
||||
'fontWeight': '500',
|
||||
'color': '#2c3e50',
|
||||
'marginBottom': '5px',
|
||||
'display': 'block'
|
||||
}),
|
||||
dcc.DatePickerSingle(
|
||||
id='bt-end-date',
|
||||
min_date_allowed=full_data.index[0],
|
||||
max_date_allowed=full_data.index[-1],
|
||||
initial_visible_month=full_data.index[0],
|
||||
date=full_data.index[len(full_data) // 2],
|
||||
display_format='YYYY-MM-DD',
|
||||
style={'width': '150px'}
|
||||
)
|
||||
], style={'marginRight': '20px', 'display': 'inline-block'}),
|
||||
|
||||
html.Div([
|
||||
html.Label("Time Factor:", style={
|
||||
'fontWeight': '500',
|
||||
'color': '#2c3e50',
|
||||
'marginBottom': '5px',
|
||||
'display': 'block'
|
||||
}),
|
||||
dcc.Input(
|
||||
id='time-factor',
|
||||
type='number',
|
||||
value=8760,
|
||||
style={
|
||||
'width': '100px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px'
|
||||
}
|
||||
)
|
||||
], style={'marginRight': '20px', 'display': 'inline-block'}),
|
||||
|
||||
html.Div([
|
||||
html.Label("Commission (%):", style={
|
||||
'fontWeight': '500',
|
||||
'color': '#2c3e50',
|
||||
'marginBottom': '5px',
|
||||
'display': 'block'
|
||||
}),
|
||||
dcc.Input(
|
||||
id='commission',
|
||||
type='number',
|
||||
value=0.06,
|
||||
step=0.01,
|
||||
style={
|
||||
'width': '80px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px'
|
||||
}
|
||||
)
|
||||
], style={'marginRight': '20px', 'display': 'inline-block'}),
|
||||
|
||||
html.Div([
|
||||
html.Label("Heatmap Title:", style={
|
||||
'fontWeight': '500',
|
||||
'color': '#2c3e50',
|
||||
'marginBottom': '5px',
|
||||
'display': 'block'
|
||||
}),
|
||||
dcc.Input(
|
||||
id='heatmap-title',
|
||||
type='text',
|
||||
placeholder='Enter heatmap title',
|
||||
autoComplete='off',
|
||||
style={
|
||||
'width': '200px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px'
|
||||
}
|
||||
)
|
||||
], style={'display': 'inline-block'}),
|
||||
], style={'marginBottom': '15px'}),
|
||||
|
||||
# Second row of inputs
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label("Window Range:", style={
|
||||
'fontWeight': '500',
|
||||
'color': '#2c3e50',
|
||||
'marginBottom': '5px',
|
||||
'display': 'block'
|
||||
}),
|
||||
html.Div([
|
||||
dcc.Input(
|
||||
id='window-start',
|
||||
type='number',
|
||||
value=100,
|
||||
placeholder='Start',
|
||||
style={
|
||||
'width': '80px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px',
|
||||
'marginRight': '10px'
|
||||
}
|
||||
),
|
||||
dcc.Input(
|
||||
id='window-end',
|
||||
type='number',
|
||||
value=1000,
|
||||
placeholder='End',
|
||||
style={
|
||||
'width': '80px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px',
|
||||
'marginRight': '10px'
|
||||
}
|
||||
),
|
||||
dcc.Input(
|
||||
id='window-step',
|
||||
type='number',
|
||||
value=100,
|
||||
placeholder='Step',
|
||||
style={
|
||||
'width': '80px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px'
|
||||
}
|
||||
),
|
||||
], style={'display': 'flex'})
|
||||
], style={'marginRight': '40px', 'display': 'inline-block'}),
|
||||
|
||||
html.Div([
|
||||
html.Label("Threshold Range:", style={
|
||||
'fontWeight': '500',
|
||||
'color': '#2c3e50',
|
||||
'marginBottom': '5px',
|
||||
'display': 'block'
|
||||
}),
|
||||
html.Div([
|
||||
dcc.Input(
|
||||
id='threshold-start',
|
||||
type='number',
|
||||
value=0.0,
|
||||
placeholder='Start',
|
||||
style={
|
||||
'width': '80px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px',
|
||||
'marginRight': '10px'
|
||||
}
|
||||
),
|
||||
dcc.Input(
|
||||
id='threshold-end',
|
||||
type='number',
|
||||
value=2.0,
|
||||
placeholder='End',
|
||||
style={
|
||||
'width': '80px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px',
|
||||
'marginRight': '10px'
|
||||
}
|
||||
),
|
||||
dcc.Input(
|
||||
id='threshold-step',
|
||||
type='number',
|
||||
value=0.2,
|
||||
placeholder='Step',
|
||||
style={
|
||||
'width': '80px',
|
||||
'height': '36px',
|
||||
'padding': '8px',
|
||||
'borderRadius': '4px',
|
||||
'border': '1px solid #dcdfe6',
|
||||
'fontSize': '14px'
|
||||
}
|
||||
),
|
||||
], style={'display': 'flex'})
|
||||
], style={'display': 'inline-block'}),
|
||||
], style={'marginBottom': '15px'}),
|
||||
|
||||
html.Button(
|
||||
'Run Backtest',
|
||||
id='run-backtest',
|
||||
n_clicks=0,
|
||||
style={
|
||||
'width': '150px',
|
||||
'height': '36px',
|
||||
'marginTop': '10px',
|
||||
'backgroundColor': '#1a73e8',
|
||||
'color': 'white',
|
||||
'border': 'none',
|
||||
'borderRadius': '4px',
|
||||
'cursor': 'pointer',
|
||||
'fontSize': '14px',
|
||||
'fontWeight': '500'
|
||||
}
|
||||
),
|
||||
], style={
|
||||
'padding': '20px',
|
||||
'backgroundColor': 'white',
|
||||
'borderRadius': '8px',
|
||||
'marginBottom': '20px',
|
||||
'boxShadow': '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}),
|
||||
|
||||
# Heatmap Container
|
||||
html.Div([
|
||||
dcc.Graph(id='combined-heatmap'),
|
||||
], style={
|
||||
'marginBottom': '30px',
|
||||
'backgroundColor': 'white',
|
||||
'borderRadius': '10px',
|
||||
'padding': '15px',
|
||||
'boxShadow': '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}),
|
||||
|
||||
# Strategy details section
|
||||
html.Div([
|
||||
html.H3("Strategy Details",
|
||||
id='strategy-header',
|
||||
style={'textAlign': 'center', 'marginTop': '30px', 'marginBottom': '20px'}),
|
||||
|
||||
# Date range picker section
|
||||
html.Div([
|
||||
html.Label("Select Date Range:", style={'fontWeight': 'bold', 'marginRight': '10px'}),
|
||||
dcc.DatePickerRange(
|
||||
id='date-range-picker',
|
||||
min_date_allowed=full_data.index[0],
|
||||
max_date_allowed=full_data.index[-1],
|
||||
start_date=full_data.index[0],
|
||||
end_date=full_data.index[-1],
|
||||
display_format='YYYY-MM-DD'
|
||||
),
|
||||
html.Button(
|
||||
'Apply',
|
||||
id='apply-date-range',
|
||||
style={
|
||||
'marginLeft': '10px',
|
||||
'backgroundColor': '#2c3e50',
|
||||
'color': 'white',
|
||||
'border': 'none',
|
||||
'padding': '5px 15px',
|
||||
'borderRadius': '5px',
|
||||
'cursor': 'pointer'
|
||||
}
|
||||
),
|
||||
html.Button(
|
||||
'Reset',
|
||||
id='reset-date-range',
|
||||
style={
|
||||
'marginLeft': '10px',
|
||||
'backgroundColor': '#7f7f7f',
|
||||
'color': 'white',
|
||||
'border': 'none',
|
||||
'padding': '5px 15px',
|
||||
'borderRadius': '5px',
|
||||
'cursor': 'pointer'
|
||||
}
|
||||
)
|
||||
], style={
|
||||
'marginBottom': '20px',
|
||||
'display': 'flex',
|
||||
'alignItems': 'center',
|
||||
'justifyContent': 'center'
|
||||
}),
|
||||
|
||||
# New flex container for metrics and plots
|
||||
html.Div([
|
||||
# Left side - Metrics and Yearly Returns
|
||||
html.Div([
|
||||
# Metrics
|
||||
html.Div(id='strategy-metrics', style={
|
||||
'display': 'grid',
|
||||
'gridTemplateColumns': 'repeat(2, 1fr)',
|
||||
'gap': '10px', # Reduced from 20px
|
||||
'padding': '10px', # Reduced from 20px
|
||||
'backgroundColor': '#f8f9fa',
|
||||
'borderRadius': '8px', # Reduced from 10px
|
||||
'boxShadow': '0 2px 4px rgba(0,0,0,0.1)',
|
||||
'height': 'fit-content',
|
||||
'marginBottom': '15px', # Reduced from 20px
|
||||
'fontSize': '12px' # Added to reduce text size
|
||||
}),
|
||||
|
||||
# Yearly Returns
|
||||
html.Div([
|
||||
dcc.Graph(id='yearly-returns-plot')
|
||||
], style={
|
||||
'backgroundColor': 'white',
|
||||
'borderRadius': '8px', # Reduced from 10px
|
||||
'padding': '10px', # Reduced from 15px
|
||||
'boxShadow': '0 2px 4px rgba(0,0,0,0.1)',
|
||||
'marginTop': '15px' # Reduced from 20px
|
||||
})
|
||||
], style={
|
||||
'width': '30%',
|
||||
'marginRight': '15px' # Reduced from 20px
|
||||
}),
|
||||
|
||||
# Right side - Plots
|
||||
html.Div([
|
||||
# Equity curve
|
||||
html.Div([
|
||||
dcc.Graph(id='equity-curve')
|
||||
], style={
|
||||
'marginBottom': '20px',
|
||||
'backgroundColor': 'white',
|
||||
'borderRadius': '10px',
|
||||
'padding': '15px',
|
||||
'boxShadow': '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}),
|
||||
|
||||
# Underwater plot
|
||||
html.Div([
|
||||
dcc.Graph(id='underwater-plot')
|
||||
], style={
|
||||
'backgroundColor': 'white',
|
||||
'borderRadius': '10px',
|
||||
'padding': '15px',
|
||||
'boxShadow': '0 2px 4px rgba(0,0,0,0.1)'
|
||||
})
|
||||
], style={
|
||||
'width': '70%', # Adjust this value to change the width of the plots section
|
||||
'display': 'flex',
|
||||
'flexDirection': 'column'
|
||||
})
|
||||
], style={
|
||||
'display': 'flex',
|
||||
'marginTop': '20px'
|
||||
})
|
||||
])
|
||||
], style={
|
||||
'maxWidth': '1800px',
|
||||
'margin': '0 auto',
|
||||
'padding': '20px',
|
||||
'backgroundColor': '#ffffff'
|
||||
})
|
||||
])
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,116 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from utils.helpers import zscore
|
||||
|
||||
def backtest(df, window, threshold, time_factor, commission, mode):
|
||||
df = df.copy()
|
||||
df['O2C_chg'] = df['Close'] / df['Open'] - 1
|
||||
factor = df.active_addresses
|
||||
df['indicator'] = zscore(factor, window)
|
||||
|
||||
longCondition = df['indicator'].shift() > threshold
|
||||
shortCondition = df['indicator'].shift() < -threshold
|
||||
|
||||
df['pos'] = np.where(longCondition, 1, np.where(shortCondition, -1, 0))
|
||||
df['pnl'] = df['pos'] * df['O2C_chg'] - commission * abs(df['pos'].diff())
|
||||
sharpe = df['pnl'].mean() / df['pnl'].std() * np.sqrt(time_factor)
|
||||
|
||||
if mode == 'heatmap':
|
||||
return pd.Series([window, threshold, sharpe], index=['window', 'threshold', 'sharpe'])
|
||||
|
||||
elif mode == 'full_report':
|
||||
ar = df.pnl.mean() * time_factor * 100
|
||||
mdd = (df.pnl.cumsum() - df.pnl.cumsum().cummax()).min() * 100
|
||||
calmar = ar / abs(mdd)
|
||||
|
||||
beta, _ = np.polyfit(df.Close.pct_change()[1:], df.pnl.dropna(), 1)
|
||||
num_trades = int(
|
||||
pd.concat([abs(df.pos.diff()), pd.Series(abs(df.pos).iloc[-1])]).fillna(df.pos.iloc[0]).cumsum().iloc[
|
||||
-1] / 2)
|
||||
long_duration = (len(df.pos[df.pos == 1]) / len(df.pos)) * 100
|
||||
short_duration = (len(df.pos[df.pos == -1]) / len(df.pos)) * 100
|
||||
|
||||
win_rate = len(df.pnl[df.pnl > 0]) / len(df.pnl[df.pnl > 0] + df.pnl[df.pnl < 0]) * 100
|
||||
avg_win = df.pnl[df.pnl > 0].mean() * 100
|
||||
avg_loss = df.pnl[df.pnl < 0].mean() * 100
|
||||
|
||||
turnover_pct = (long_duration + short_duration) / num_trades
|
||||
turnover = turnover_pct * len(df) * 0.01
|
||||
|
||||
result_dict = {
|
||||
'Return (Ann.) [%]': ar,
|
||||
'Sharpe Ratio': sharpe,
|
||||
'Calmar Ratio': calmar,
|
||||
'Max. Drawdown [%]': mdd,
|
||||
'Beta': beta,
|
||||
'Win Rate [%]': win_rate,
|
||||
'Avg Win [%]': avg_win,
|
||||
'Avg Loss [%]': avg_loss,
|
||||
'Number of Trades': num_trades,
|
||||
'Long Duration [%]': long_duration,
|
||||
'Short Duration [%]': short_duration,
|
||||
'Turnover [%]': turnover_pct,
|
||||
'Turnover Period': turnover,
|
||||
'Equity Curve': df.pnl.cumsum().tolist(),
|
||||
'Benchmark': df.O2C_chg.cumsum().tolist(),
|
||||
'Dates': df.index.tolist(),
|
||||
'Returns': df.pnl.tolist(), # Added daily PnL
|
||||
'Benchmark Returns': df.O2C_chg.tolist() # Added benchmark returns
|
||||
}
|
||||
|
||||
return result_dict
|
||||
|
||||
def combine_strategies(df, strategies, time_factor, commission):
|
||||
"""
|
||||
Combine multiple strategies and calculate aggregate metrics.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
df : pandas.DataFrame
|
||||
Input data with OHLCV columns
|
||||
strategies : list of dict
|
||||
List of strategies with 'window' and 'threshold' parameters
|
||||
time_factor : int
|
||||
Time factor for annualization
|
||||
commission : float
|
||||
Commission rate
|
||||
|
||||
Returns:
|
||||
--------
|
||||
dict
|
||||
Dictionary containing combined backtest results
|
||||
"""
|
||||
combined_pnl = pd.Series(0.0, index=df.index)
|
||||
combined_benchmark = pd.Series(0.0, index=df.index)
|
||||
|
||||
# Combine strategies
|
||||
for strategy in strategies:
|
||||
result = backtest(df, strategy['window'], strategy['threshold'],
|
||||
time_factor, commission, 'full_report')
|
||||
combined_pnl += pd.Series(result['Returns'], index=pd.to_datetime(result['Dates']))
|
||||
combined_benchmark += pd.Series(result['Benchmark Returns'], index=pd.to_datetime(result['Dates']))
|
||||
|
||||
# Average the returns
|
||||
n_strategies = len(strategies)
|
||||
combined_pnl = combined_pnl / n_strategies
|
||||
combined_benchmark = combined_benchmark / n_strategies
|
||||
|
||||
# Calculate metrics
|
||||
ar = combined_pnl.mean() * time_factor * 100
|
||||
mdd = (combined_pnl.cumsum() - combined_pnl.cumsum().cummax()).min() * 100
|
||||
calmar = ar / abs(mdd)
|
||||
sharpe = combined_pnl.mean() / combined_pnl.std() * np.sqrt(time_factor)
|
||||
beta, _ = np.polyfit(df.Close.pct_change()[1:], combined_pnl.dropna(), 1)
|
||||
|
||||
return {
|
||||
'Returns': combined_pnl.values,
|
||||
'Benchmark Returns': combined_benchmark.values,
|
||||
'Dates': df.index,
|
||||
'Equity Curve': combined_pnl.cumsum().values,
|
||||
'Benchmark': combined_benchmark.cumsum().values,
|
||||
'Return (Ann.) [%]': ar,
|
||||
'Sharpe Ratio': sharpe,
|
||||
'Calmar Ratio': calmar,
|
||||
'Max. Drawdown [%]': mdd,
|
||||
'Beta': beta
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
def load_data(file_path='data/data.csv'):
|
||||
try:
|
||||
df = pd.read_csv(file_path)
|
||||
df['date'] = pd.to_datetime(df['date'])
|
||||
df.set_index('date', inplace=True)
|
||||
|
||||
required_columns = ['Open', 'Close', 'active_addresses']
|
||||
missing_columns = [col for col in required_columns if col not in df.columns]
|
||||
|
||||
if missing_columns:
|
||||
raise ValueError(f"Missing required columns: {missing_columns}")
|
||||
|
||||
return df
|
||||
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"{file_path} not found")
|
||||
except Exception as e:
|
||||
raise Exception(f"Error loading data: {str(e)}")
|
||||
|
||||
def get_default_date_range(df):
|
||||
"""Get the earliest and latest dates from the dataframe"""
|
||||
return df.index.min().strftime('%Y-%m-%d'), df.index.max().strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
def zscore(x, window):
|
||||
return (x - x.rolling(window).mean()) / x.rolling(window).std()
|
10
hkdse_maths_tutor/quotation1/notes.md
Normal file
10
hkdse_maths_tutor/quotation1/notes.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
tags: python, dashboard
|
||||
---
|
||||
|
||||
主要想 de 一個 bug ,
|
||||
|
||||
加整多幾個
|
||||
|
||||
第一功能就係新增一個 correlation heatmap,
|
||||
另外就新增一個 export 功能,把我所選的數據 export 到 csv
|
Reference in New Issue
Block a user