mirror of
https://github.com/polakowo/vectorbt.git
synced 2022-03-22 01:31:39 +03:00
1587 lines
73 KiB
Python
1587 lines
73 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (c) 2021 Oleg Polakow. All rights reserved.
|
|
# This code is licensed under Apache 2.0 with Commons Clause license (see LICENSE.md for details)
|
|
|
|
# Run this app with `python app.py` and
|
|
# visit http://127.0.0.1:8050/ in your web browser.
|
|
|
|
import dash
|
|
import dash_table
|
|
import dash_core_components as dcc
|
|
import dash_bootstrap_components as dbc
|
|
import dash_html_components as html
|
|
from dash.dependencies import State, Input, Output
|
|
from flask_caching import Cache
|
|
import plotly.graph_objects as go
|
|
|
|
import os
|
|
import numpy as np
|
|
import pandas as pd
|
|
import json
|
|
import random
|
|
import yfinance as yf
|
|
import talib
|
|
from talib import abstract
|
|
from talib._ta_lib import (
|
|
CandleSettingType,
|
|
RangeType,
|
|
_ta_set_candle_settings
|
|
)
|
|
from vectorbt import settings
|
|
from vectorbt.utils.config import merge_dicts
|
|
from vectorbt.utils.colors import adjust_opacity
|
|
from vectorbt.portfolio.enums import Direction, DirectionConflictMode
|
|
from vectorbt.portfolio.base import Portfolio
|
|
|
|
USE_CACHING = os.environ.get(
|
|
"USE_CACHING",
|
|
"True",
|
|
) == "True"
|
|
HOST = os.environ.get(
|
|
"HOST",
|
|
"127.0.0.1",
|
|
)
|
|
PORT = int(os.environ.get(
|
|
"PORT",
|
|
8050,
|
|
))
|
|
DEBUG = os.environ.get(
|
|
"DEBUG",
|
|
"True",
|
|
) == "True"
|
|
GITHUB_LINK = os.environ.get(
|
|
"GITHUB_LINK",
|
|
"https://github.com/polakowo/vectorbt/tree/master/apps/candlestick-patterns",
|
|
)
|
|
|
|
app = dash.Dash(
|
|
__name__,
|
|
meta_tags=[
|
|
{
|
|
"name": "viewport",
|
|
"content": "width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no",
|
|
}
|
|
],
|
|
external_stylesheets=[dbc.themes.GRID]
|
|
)
|
|
CACHE_CONFIG = {
|
|
'CACHE_TYPE': 'filesystem' if USE_CACHING else 'null',
|
|
'CACHE_DIR': 'data',
|
|
'CACHE_DEFAULT_TIMEOUT': 0,
|
|
'CACHE_THRESHOLD': 50
|
|
}
|
|
cache = Cache()
|
|
cache.init_app(app.server, config=CACHE_CONFIG)
|
|
|
|
# Settings
|
|
periods = ['1d', '5d', '1mo', '3mo', '6mo', '1y', '2y', '5y', '10y', 'ytd', 'max']
|
|
intervals = ['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1d', '5d', '1wk', '1mo', '3mo']
|
|
patterns = talib.get_function_groups()['Pattern Recognition']
|
|
stats_table_columns = ["Metric", "Buy & Hold", "Random (Median)", "Strategy", "Z-Score"]
|
|
directions = Direction._fields
|
|
conflict_modes = DirectionConflictMode._fields
|
|
plot_types = ['OHLC', 'Candlestick']
|
|
|
|
# Colors
|
|
color_schema = settings['plotting']['color_schema']
|
|
bgcolor = "#272a32"
|
|
dark_bgcolor = "#1d2026"
|
|
fontcolor = "#9fa6b7"
|
|
dark_fontcolor = "#7b7d8d"
|
|
gridcolor = "#323b56"
|
|
loadcolor = "#387c9e"
|
|
active_color = "#88ccee"
|
|
|
|
# Defaults
|
|
data_path = 'data/data.h5'
|
|
default_metric = 'Total Return [%]'
|
|
default_symbol = 'BTC-USD'
|
|
default_period = '1y'
|
|
default_interval = '1d'
|
|
default_date_range = [0, 1]
|
|
default_fees = 0.1
|
|
default_fixed_fees = 0.
|
|
default_slippage = 5.
|
|
default_yf_options = ['auto_adjust']
|
|
default_exit_n_random = default_entry_n_random = 5
|
|
default_prob_options = ['mimic_strategy']
|
|
default_entry_prob = 0.1
|
|
default_exit_prob = 0.1
|
|
default_entry_patterns = [
|
|
'CDLHAMMER',
|
|
'CDLINVERTEDHAMMER',
|
|
'CDLPIERCING',
|
|
'CDLMORNINGSTAR',
|
|
'CDL3WHITESOLDIERS'
|
|
]
|
|
default_exit_options = []
|
|
default_exit_patterns = [
|
|
'CDLHANGINGMAN',
|
|
'CDLSHOOTINGSTAR',
|
|
'CDLEVENINGSTAR',
|
|
'CDL3BLACKCROWS',
|
|
'CDLDARKCLOUDCOVER'
|
|
]
|
|
default_candle_settings = pd.DataFrame({
|
|
'SettingType': [
|
|
'BodyLong',
|
|
'BodyVeryLong',
|
|
'BodyShort',
|
|
'BodyDoji',
|
|
'ShadowLong',
|
|
'ShadowVeryLong',
|
|
'ShadowShort',
|
|
'ShadowVeryShort',
|
|
'Near',
|
|
'Far',
|
|
'Equal'
|
|
],
|
|
'RangeType': [
|
|
'RealBody',
|
|
'RealBody',
|
|
'RealBody',
|
|
'HighLow',
|
|
'RealBody',
|
|
'RealBody',
|
|
'Shadows',
|
|
'HighLow',
|
|
'HighLow',
|
|
'HighLow',
|
|
'HighLow'
|
|
],
|
|
'AvgPeriod': [
|
|
10,
|
|
10,
|
|
10,
|
|
10,
|
|
0,
|
|
0,
|
|
10,
|
|
10,
|
|
5,
|
|
5,
|
|
5
|
|
],
|
|
'Factor': [
|
|
1.0,
|
|
3.0,
|
|
1.0,
|
|
0.1,
|
|
1.0,
|
|
2.0,
|
|
1.0,
|
|
0.1,
|
|
0.2,
|
|
0.6,
|
|
0.05
|
|
]
|
|
})
|
|
default_entry_dates = []
|
|
default_exit_dates = []
|
|
default_direction = directions[0]
|
|
default_conflict_mode = conflict_modes[0]
|
|
default_sim_options = ['allow_accumulate']
|
|
default_n_random_strat = 50
|
|
default_stats_options = ['incl_open']
|
|
default_layout = dict(
|
|
autosize=True,
|
|
margin=dict(b=40, t=20),
|
|
font=dict(
|
|
color=fontcolor
|
|
),
|
|
plot_bgcolor=bgcolor,
|
|
paper_bgcolor=bgcolor,
|
|
legend=dict(
|
|
font=dict(size=10),
|
|
orientation="h",
|
|
yanchor="bottom",
|
|
y=1.02,
|
|
xanchor="right",
|
|
x=1
|
|
),
|
|
)
|
|
default_subplots = ['orders', 'trade_pnl', 'cum_returns']
|
|
default_plot_type = 'OHLC'
|
|
|
|
app.layout = html.Div(
|
|
children=[
|
|
html.Div(
|
|
className="banner",
|
|
children=[
|
|
html.H6("vectorbt: candlestick patterns"),
|
|
html.Div(
|
|
html.A(
|
|
"View on GitHub",
|
|
href=GITHUB_LINK,
|
|
target="_blank",
|
|
className="button",
|
|
)
|
|
),
|
|
],
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
lg=8, sm=12,
|
|
children=[
|
|
html.Div(
|
|
className="pretty-container",
|
|
children=[
|
|
html.Div(
|
|
className="banner",
|
|
children=[
|
|
html.H6("OHLCV and signals")
|
|
],
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
lg=4, sm=12,
|
|
children=[
|
|
html.Label("Select plot type:"),
|
|
dcc.Dropdown(
|
|
id="plot_type_dropdown",
|
|
options=[{"value": i, "label": i} for i in plot_types],
|
|
value=default_plot_type,
|
|
),
|
|
]
|
|
)
|
|
],
|
|
),
|
|
dcc.Loading(
|
|
id="ohlcv_loading",
|
|
type="default",
|
|
color=loadcolor,
|
|
children=[
|
|
dcc.Graph(
|
|
id="ohlcv_graph",
|
|
figure={
|
|
"layout": default_layout
|
|
}
|
|
)
|
|
],
|
|
),
|
|
html.Small("Hint: Use Box and Lasso Select to filter signals"),
|
|
],
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Div(
|
|
className="pretty-container",
|
|
children=[
|
|
html.Div(
|
|
className="banner",
|
|
children=[
|
|
html.H6("Portfolio")
|
|
],
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
lg=6, sm=12,
|
|
children=[
|
|
html.Label("Select subplots:"),
|
|
dcc.Dropdown(
|
|
id="subplot_dropdown",
|
|
options=[
|
|
{"value": k, "label": v['title']}
|
|
for k, v in Portfolio.subplots.items()
|
|
],
|
|
multi=True,
|
|
value=default_subplots,
|
|
),
|
|
]
|
|
)
|
|
],
|
|
),
|
|
dcc.Loading(
|
|
id="portfolio_loading",
|
|
type="default",
|
|
color=loadcolor,
|
|
children=[
|
|
dcc.Graph(
|
|
id="portfolio_graph",
|
|
figure={
|
|
"layout": default_layout
|
|
}
|
|
)
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
]
|
|
)
|
|
]
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Div(
|
|
className="pretty-container",
|
|
children=[
|
|
html.Div(
|
|
className="banner",
|
|
children=[
|
|
html.H6("Stats")
|
|
],
|
|
),
|
|
dcc.Loading(
|
|
id="stats_loading",
|
|
type="default",
|
|
color=loadcolor,
|
|
children=[
|
|
dash_table.DataTable(
|
|
id="stats_table",
|
|
columns=[
|
|
{
|
|
"name": c,
|
|
"id": c,
|
|
}
|
|
for c in stats_table_columns
|
|
],
|
|
style_data_conditional=[{
|
|
"if": {"column_id": stats_table_columns[1]},
|
|
"fontWeight": "bold",
|
|
"borderLeft": "1px solid dimgrey"
|
|
}, {
|
|
"if": {"column_id": stats_table_columns[2]},
|
|
"fontWeight": "bold",
|
|
}, {
|
|
"if": {"column_id": stats_table_columns[3]},
|
|
"fontWeight": "bold",
|
|
}, {
|
|
"if": {"column_id": stats_table_columns[4]},
|
|
"fontWeight": "bold",
|
|
}, {
|
|
"if": {"state": "selected"},
|
|
"backgroundColor": dark_bgcolor,
|
|
"color": active_color,
|
|
"border": "1px solid " + active_color,
|
|
}, {
|
|
"if": {"state": "active"},
|
|
"backgroundColor": dark_bgcolor,
|
|
"color": active_color,
|
|
"border": "1px solid " + active_color,
|
|
}],
|
|
style_header={
|
|
"border": "none",
|
|
"backgroundColor": bgcolor,
|
|
"fontWeight": "bold",
|
|
"padding": "0px 5px"
|
|
},
|
|
style_data={
|
|
"border": "none",
|
|
"backgroundColor": bgcolor,
|
|
"color": dark_fontcolor,
|
|
"paddingRight": "10px"
|
|
},
|
|
style_table={
|
|
'overflowX': 'scroll',
|
|
},
|
|
style_as_list_view=False,
|
|
editable=False,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
]
|
|
)
|
|
]
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Div(
|
|
className="pretty-container",
|
|
children=[
|
|
html.Div(
|
|
className="banner",
|
|
children=[
|
|
html.H6("Metric stats")
|
|
],
|
|
),
|
|
dcc.Loading(
|
|
id="metric_stats_loading",
|
|
type="default",
|
|
color=loadcolor,
|
|
children=[
|
|
html.Label("Metric:"),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
lg=4, sm=12,
|
|
children=[
|
|
dcc.Dropdown(
|
|
id="metric_dropdown"
|
|
),
|
|
]
|
|
),
|
|
dbc.Col(
|
|
lg=8, sm=12,
|
|
children=[
|
|
dcc.Graph(
|
|
id="metric_graph",
|
|
figure={
|
|
"layout": default_layout
|
|
}
|
|
)
|
|
]
|
|
)
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
]
|
|
)
|
|
]
|
|
),
|
|
|
|
]
|
|
),
|
|
dbc.Col(
|
|
lg=4, sm=12,
|
|
children=[
|
|
html.Div(
|
|
className="pretty-container",
|
|
children=[
|
|
html.Div(
|
|
className="banner",
|
|
children=[
|
|
html.H6("Settings")
|
|
],
|
|
),
|
|
html.Button(
|
|
"Reset",
|
|
id="reset_button"
|
|
),
|
|
html.Details(
|
|
open=True,
|
|
children=[
|
|
html.Summary(
|
|
className="section-title",
|
|
children="Data",
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Yahoo! Finance symbol:"),
|
|
dcc.Input(
|
|
id="symbol_input",
|
|
className="input-control",
|
|
type="text",
|
|
value=default_symbol,
|
|
placeholder="Enter symbol...",
|
|
debounce=True
|
|
),
|
|
]
|
|
),
|
|
dbc.Col()
|
|
],
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Period:"),
|
|
dcc.Dropdown(
|
|
id="period_dropdown",
|
|
options=[{"value": i, "label": i} for i in periods],
|
|
value=default_period,
|
|
),
|
|
]
|
|
),
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Interval:"),
|
|
dcc.Dropdown(
|
|
id="interval_dropdown",
|
|
options=[{"value": i, "label": i} for i in intervals],
|
|
value=default_interval,
|
|
),
|
|
]
|
|
)
|
|
],
|
|
),
|
|
html.Label("Filter period:"),
|
|
dcc.RangeSlider(
|
|
id="date_slider",
|
|
min=0.,
|
|
max=1.,
|
|
value=default_date_range,
|
|
allowCross=False,
|
|
tooltip={
|
|
'placement': 'bottom'
|
|
}
|
|
),
|
|
dcc.Checklist(
|
|
id="yf_checklist",
|
|
options=[{
|
|
"label": "Adjust all OHLC automatically",
|
|
"value": "auto_adjust"
|
|
}, {
|
|
"label": "Use back-adjusted data to mimic true historical prices",
|
|
"value": "back_adjust"
|
|
}],
|
|
value=default_yf_options,
|
|
style={
|
|
"color": dark_fontcolor
|
|
}
|
|
),
|
|
],
|
|
),
|
|
html.Details(
|
|
open=True,
|
|
children=[
|
|
html.Summary(
|
|
className="section-title",
|
|
children="Entry patterns",
|
|
),
|
|
html.Div(
|
|
id='entry_settings',
|
|
children=[
|
|
html.Button(
|
|
"All",
|
|
id="entry_all_button"
|
|
),
|
|
html.Button(
|
|
"Random",
|
|
id="entry_random_button"
|
|
),
|
|
html.Button(
|
|
"Clear",
|
|
id="entry_clear_button"
|
|
),
|
|
html.Label("Number of random patterns:"),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
dcc.Input(
|
|
id="entry_n_random_input",
|
|
className="input-control",
|
|
value=default_entry_n_random,
|
|
placeholder="Enter number...",
|
|
debounce=True,
|
|
type="number",
|
|
min=1, max=len(patterns), step=1
|
|
),
|
|
]
|
|
),
|
|
dbc.Col()
|
|
],
|
|
),
|
|
html.Label("Select patterns:"),
|
|
dcc.Dropdown(
|
|
id="entry_pattern_dropdown",
|
|
options=[{"value": i, "label": i} for i in patterns],
|
|
multi=True,
|
|
value=default_entry_patterns,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
html.Details(
|
|
open=True,
|
|
children=[
|
|
html.Summary(
|
|
className="section-title",
|
|
children="Exit patterns",
|
|
),
|
|
dcc.Checklist(
|
|
id="exit_checklist",
|
|
options=[{
|
|
"label": "Same as entry patterns",
|
|
"value": "same_as_entry"
|
|
}],
|
|
value=default_exit_options,
|
|
style={
|
|
"color": dark_fontcolor
|
|
}
|
|
),
|
|
html.Div(
|
|
id='exit_settings',
|
|
hidden="same_as_entry" in default_exit_options,
|
|
children=[
|
|
html.Button(
|
|
"All",
|
|
id="exit_all_button"
|
|
),
|
|
html.Button(
|
|
"Random",
|
|
id="exit_random_button"
|
|
),
|
|
html.Button(
|
|
"Clear",
|
|
id="exit_clear_button"
|
|
),
|
|
html.Label("Number of random patterns:"),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
dcc.Input(
|
|
id="exit_n_random_input",
|
|
className="input-control",
|
|
value=default_exit_n_random,
|
|
placeholder="Enter number...",
|
|
debounce=True,
|
|
type="number",
|
|
min=1, max=len(patterns), step=1
|
|
),
|
|
]
|
|
),
|
|
dbc.Col()
|
|
],
|
|
),
|
|
html.Label("Select patterns:"),
|
|
dcc.Dropdown(
|
|
id="exit_pattern_dropdown",
|
|
options=[{"value": i, "label": i} for i in patterns],
|
|
multi=True,
|
|
value=default_exit_patterns,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
html.Details(
|
|
children=[
|
|
html.Summary(
|
|
className="section-title",
|
|
children="Candle settings",
|
|
),
|
|
dash_table.DataTable(
|
|
id="candle_settings_table",
|
|
columns=[
|
|
{
|
|
"name": c,
|
|
"id": c,
|
|
"editable": i in (2, 3),
|
|
"type": "numeric" if i in (2, 3) else "any"
|
|
}
|
|
for i, c in enumerate(default_candle_settings.columns)
|
|
],
|
|
data=default_candle_settings.to_dict("records"),
|
|
style_data_conditional=[{
|
|
"if": {"column_editable": True},
|
|
"backgroundColor": dark_bgcolor,
|
|
"border": "1px solid dimgrey"
|
|
}, {
|
|
"if": {"state": "selected"},
|
|
"backgroundColor": dark_bgcolor,
|
|
"color": active_color,
|
|
"border": "1px solid " + active_color,
|
|
}, {
|
|
"if": {"state": "active"},
|
|
"backgroundColor": dark_bgcolor,
|
|
"color": active_color,
|
|
"border": "1px solid " + active_color,
|
|
}],
|
|
style_header={
|
|
"border": "none",
|
|
"backgroundColor": bgcolor,
|
|
"fontWeight": "bold",
|
|
"padding": "0px 5px"
|
|
},
|
|
style_data={
|
|
"border": "none",
|
|
"backgroundColor": bgcolor,
|
|
"color": dark_fontcolor
|
|
},
|
|
style_table={
|
|
'overflowX': 'scroll',
|
|
},
|
|
style_as_list_view=False,
|
|
editable=True,
|
|
),
|
|
],
|
|
),
|
|
html.Details(
|
|
open=False,
|
|
children=[
|
|
html.Summary(
|
|
className="section-title",
|
|
children="Custom pattern",
|
|
),
|
|
html.Div(
|
|
id='custom_settings',
|
|
children=[
|
|
html.Label("Select entry dates:"),
|
|
dcc.Dropdown(
|
|
id="custom_entry_dropdown",
|
|
options=[],
|
|
multi=True,
|
|
value=default_entry_dates,
|
|
),
|
|
html.Label("Select exit dates:"),
|
|
dcc.Dropdown(
|
|
id="custom_exit_dropdown",
|
|
options=[],
|
|
multi=True,
|
|
value=default_exit_dates,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
html.Details(
|
|
open=True,
|
|
children=[
|
|
html.Summary(
|
|
className="section-title",
|
|
children="Simulation settings",
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Fees (in %):"),
|
|
dcc.Input(
|
|
id="fees_input",
|
|
className="input-control",
|
|
type="number",
|
|
value=default_fees,
|
|
placeholder="Enter fees...",
|
|
debounce=True,
|
|
min=0, max=100
|
|
),
|
|
]
|
|
),
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Fixed fees:"),
|
|
dcc.Input(
|
|
id="fixed_fees_input",
|
|
className="input-control",
|
|
type="number",
|
|
value=default_fixed_fees,
|
|
placeholder="Enter fixed fees...",
|
|
debounce=True,
|
|
min=0
|
|
),
|
|
]
|
|
),
|
|
]
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Slippage (in % of H-O):"),
|
|
dcc.Input(
|
|
id="slippage_input",
|
|
className="input-control",
|
|
type="number",
|
|
value=default_slippage,
|
|
placeholder="Enter slippage...",
|
|
debounce=True,
|
|
min=0, max=100
|
|
),
|
|
]
|
|
),
|
|
dbc.Col()
|
|
],
|
|
),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Direction:"),
|
|
dcc.Dropdown(
|
|
id="direction_dropdown",
|
|
options=[{"value": i, "label": i} for i in directions],
|
|
value=default_direction,
|
|
),
|
|
]
|
|
),
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Conflict Mode:"),
|
|
dcc.Dropdown(
|
|
id="conflict_mode_dropdown",
|
|
options=[{"value": i, "label": i} for i in conflict_modes],
|
|
value=default_conflict_mode
|
|
),
|
|
]
|
|
),
|
|
],
|
|
),
|
|
dcc.Checklist(
|
|
id="sim_checklist",
|
|
options=[{
|
|
"label": "Allow signal accumulation",
|
|
"value": "allow_accumulate"
|
|
}],
|
|
value=default_sim_options,
|
|
style={
|
|
"color": dark_fontcolor
|
|
}
|
|
),
|
|
html.Label("Number of random strategies to test against:"),
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
dcc.Input(
|
|
id="n_random_strat_input",
|
|
className="input-control",
|
|
value=default_n_random_strat,
|
|
placeholder="Enter number...",
|
|
debounce=True,
|
|
type="number",
|
|
min=10, max=1000, step=1
|
|
),
|
|
]
|
|
),
|
|
dbc.Col()
|
|
],
|
|
),
|
|
dcc.Checklist(
|
|
id="prob_checklist",
|
|
options=[{
|
|
"label": "Mimic strategy by shuffling",
|
|
"value": "mimic_strategy"
|
|
}],
|
|
value=default_prob_options,
|
|
style={
|
|
"color": dark_fontcolor
|
|
}
|
|
),
|
|
html.Div(
|
|
id='prob_settings',
|
|
hidden="mimic_strategy" in default_prob_options,
|
|
children=[
|
|
dbc.Row(
|
|
children=[
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Entry probability (in %):"),
|
|
dcc.Input(
|
|
id="entry_prob_input",
|
|
className="input-control",
|
|
value=default_entry_prob,
|
|
placeholder="Enter number...",
|
|
debounce=True,
|
|
type="number",
|
|
min=0, max=100
|
|
),
|
|
]
|
|
),
|
|
dbc.Col(
|
|
children=[
|
|
html.Label("Exit probability (in %):"),
|
|
dcc.Input(
|
|
id="exit_prob_input",
|
|
className="input-control",
|
|
value=default_exit_prob,
|
|
placeholder="Enter number...",
|
|
debounce=True,
|
|
type="number",
|
|
min=0, max=100
|
|
),
|
|
]
|
|
),
|
|
],
|
|
),
|
|
]
|
|
),
|
|
dcc.Checklist(
|
|
id="stats_checklist",
|
|
options=[{
|
|
"label": "Include open trades in stats",
|
|
"value": "incl_open"
|
|
}, {
|
|
"label": "Use positions instead of trades in stats",
|
|
"value": "use_positions"
|
|
}],
|
|
value=default_stats_options,
|
|
style={
|
|
"color": dark_fontcolor
|
|
}
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
]
|
|
)
|
|
]
|
|
),
|
|
html.Div(id='data_signal', style={'display': 'none'}),
|
|
html.Div(id='index_signal', style={'display': 'none'}),
|
|
html.Div(id='candle_settings_signal', style={'display': 'none'}),
|
|
html.Div(id='stats_signal', style={'display': 'none'}),
|
|
html.Div(id="window_width", style={'display': 'none'}),
|
|
dcc.Location(id="url")
|
|
],
|
|
)
|
|
|
|
app.clientside_callback(
|
|
"""
|
|
function(href) {
|
|
return window.innerWidth;
|
|
}
|
|
""",
|
|
Output("window_width", "children"),
|
|
[Input("url", "href")],
|
|
)
|
|
|
|
|
|
@cache.memoize()
|
|
def fetch_data(symbol, period, interval, auto_adjust, back_adjust):
|
|
"""Fetch OHLCV data from Yahoo! Finance."""
|
|
return yf.Ticker(symbol).history(
|
|
period=period,
|
|
interval=interval,
|
|
actions=False,
|
|
auto_adjust=auto_adjust,
|
|
back_adjust=back_adjust
|
|
)
|
|
|
|
|
|
@app.callback(
|
|
[Output('data_signal', 'children'),
|
|
Output('index_signal', 'children')],
|
|
[Input('symbol_input', 'value'),
|
|
Input('period_dropdown', 'value'),
|
|
Input('interval_dropdown', 'value'),
|
|
Input("yf_checklist", "value")]
|
|
)
|
|
def update_data(symbol, period, interval, yf_options):
|
|
"""Store data into a hidden DIV to avoid repeatedly calling Yahoo's API."""
|
|
auto_adjust = 'auto_adjust' in yf_options
|
|
back_adjust = 'back_adjust' in yf_options
|
|
df = fetch_data(symbol, period, interval, auto_adjust, back_adjust)
|
|
return df.to_json(date_format='iso', orient='split'), df.index.tolist()
|
|
|
|
|
|
@app.callback(
|
|
[Output('date_slider', 'min'),
|
|
Output('date_slider', 'max'),
|
|
Output('date_slider', 'value')],
|
|
[Input('index_signal', 'children')]
|
|
)
|
|
def update_date_slider(date_list):
|
|
"""Once index (dates) has changed, reset the date slider."""
|
|
return 0, len(date_list) - 1, [0, len(date_list) - 1]
|
|
|
|
|
|
@app.callback(
|
|
[Output('custom_entry_dropdown', 'options'),
|
|
Output('custom_exit_dropdown', 'options')],
|
|
[Input('index_signal', 'children'),
|
|
Input('date_slider', 'value')]
|
|
)
|
|
def update_custom_options(date_list, date_range):
|
|
"""Once dates have changed, update entry/exit dates in custom pattern section.
|
|
|
|
If selected dates cannot be found in new dates, they will be automatically removed."""
|
|
filtered_dates = np.asarray(date_list)[date_range[0]:date_range[1] + 1].tolist()
|
|
custom_options = [{"value": i, "label": i} for i in filtered_dates]
|
|
return custom_options, custom_options
|
|
|
|
|
|
@app.callback(
|
|
Output('entry_pattern_dropdown', 'value'),
|
|
[Input("entry_all_button", "n_clicks"),
|
|
Input("entry_random_button", "n_clicks"),
|
|
Input("entry_clear_button", "n_clicks"),
|
|
Input("reset_button", "n_clicks")],
|
|
[State("entry_n_random_input", "value")]
|
|
)
|
|
def select_entry_patterns(_1, _2, _3, _4, n_random):
|
|
"""Select all/random entry patterns or clear."""
|
|
ctx = dash.callback_context
|
|
if ctx.triggered:
|
|
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
|
if button_id == 'entry_all_button':
|
|
return patterns
|
|
elif button_id == 'entry_random_button':
|
|
return random.sample(patterns, n_random)
|
|
elif button_id == 'entry_clear_button':
|
|
return []
|
|
elif button_id == 'reset_button':
|
|
return default_entry_patterns
|
|
return dash.no_update
|
|
|
|
|
|
@app.callback(
|
|
[Output("exit_settings", "hidden"),
|
|
Output("exit_n_random_input", "value"),
|
|
Output('exit_pattern_dropdown', 'value')],
|
|
[Input("exit_checklist", "value"),
|
|
Input("exit_all_button", "n_clicks"),
|
|
Input("exit_random_button", "n_clicks"),
|
|
Input("exit_clear_button", "n_clicks"),
|
|
Input("reset_button", "n_clicks"),
|
|
Input("entry_n_random_input", "value"),
|
|
Input('entry_pattern_dropdown', 'value')],
|
|
[State("exit_n_random_input", "value")]
|
|
)
|
|
def select_exit_patterns(exit_options, _1, _2, _3, _4, entry_n_random, entry_patterns, exit_n_random):
|
|
"""Select all/random exit patterns, clear, or configure the same way as entry patterns."""
|
|
ctx = dash.callback_context
|
|
same_as_entry = 'same_as_entry' in exit_options
|
|
if ctx.triggered:
|
|
control_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
|
if control_id == 'exit_checklist':
|
|
return same_as_entry, entry_n_random, entry_patterns
|
|
elif control_id == 'exit_all_button':
|
|
return same_as_entry, exit_n_random, patterns
|
|
elif control_id == 'exit_random_button':
|
|
return same_as_entry, exit_n_random, random.sample(patterns, exit_n_random)
|
|
elif control_id == 'exit_clear_button':
|
|
return same_as_entry, exit_n_random, []
|
|
elif control_id == 'reset_button':
|
|
default_same_as_entry = 'same_as_entry' in default_exit_options
|
|
return default_same_as_entry, default_exit_n_random, default_exit_patterns
|
|
elif control_id in ('entry_n_random_input', 'entry_pattern_dropdown'):
|
|
if same_as_entry:
|
|
return same_as_entry, entry_n_random, entry_patterns
|
|
return dash.no_update
|
|
|
|
|
|
@app.callback(
|
|
Output('candle_settings_signal', 'children'),
|
|
[Input('candle_settings_table', 'data')]
|
|
)
|
|
def set_candle_settings(data):
|
|
"""Update candle settings in TA-Lib."""
|
|
for d in data:
|
|
AvgPeriod = d["AvgPeriod"]
|
|
if isinstance(AvgPeriod, float) and float.is_integer(AvgPeriod):
|
|
AvgPeriod = int(AvgPeriod)
|
|
Factor = float(d["Factor"])
|
|
_ta_set_candle_settings(
|
|
getattr(CandleSettingType, d["SettingType"]),
|
|
getattr(RangeType, d["RangeType"]),
|
|
AvgPeriod,
|
|
Factor
|
|
)
|
|
|
|
|
|
@app.callback(
|
|
[Output('ohlcv_graph', 'figure'),
|
|
Output("prob_settings", "hidden"),
|
|
Output("entry_prob_input", "value"),
|
|
Output("exit_prob_input", "value")],
|
|
[Input('window_width', 'children'),
|
|
Input('plot_type_dropdown', 'value'),
|
|
Input('data_signal', 'children'),
|
|
Input('date_slider', 'value'),
|
|
Input('entry_pattern_dropdown', 'value'),
|
|
Input('exit_pattern_dropdown', 'value'),
|
|
Input('candle_settings_signal', 'children'),
|
|
Input('custom_entry_dropdown', 'value'),
|
|
Input('custom_exit_dropdown', 'value'),
|
|
Input('prob_checklist', 'value'),
|
|
Input("reset_button", "n_clicks")],
|
|
[State("entry_prob_input", "value"),
|
|
State("exit_prob_input", "value")]
|
|
)
|
|
def update_ohlcv(window_width, plot_type, df_json, date_range, entry_patterns, exit_patterns, _1,
|
|
entry_dates, exit_dates, prob_options, _2, entry_prob, exit_prob):
|
|
"""Update OHLCV graph.
|
|
|
|
Also update probability settings, as they also depend upon conversion of patterns into signals."""
|
|
df = pd.read_json(df_json, orient='split')
|
|
|
|
# Filter by date
|
|
df = df.iloc[date_range[0]:date_range[1] + 1]
|
|
|
|
# Run pattern recognition indicators and combine results
|
|
talib_inputs = {
|
|
'open': df['Open'].values,
|
|
'high': df['High'].values,
|
|
'low': df['Low'].values,
|
|
'close': df['Close'].values,
|
|
'volume': df['Volume'].values
|
|
}
|
|
entry_patterns += ['CUSTOM']
|
|
exit_patterns += ['CUSTOM']
|
|
all_patterns = list(set(entry_patterns + exit_patterns))
|
|
signal_df = pd.DataFrame.vbt.empty(
|
|
(len(df.index), len(all_patterns)),
|
|
fill_value=0.,
|
|
index=df.index,
|
|
columns=all_patterns
|
|
)
|
|
for pattern in all_patterns:
|
|
if pattern != 'CUSTOM':
|
|
signal_df[pattern] = abstract.Function(pattern)(talib_inputs)
|
|
signal_df['CUSTOM'].loc[entry_dates] += 100.
|
|
signal_df['CUSTOM'].loc[exit_dates] += -100.
|
|
entry_signal_df = signal_df[entry_patterns]
|
|
exit_signal_df = signal_df[exit_patterns]
|
|
|
|
# Entry patterns
|
|
entry_df = entry_signal_df[(entry_signal_df > 0).any(axis=1)]
|
|
entry_patterns = []
|
|
for row_i, row in entry_df.iterrows():
|
|
entry_patterns.append('<br>'.join(row.index[row != 0]))
|
|
entry_patterns = np.asarray(entry_patterns)
|
|
|
|
# Exit patterns
|
|
exit_df = exit_signal_df[(exit_signal_df < 0).any(axis=1)]
|
|
exit_patterns = []
|
|
for row_i, row in exit_df.iterrows():
|
|
exit_patterns.append('<br>'.join(row.index[row != 0]))
|
|
exit_patterns = np.asarray(exit_patterns)
|
|
|
|
# Prepare scatter data
|
|
highest_high = df['High'].max()
|
|
lowest_low = df['Low'].min()
|
|
distance = (highest_high - lowest_low) / 5
|
|
entry_y = df.loc[entry_df.index, 'Low'] - distance
|
|
entry_y.index = pd.to_datetime(entry_y.index)
|
|
exit_y = df.loc[exit_df.index, 'High'] + distance
|
|
exit_y.index = pd.to_datetime(exit_y.index)
|
|
|
|
# Prepare signals
|
|
entry_signals = pd.Series.vbt.empty_like(entry_y, True)
|
|
exit_signals = pd.Series.vbt.empty_like(exit_y, True)
|
|
|
|
# Build graph
|
|
height = int(9 / 21 * 2 / 3 * window_width)
|
|
fig = df.vbt.ohlcv.plot(
|
|
plot_type=plot_type,
|
|
**merge_dicts(
|
|
default_layout,
|
|
dict(
|
|
width=None,
|
|
height=max(500, height),
|
|
margin=dict(r=40),
|
|
hovermode="closest",
|
|
xaxis2=dict(
|
|
title='Date'
|
|
),
|
|
yaxis2=dict(
|
|
title='Volume'
|
|
),
|
|
yaxis=dict(
|
|
title='Price',
|
|
)
|
|
)
|
|
)
|
|
)
|
|
entry_signals.vbt.signals.plot_as_entry_markers(
|
|
y=entry_y,
|
|
trace_kwargs=dict(
|
|
customdata=entry_patterns[:, None],
|
|
hovertemplate='%{x}<br>%{customdata[0]}',
|
|
name='Bullish signal'
|
|
),
|
|
add_trace_kwargs=dict(row=1, col=1),
|
|
fig=fig
|
|
)
|
|
exit_signals.vbt.signals.plot_as_exit_markers(
|
|
y=exit_y,
|
|
trace_kwargs=dict(
|
|
customdata=exit_patterns[:, None],
|
|
hovertemplate='%{x}<br>%{customdata[0]}',
|
|
name='Bearish signal'
|
|
),
|
|
add_trace_kwargs=dict(row=1, col=1),
|
|
fig=fig
|
|
)
|
|
fig.update_xaxes(gridcolor=gridcolor)
|
|
fig.update_yaxes(gridcolor=gridcolor, zerolinecolor=gridcolor)
|
|
figure = dict(data=fig.data, layout=fig.layout)
|
|
|
|
mimic_strategy = 'mimic_strategy' in prob_options
|
|
ctx = dash.callback_context
|
|
if ctx.triggered:
|
|
control_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
|
if control_id == 'reset_button':
|
|
mimic_strategy = 'mimic_strategy' in default_prob_options
|
|
entry_prob = default_entry_prob
|
|
exit_prob = default_exit_prob
|
|
if mimic_strategy:
|
|
entry_prob = np.round(len(entry_df.index) / len(df.index) * 100, 4)
|
|
exit_prob = np.round(len(exit_df.index) / len(df.index) * 100, 4)
|
|
return figure, mimic_strategy, entry_prob, exit_prob
|
|
|
|
|
|
def simulate_portfolio(df, interval, date_range, selected_data, entry_patterns, exit_patterns,
|
|
entry_dates, exit_dates, fees, fixed_fees, slippage, direction, conflict_mode,
|
|
sim_options, n_random_strat, prob_options, entry_prob, exit_prob):
|
|
"""Simulate portfolio of the main strategy, buy & hold strategy, and a bunch of random strategies."""
|
|
# Filter by date
|
|
df = df.iloc[date_range[0]:date_range[1] + 1]
|
|
|
|
# Run pattern recognition indicators and combine results
|
|
talib_inputs = {
|
|
'open': df['Open'].values,
|
|
'high': df['High'].values,
|
|
'low': df['Low'].values,
|
|
'close': df['Close'].values,
|
|
'volume': df['Volume'].values
|
|
}
|
|
entry_patterns += ['CUSTOM']
|
|
exit_patterns += ['CUSTOM']
|
|
all_patterns = list(set(entry_patterns + exit_patterns))
|
|
entry_i = [all_patterns.index(p) for p in entry_patterns]
|
|
exit_i = [all_patterns.index(p) for p in exit_patterns]
|
|
signals = np.full((len(df.index), len(all_patterns)), 0., dtype=np.float_)
|
|
for i, pattern in enumerate(all_patterns):
|
|
if pattern != 'CUSTOM':
|
|
signals[:, i] = abstract.Function(pattern)(talib_inputs)
|
|
signals[np.flatnonzero(df.index.isin(entry_dates)), all_patterns.index('CUSTOM')] += 100.
|
|
signals[np.flatnonzero(df.index.isin(exit_dates)), all_patterns.index('CUSTOM')] += -100.
|
|
signals /= 100. # TA-Lib functions have output in increments of 100
|
|
|
|
# Filter signals
|
|
if selected_data is not None:
|
|
new_signals = np.full_like(signals, 0.)
|
|
for point in selected_data['points']:
|
|
if 'customdata' in point:
|
|
point_patterns = point['customdata'][0].split('<br>')
|
|
pi = df.index.get_loc(point['x'])
|
|
for p in point_patterns:
|
|
pc = all_patterns.index(p)
|
|
new_signals[pi, pc] = signals[pi, pc]
|
|
signals = new_signals
|
|
|
|
# Generate size for main
|
|
def _generate_size(signals):
|
|
entry_signals = signals[:, entry_i]
|
|
exit_signals = signals[:, exit_i]
|
|
return np.where(entry_signals > 0, entry_signals, 0).sum(axis=1) + \
|
|
np.where(exit_signals < 0, exit_signals, 0).sum(axis=1)
|
|
|
|
main_size = np.empty((len(df.index),), dtype=np.float_)
|
|
main_size[0] = 0 # avoid looking into future
|
|
main_size[1:] = _generate_size(signals)[:-1]
|
|
|
|
# Generate size for buy & hold
|
|
hold_size = np.full_like(main_size, 0.)
|
|
hold_size[0] = np.inf
|
|
|
|
# Generate size for random
|
|
def _shuffle_along_axis(a, axis):
|
|
idx = np.random.rand(*a.shape).argsort(axis=axis)
|
|
return np.take_along_axis(a, idx, axis=axis)
|
|
|
|
rand_size = np.empty((len(df.index), n_random_strat), dtype=np.float_)
|
|
rand_size[0] = 0 # avoid looking into future
|
|
if 'mimic_strategy' in prob_options:
|
|
for i in range(n_random_strat):
|
|
rand_signals = _shuffle_along_axis(signals, 0)
|
|
rand_size[1:, i] = _generate_size(rand_signals)[:-1]
|
|
else:
|
|
entry_signals = pd.DataFrame.vbt.signals.generate_random(
|
|
(rand_size.shape[0] - 1, rand_size.shape[1]), prob=entry_prob / 100).values
|
|
exit_signals = pd.DataFrame.vbt.signals.generate_random(
|
|
(rand_size.shape[0] - 1, rand_size.shape[1]), prob=exit_prob / 100).values
|
|
rand_size[1:, :] = np.where(entry_signals, 1., 0.) - np.where(exit_signals, 1., 0.)
|
|
|
|
# Simulate portfolio
|
|
def _simulate_portfolio(size, init_cash='autoalign'):
|
|
return Portfolio.from_signals(
|
|
close=df['Close'],
|
|
entries=size > 0,
|
|
exits=size < 0,
|
|
price=df['Open'],
|
|
size=np.abs(size),
|
|
direction=direction,
|
|
upon_dir_conflict=conflict_mode,
|
|
accumulate='allow_accumulate' in sim_options,
|
|
init_cash=init_cash,
|
|
fees=float(fees) / 100,
|
|
fixed_fees=float(fixed_fees),
|
|
slippage=(float(slippage) / 100) * (df['High'] / df['Open'] - 1),
|
|
freq=interval
|
|
)
|
|
|
|
# Align initial cash across main and random strategies
|
|
aligned_portfolio = _simulate_portfolio(np.hstack((main_size[:, None], rand_size)))
|
|
# Fixate initial cash for indexing
|
|
aligned_portfolio = aligned_portfolio.replace(
|
|
init_cash=aligned_portfolio.init_cash
|
|
)
|
|
# Separate portfolios
|
|
main_portfolio = aligned_portfolio.iloc[0]
|
|
rand_portfolio = aligned_portfolio.iloc[1:]
|
|
|
|
# Simulate buy & hold portfolio
|
|
hold_portfolio = _simulate_portfolio(hold_size, init_cash=main_portfolio.init_cash)
|
|
|
|
return main_portfolio, hold_portfolio, rand_portfolio
|
|
|
|
|
|
@app.callback(
|
|
[Output('portfolio_graph', 'figure'),
|
|
Output('stats_table', 'data'),
|
|
Output('stats_signal', 'children'),
|
|
Output('metric_dropdown', 'options'),
|
|
Output('metric_dropdown', 'value')],
|
|
[Input('window_width', 'children'),
|
|
Input('subplot_dropdown', 'value'),
|
|
Input('data_signal', 'children'),
|
|
Input('symbol_input', 'value'),
|
|
Input('interval_dropdown', 'value'),
|
|
Input('date_slider', 'value'),
|
|
Input('ohlcv_graph', 'selectedData'),
|
|
Input('entry_pattern_dropdown', 'value'),
|
|
Input('exit_pattern_dropdown', 'value'),
|
|
Input('candle_settings_signal', 'children'),
|
|
Input('custom_entry_dropdown', 'value'),
|
|
Input('custom_exit_dropdown', 'value'),
|
|
Input('fees_input', 'value'),
|
|
Input('fixed_fees_input', 'value'),
|
|
Input('slippage_input', 'value'),
|
|
Input('direction_dropdown', 'value'),
|
|
Input('conflict_mode_dropdown', 'value'),
|
|
Input('sim_checklist', 'value'),
|
|
Input('n_random_strat_input', 'value'),
|
|
Input('prob_checklist', 'value'),
|
|
Input("entry_prob_input", "value"),
|
|
Input("exit_prob_input", "value"),
|
|
Input('stats_checklist', 'value'),
|
|
Input("reset_button", "n_clicks")],
|
|
[State('metric_dropdown', 'value')]
|
|
)
|
|
def update_stats(window_width, subplots, df_json, symbol, interval, date_range, selected_data,
|
|
entry_patterns, exit_patterns, _1, entry_dates, exit_dates, fees, fixed_fees,
|
|
slippage, direction, conflict_mode, sim_options, n_random_strat, prob_options,
|
|
entry_prob, exit_prob, stats_options, _2, curr_metric):
|
|
"""Final stage where we calculate key performance metrics and compare strategies."""
|
|
df = pd.read_json(df_json, orient='split')
|
|
|
|
# Simulate portfolio
|
|
main_portfolio, hold_portfolio, rand_portfolio = simulate_portfolio(
|
|
df, interval, date_range, selected_data, entry_patterns, exit_patterns,
|
|
entry_dates, exit_dates, fees, fixed_fees, slippage, direction, conflict_mode,
|
|
sim_options, n_random_strat, prob_options, entry_prob, exit_prob)
|
|
|
|
subplot_settings = dict()
|
|
if 'cum_returns' in subplots:
|
|
subplot_settings['cum_returns'] = dict(
|
|
benchmark_kwargs=dict(
|
|
trace_kwargs=dict(
|
|
line=dict(
|
|
color=adjust_opacity(color_schema['yellow'], 0.5)
|
|
),
|
|
name=symbol
|
|
)
|
|
)
|
|
)
|
|
height = int(6 / 21 * 2 / 3 * window_width)
|
|
fig = main_portfolio.plot(
|
|
subplots=subplots,
|
|
subplot_settings=subplot_settings,
|
|
**merge_dicts(
|
|
default_layout,
|
|
dict(
|
|
width=None,
|
|
height=len(subplots) * max(300, height) if len(subplots) > 1 else max(350, height)
|
|
)
|
|
)
|
|
)
|
|
fig.update_traces(xaxis="x" if len(subplots) == 1 else "x" + str(len(subplots)))
|
|
fig.update_xaxes(
|
|
gridcolor=gridcolor
|
|
)
|
|
fig.update_yaxes(
|
|
gridcolor=gridcolor,
|
|
zerolinecolor=gridcolor
|
|
)
|
|
|
|
def _chop_microseconds(delta):
|
|
return delta - pd.Timedelta(microseconds=delta.microseconds, nanoseconds=delta.nanoseconds)
|
|
|
|
def _metric_to_str(x):
|
|
if isinstance(x, float):
|
|
return '%.2f' % x
|
|
if isinstance(x, pd.Timedelta):
|
|
return str(_chop_microseconds(x))
|
|
return str(x)
|
|
|
|
incl_open = 'incl_open' in stats_options
|
|
use_positions = 'use_positions' in stats_options
|
|
main_stats = main_portfolio.stats(settings=dict(incl_open=incl_open, use_positions=use_positions))
|
|
hold_stats = hold_portfolio.stats(settings=dict(incl_open=True, use_positions=use_positions))
|
|
rand_stats = rand_portfolio.stats(settings=dict(incl_open=incl_open, use_positions=use_positions), agg_func=None)
|
|
rand_stats_median = rand_stats.iloc[:, 3:].median(axis=0)
|
|
rand_stats_mean = rand_stats.iloc[:, 3:].mean(axis=0)
|
|
rand_stats_std = rand_stats.iloc[:, 3:].std(axis=0, ddof=0)
|
|
stats_mean_diff = main_stats.iloc[3:] - rand_stats_mean
|
|
|
|
def _to_float(x):
|
|
if pd.isnull(x):
|
|
return np.nan
|
|
if isinstance(x, float):
|
|
if np.allclose(x, 0):
|
|
return 0.
|
|
if isinstance(x, pd.Timedelta):
|
|
return float(x.total_seconds())
|
|
return float(x)
|
|
|
|
z = stats_mean_diff.apply(_to_float) / rand_stats_std.apply(_to_float)
|
|
|
|
table_data = pd.DataFrame(columns=stats_table_columns)
|
|
table_data.iloc[:, 0] = main_stats.index
|
|
table_data.iloc[:, 1] = hold_stats.apply(_metric_to_str).values
|
|
table_data.iloc[:3, 2] = table_data.iloc[:3, 1]
|
|
table_data.iloc[3:, 2] = rand_stats_median.apply(_metric_to_str).values
|
|
table_data.iloc[:, 3] = main_stats.apply(_metric_to_str).values
|
|
table_data.iloc[3:, 4] = z.apply(_metric_to_str).values
|
|
|
|
metric = curr_metric
|
|
ctx = dash.callback_context
|
|
if ctx.triggered:
|
|
control_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
|
if control_id == 'reset_button':
|
|
metric = default_metric
|
|
if metric is None:
|
|
metric = default_metric
|
|
return dict(data=fig.data, layout=fig.layout), \
|
|
table_data.to_dict("records"), \
|
|
json.dumps({
|
|
'main': {m: [_to_float(main_stats[m])] for m in main_stats.index[3:]},
|
|
'hold': {m: [_to_float(hold_stats[m])] for m in main_stats.index[3:]},
|
|
'rand': {m: rand_stats[m].apply(_to_float).values.tolist() for m in main_stats.index[3:]}
|
|
}), \
|
|
[{"value": i, "label": i} for i in main_stats.index[3:]], \
|
|
metric
|
|
|
|
|
|
@app.callback(
|
|
Output('metric_graph', 'figure'),
|
|
[Input('window_width', 'children'),
|
|
Input('stats_signal', 'children'),
|
|
Input('metric_dropdown', 'value')]
|
|
)
|
|
def update_metric_stats(window_width, stats_json, metric):
|
|
"""Once a new metric has been selected, plot its distribution."""
|
|
stats_dict = json.loads(stats_json)
|
|
height = int(9 / 21 * 2 / 3 * 2 / 3 * window_width)
|
|
return dict(
|
|
data=[
|
|
go.Box(
|
|
x=stats_dict['rand'][metric],
|
|
quartilemethod="linear",
|
|
jitter=0.3,
|
|
pointpos=1.8,
|
|
boxpoints='all',
|
|
boxmean='sd',
|
|
hoveron="points",
|
|
hovertemplate='%{x}<br>Random',
|
|
name='',
|
|
marker=dict(
|
|
color=color_schema['blue'],
|
|
opacity=0.5,
|
|
size=8,
|
|
),
|
|
),
|
|
go.Box(
|
|
x=stats_dict['hold'][metric],
|
|
quartilemethod="linear",
|
|
boxpoints="all",
|
|
jitter=0,
|
|
pointpos=1.8,
|
|
hoveron="points",
|
|
hovertemplate='%{x}<br>Buy & Hold',
|
|
fillcolor="rgba(0,0,0,0)",
|
|
line=dict(color="rgba(0,0,0,0)"),
|
|
name='',
|
|
marker=dict(
|
|
color=color_schema['orange'],
|
|
size=8,
|
|
),
|
|
),
|
|
go.Box(
|
|
x=stats_dict['main'][metric],
|
|
quartilemethod="linear",
|
|
boxpoints="all",
|
|
jitter=0,
|
|
pointpos=1.8,
|
|
hoveron="points",
|
|
hovertemplate='%{x}<br>Strategy',
|
|
fillcolor="rgba(0,0,0,0)",
|
|
line=dict(color="rgba(0,0,0,0)"),
|
|
name='',
|
|
marker=dict(
|
|
color=color_schema['green'],
|
|
size=8,
|
|
),
|
|
),
|
|
],
|
|
layout=merge_dicts(
|
|
default_layout,
|
|
dict(
|
|
height=max(350, height),
|
|
showlegend=False,
|
|
margin=dict(l=60, r=20, t=40, b=20),
|
|
hovermode="closest",
|
|
xaxis=dict(
|
|
gridcolor=gridcolor,
|
|
title=metric,
|
|
side='top'
|
|
),
|
|
yaxis=dict(
|
|
gridcolor=gridcolor
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
@app.callback(
|
|
[Output('symbol_input', 'value'),
|
|
Output('period_dropdown', 'value'),
|
|
Output('interval_dropdown', 'value'),
|
|
Output("yf_checklist", "value"),
|
|
Output("entry_n_random_input", "value"),
|
|
Output("exit_checklist", "value"),
|
|
Output('candle_settings_table', 'data'),
|
|
Output('custom_entry_dropdown', 'value'),
|
|
Output('custom_exit_dropdown', 'value'),
|
|
Output('fees_input', 'value'),
|
|
Output('fixed_fees_input', 'value'),
|
|
Output('slippage_input', 'value'),
|
|
Output('conflict_mode_dropdown', 'value'),
|
|
Output('direction_dropdown', 'value'),
|
|
Output('sim_checklist', 'value'),
|
|
Output('n_random_strat_input', 'value'),
|
|
Output("prob_checklist", "value"),
|
|
Output('stats_checklist', 'value')],
|
|
[Input("reset_button", "n_clicks")],
|
|
prevent_initial_call=True
|
|
)
|
|
def reset_settings(_):
|
|
"""Reset most settings. Other settings are reset in their callbacks."""
|
|
return default_symbol, \
|
|
default_period, \
|
|
default_interval, \
|
|
default_yf_options, \
|
|
default_entry_n_random, \
|
|
default_exit_options, \
|
|
default_candle_settings.to_dict("records"), \
|
|
default_entry_dates, \
|
|
default_exit_dates, \
|
|
default_fees, \
|
|
default_fixed_fees, \
|
|
default_slippage, \
|
|
default_conflict_mode, \
|
|
default_direction, \
|
|
default_sim_options, \
|
|
default_n_random_strat, \
|
|
default_prob_options, \
|
|
default_stats_options
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app.run_server(host=HOST, port=PORT, debug=DEBUG)
|