Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
polakowo
GitHub Repository: polakowo/vectorbt
Path: blob/master/apps/candlestick-patterns/app.py
1151 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright (c) 2021 Oleg Polakow. All rights reserved.
4
# This code is licensed under Apache 2.0 with Commons Clause license (see LICENSE.md for details)
5
6
# Run this app with `python app.py` and
7
# visit http://127.0.0.1:8050/ in your web browser.
8
9
import json
10
import os
11
import random
12
from io import StringIO
13
14
import dash_bootstrap_components as dbc
15
import numpy as np
16
import pandas as pd
17
import plotly.graph_objects as go
18
import talib
19
import yfinance as yf
20
from dash import Dash, dcc, html, dash_table, Input, Output, State, no_update, callback_context
21
from flask_caching import Cache
22
from talib import abstract
23
from talib._ta_lib import CandleSettingType, RangeType, _ta_set_candle_settings
24
25
from vectorbt import settings
26
from vectorbt.portfolio.base import Portfolio
27
from vectorbt.portfolio.enums import Direction, DirectionConflictMode
28
from vectorbt.utils.colors import adjust_opacity
29
from vectorbt.utils.config import merge_dicts
30
31
USE_CACHING = (
32
os.environ.get(
33
"USE_CACHING",
34
"True",
35
)
36
== "True"
37
)
38
HOST = os.environ.get(
39
"HOST",
40
"127.0.0.1",
41
)
42
PORT = int(
43
os.environ.get(
44
"PORT",
45
8050,
46
)
47
)
48
DEBUG = (
49
os.environ.get(
50
"DEBUG",
51
"True",
52
)
53
== "True"
54
)
55
GITHUB_LINK = os.environ.get(
56
"GITHUB_LINK",
57
"https://github.com/polakowo/vectorbt/tree/master/apps/candlestick-patterns",
58
)
59
60
app = Dash(
61
__name__,
62
title="VectorBT: Candlestick Patterns",
63
meta_tags=[
64
{
65
"name": "viewport",
66
"content": "width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no",
67
}
68
],
69
external_stylesheets=[dbc.themes.BOOTSTRAP],
70
)
71
CACHE_CONFIG = {
72
"CACHE_TYPE": "filesystem" if USE_CACHING else "null",
73
"CACHE_DIR": "data",
74
"CACHE_DEFAULT_TIMEOUT": 0,
75
"CACHE_THRESHOLD": 50,
76
}
77
cache = Cache()
78
cache.init_app(app.server, config=CACHE_CONFIG)
79
80
# Settings
81
periods = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"]
82
intervals = ["1m", "2m", "5m", "15m", "30m", "60m", "90m", "1d", "5d", "1wk", "1mo", "3mo"]
83
patterns = talib.get_function_groups()["Pattern Recognition"]
84
stats_table_columns = ["Metric", "Buy & Hold", "Random (Median)", "Strategy", "Z-Score"]
85
directions = Direction._fields
86
conflict_modes = DirectionConflictMode._fields
87
plot_types = ["OHLC", "Candlestick"]
88
89
# Colors
90
color_schema = settings["plotting"]["color_schema"]
91
bgcolor = "#ffffff"
92
dark_bgcolor = "#f6f7fb"
93
fontcolor = "#0f172a"
94
dark_fontcolor = "#475569"
95
gridcolor = "#e5e7eb"
96
active_color = "#2563EB"
97
loadcolor = "#2563EB"
98
bordercolor = "#e5e7eb"
99
bordercolor_strong = "#d1d5db"
100
101
# Defaults
102
data_path = "data/data.h5"
103
default_metric = "Total Return [%]"
104
default_symbol = "BTC-USD"
105
default_period = "1y"
106
default_interval = "1d"
107
default_date_range = [0, 1]
108
default_fees = 0.1
109
default_fixed_fees = 0.0
110
default_slippage = 5.0
111
default_yf_options = ["auto_adjust"]
112
default_exit_n_random = default_entry_n_random = 5
113
default_prob_options = ["mimic_strategy"]
114
default_entry_prob = 0.1
115
default_exit_prob = 0.1
116
default_entry_patterns = [
117
"CDLHAMMER",
118
"CDLINVERTEDHAMMER",
119
"CDLPIERCING",
120
"CDLMORNINGSTAR",
121
"CDL3WHITESOLDIERS",
122
]
123
default_exit_options = []
124
default_exit_patterns = [
125
"CDLHANGINGMAN",
126
"CDLSHOOTINGSTAR",
127
"CDLEVENINGSTAR",
128
"CDL3BLACKCROWS",
129
"CDLDARKCLOUDCOVER",
130
]
131
default_candle_settings = pd.DataFrame(
132
{
133
"SettingType": [
134
"BodyLong",
135
"BodyVeryLong",
136
"BodyShort",
137
"BodyDoji",
138
"ShadowLong",
139
"ShadowVeryLong",
140
"ShadowShort",
141
"ShadowVeryShort",
142
"Near",
143
"Far",
144
"Equal",
145
],
146
"RangeType": [
147
"RealBody",
148
"RealBody",
149
"RealBody",
150
"HighLow",
151
"RealBody",
152
"RealBody",
153
"Shadows",
154
"HighLow",
155
"HighLow",
156
"HighLow",
157
"HighLow",
158
],
159
"AvgPeriod": [10, 10, 10, 10, 0, 0, 10, 10, 5, 5, 5],
160
"Factor": [1.0, 3.0, 1.0, 0.1, 1.0, 2.0, 1.0, 0.1, 0.2, 0.6, 0.05],
161
}
162
)
163
default_entry_dates = []
164
default_exit_dates = []
165
default_direction = directions[0]
166
default_conflict_mode = conflict_modes[0]
167
default_sim_options = ["allow_accumulate"]
168
default_n_random_strat = 50
169
default_stats_options = ["incl_open"]
170
default_layout = dict(
171
template="plotly_white",
172
autosize=True,
173
margin=dict(b=40, t=20),
174
font=dict(color=fontcolor),
175
plot_bgcolor=bgcolor,
176
paper_bgcolor=bgcolor,
177
legend=dict(font=dict(size=10), orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
178
)
179
default_subplots = ["orders", "trade_pnl", "cum_returns"]
180
default_plot_type = "OHLC"
181
182
183
def section_summary(text: str):
184
return html.Summary(className="section-title", children=text)
185
186
187
app.layout = html.Div(
188
className="app-shell",
189
children=[
190
html.Div(
191
className="app-banner",
192
children=[
193
html.H6("VectorBT: Candlestick Patterns"),
194
html.A(
195
"View on GitHub",
196
href=GITHUB_LINK,
197
target="_blank",
198
className="btn-link", # stays special and NOT overridden by generic button rules
199
),
200
],
201
),
202
dbc.Row(
203
className="g-3", # bootstrap gutter
204
children=[
205
dbc.Col(
206
lg=8,
207
sm=12,
208
children=[
209
html.Div(
210
className="pretty-container",
211
children=[
212
html.Div(
213
className="card-header",
214
children=[html.H6("OHLCV and signals")],
215
),
216
dbc.Row(
217
className="g-2",
218
children=[
219
dbc.Col(
220
lg=4,
221
sm=12,
222
children=[
223
html.Label("Select plot type:"),
224
dcc.Dropdown(
225
id="plot_type_dropdown",
226
options=[{"value": i, "label": i} for i in plot_types],
227
value=default_plot_type,
228
className="dropdown-control",
229
),
230
],
231
)
232
],
233
),
234
dcc.Loading(
235
id="ohlcv_loading",
236
type="default",
237
color=loadcolor,
238
children=[dcc.Graph(id="ohlcv_graph", figure={"layout": default_layout})],
239
),
240
html.Small("Hint: Use Box and Lasso Select to filter signals"),
241
],
242
),
243
dbc.Row(
244
className="g-3",
245
children=[
246
dbc.Col(
247
children=[
248
html.Div(
249
className="pretty-container",
250
children=[
251
html.Div(
252
className="card-header",
253
children=[html.H6("Portfolio")],
254
),
255
dbc.Row(
256
className="g-2",
257
children=[
258
dbc.Col(
259
lg=6,
260
sm=12,
261
children=[
262
html.Label("Select subplots:"),
263
dcc.Dropdown(
264
id="subplot_dropdown",
265
options=[
266
{"value": k, "label": v["title"]}
267
for k, v in Portfolio.subplots.items()
268
],
269
multi=True,
270
value=default_subplots,
271
className="dropdown-control",
272
),
273
],
274
)
275
],
276
),
277
dcc.Loading(
278
id="portfolio_loading",
279
type="default",
280
color=loadcolor,
281
children=[
282
dcc.Graph(
283
id="portfolio_graph",
284
figure={"layout": default_layout},
285
)
286
],
287
),
288
],
289
),
290
]
291
)
292
],
293
),
294
dbc.Row(
295
className="g-3",
296
children=[
297
dbc.Col(
298
children=[
299
html.Div(
300
className="pretty-container",
301
children=[
302
html.Div(
303
className="card-header",
304
children=[html.H6("Stats")],
305
),
306
dcc.Loading(
307
id="stats_loading",
308
type="default",
309
color=loadcolor,
310
children=[
311
dash_table.DataTable(
312
id="stats_table",
313
columns=[{"name": c, "id": c} for c in stats_table_columns],
314
style_data_conditional=[
315
{
316
"if": {"column_id": stats_table_columns[1]},
317
"fontWeight": "bold",
318
"borderLeft": "1px solid " + bordercolor,
319
},
320
{
321
"if": {"column_id": stats_table_columns[2]},
322
"fontWeight": "bold",
323
},
324
{
325
"if": {"column_id": stats_table_columns[3]},
326
"fontWeight": "bold",
327
},
328
{
329
"if": {"column_id": stats_table_columns[4]},
330
"fontWeight": "bold",
331
},
332
{
333
"if": {"state": "selected"},
334
"backgroundColor": dark_bgcolor,
335
"color": active_color,
336
"border": "1px solid " + active_color,
337
},
338
{
339
"if": {"state": "active"},
340
"backgroundColor": dark_bgcolor,
341
"color": active_color,
342
"border": "1px solid " + active_color,
343
},
344
],
345
style_header={
346
"border": "none",
347
"backgroundColor": bgcolor,
348
"fontWeight": "bold",
349
"padding": "0px 5px",
350
},
351
style_data={
352
"border": "none",
353
"backgroundColor": bgcolor,
354
"color": dark_fontcolor,
355
"paddingRight": "10px",
356
},
357
style_table={"overflowX": "auto"},
358
style_as_list_view=False,
359
editable=False,
360
),
361
],
362
),
363
],
364
),
365
]
366
)
367
],
368
),
369
dbc.Row(
370
className="g-3",
371
children=[
372
dbc.Col(
373
children=[
374
html.Div(
375
className="pretty-container",
376
children=[
377
html.Div(
378
className="card-header",
379
children=[html.H6("Metric stats")],
380
),
381
dcc.Loading(
382
id="metric_stats_loading",
383
type="default",
384
color=loadcolor,
385
children=[
386
html.Label("Metric:"),
387
dbc.Row(
388
className="g-2",
389
children=[
390
dbc.Col(
391
lg=4,
392
sm=12,
393
children=[
394
dcc.Dropdown(
395
id="metric_dropdown",
396
className="dropdown-control",
397
),
398
],
399
),
400
dbc.Col(
401
lg=8,
402
sm=12,
403
children=[
404
dcc.Graph(
405
id="metric_graph",
406
figure={"layout": default_layout},
407
)
408
],
409
),
410
],
411
),
412
],
413
),
414
],
415
),
416
]
417
)
418
],
419
),
420
],
421
),
422
dbc.Col(
423
lg=4,
424
sm=12,
425
children=[
426
html.Div(
427
className="pretty-container",
428
children=[
429
html.Div(
430
className="card-header",
431
children=[html.H6("Settings")],
432
),
433
html.Button("Reset", id="reset_button", className="btn-neutral"),
434
html.Details(
435
open=True,
436
children=[
437
section_summary("Data"),
438
dbc.Row(
439
className="g-2",
440
children=[
441
dbc.Col(
442
children=[
443
html.Label("Yahoo! Finance symbol:"),
444
dcc.Input(
445
id="symbol_input",
446
className="input-control",
447
type="text",
448
value=default_symbol,
449
placeholder="Enter symbol...",
450
debounce=True,
451
),
452
]
453
),
454
dbc.Col(),
455
],
456
),
457
dbc.Row(
458
className="g-2",
459
children=[
460
dbc.Col(
461
children=[
462
html.Label("Period:"),
463
dcc.Dropdown(
464
id="period_dropdown",
465
options=[{"value": i, "label": i} for i in periods],
466
value=default_period,
467
className="dropdown-control",
468
),
469
]
470
),
471
dbc.Col(
472
children=[
473
html.Label("Interval:"),
474
dcc.Dropdown(
475
id="interval_dropdown",
476
options=[{"value": i, "label": i} for i in intervals],
477
value=default_interval,
478
className="dropdown-control",
479
),
480
]
481
),
482
],
483
),
484
html.Label("Filter period:"),
485
dcc.RangeSlider(
486
id="date_slider",
487
min=0.0,
488
max=1.0,
489
value=default_date_range,
490
allowCross=False,
491
tooltip={"placement": "bottom"},
492
),
493
dcc.Checklist(
494
id="yf_checklist",
495
options=[
496
{"label": "Adjust all OHLC automatically", "value": "auto_adjust"},
497
{
498
"label": "Use back-adjusted data to mimic true historical prices",
499
"value": "back_adjust",
500
},
501
],
502
value=default_yf_options,
503
className="checklist",
504
),
505
],
506
),
507
html.Details(
508
open=True,
509
children=[
510
section_summary("Entry patterns"),
511
html.Div(
512
id="entry_settings",
513
children=[
514
html.Div(
515
className="button-row",
516
children=[
517
html.Button(
518
"All", id="entry_all_button", className="btn-neutral"
519
),
520
html.Button(
521
"Random", id="entry_random_button", className="btn-neutral"
522
),
523
html.Button(
524
"Clear", id="entry_clear_button", className="btn-neutral"
525
),
526
],
527
),
528
html.Label("Number of random patterns:", className="field-label"),
529
dbc.Row(
530
className="g-2",
531
children=[
532
dbc.Col(
533
children=[
534
dcc.Input(
535
id="entry_n_random_input",
536
className="input-control",
537
value=default_entry_n_random,
538
placeholder="Enter number...",
539
debounce=True,
540
type="number",
541
min=1,
542
max=len(patterns),
543
step=1,
544
),
545
]
546
),
547
dbc.Col(),
548
],
549
),
550
html.Label("Select patterns:"),
551
dcc.Dropdown(
552
id="entry_pattern_dropdown",
553
options=[{"value": i, "label": i} for i in patterns],
554
multi=True,
555
value=default_entry_patterns,
556
className="dropdown-control",
557
),
558
],
559
),
560
],
561
),
562
html.Details(
563
open=True,
564
children=[
565
section_summary("Exit patterns"),
566
dcc.Checklist(
567
id="exit_checklist",
568
options=[{"label": "Same as entry patterns", "value": "same_as_entry"}],
569
value=default_exit_options,
570
className="checklist",
571
),
572
html.Div(
573
id="exit_settings",
574
hidden="same_as_entry" in default_exit_options,
575
children=[
576
html.Div(
577
className="button-row",
578
children=[
579
html.Button(
580
"All", id="exit_all_button", className="btn-neutral"
581
),
582
html.Button(
583
"Random", id="exit_random_button", className="btn-neutral"
584
),
585
html.Button(
586
"Clear", id="exit_clear_button", className="btn-neutral"
587
),
588
],
589
),
590
html.Label("Number of random patterns:", className="field-label"),
591
dbc.Row(
592
className="g-2",
593
children=[
594
dbc.Col(
595
children=[
596
dcc.Input(
597
id="exit_n_random_input",
598
className="input-control",
599
value=default_exit_n_random,
600
placeholder="Enter number...",
601
debounce=True,
602
type="number",
603
min=1,
604
max=len(patterns),
605
step=1,
606
),
607
]
608
),
609
dbc.Col(),
610
],
611
),
612
html.Label("Select patterns:"),
613
dcc.Dropdown(
614
id="exit_pattern_dropdown",
615
options=[{"value": i, "label": i} for i in patterns],
616
multi=True,
617
value=default_exit_patterns,
618
className="dropdown-control",
619
),
620
],
621
),
622
],
623
),
624
html.Details(
625
children=[
626
section_summary("Candle settings"),
627
dash_table.DataTable(
628
id="candle_settings_table",
629
columns=[
630
{
631
"name": c,
632
"id": c,
633
"editable": i in (2, 3),
634
"type": "numeric" if i in (2, 3) else "any",
635
}
636
for i, c in enumerate(default_candle_settings.columns)
637
],
638
data=default_candle_settings.to_dict("records"),
639
style_data_conditional=[
640
{
641
"if": {"column_editable": True},
642
"backgroundColor": dark_bgcolor,
643
"border": "1px solid " + bordercolor_strong,
644
},
645
{
646
"if": {"state": "selected"},
647
"backgroundColor": dark_bgcolor,
648
"color": active_color,
649
"border": "1px solid " + active_color,
650
},
651
{
652
"if": {"state": "active"},
653
"backgroundColor": dark_bgcolor,
654
"color": active_color,
655
"border": "1px solid " + active_color,
656
},
657
],
658
style_header={
659
"border": "none",
660
"backgroundColor": bgcolor,
661
"fontWeight": "bold",
662
"padding": "0px 5px",
663
},
664
style_data={
665
"border": "none",
666
"backgroundColor": bgcolor,
667
"color": dark_fontcolor,
668
},
669
style_table={"overflowX": "auto"},
670
style_as_list_view=False,
671
editable=True,
672
),
673
],
674
),
675
html.Details(
676
open=False,
677
children=[
678
section_summary("Custom pattern"),
679
html.Div(
680
id="custom_settings",
681
children=[
682
html.Label("Select entry dates:"),
683
dcc.Dropdown(
684
id="custom_entry_dropdown",
685
options=[],
686
multi=True,
687
value=default_entry_dates,
688
className="dropdown-control",
689
),
690
html.Label("Select exit dates:"),
691
dcc.Dropdown(
692
id="custom_exit_dropdown",
693
options=[],
694
multi=True,
695
value=default_exit_dates,
696
className="dropdown-control",
697
),
698
],
699
),
700
],
701
),
702
html.Details(
703
open=True,
704
children=[
705
section_summary("Simulation settings"),
706
dbc.Row(
707
className="g-2",
708
children=[
709
dbc.Col(
710
children=[
711
html.Label("Fees (in %):"),
712
dcc.Input(
713
id="fees_input",
714
className="input-control",
715
type="number",
716
value=default_fees,
717
placeholder="Enter fees...",
718
debounce=True,
719
min=0,
720
max=100,
721
),
722
]
723
),
724
dbc.Col(
725
children=[
726
html.Label("Fixed fees:"),
727
dcc.Input(
728
id="fixed_fees_input",
729
className="input-control",
730
type="number",
731
value=default_fixed_fees,
732
placeholder="Enter fixed fees...",
733
debounce=True,
734
min=0,
735
),
736
]
737
),
738
],
739
),
740
dbc.Row(
741
className="g-2",
742
children=[
743
dbc.Col(
744
children=[
745
html.Label("Slippage (in % of H-O):"),
746
dcc.Input(
747
id="slippage_input",
748
className="input-control",
749
type="number",
750
value=default_slippage,
751
placeholder="Enter slippage...",
752
debounce=True,
753
min=0,
754
max=100,
755
),
756
]
757
),
758
dbc.Col(),
759
],
760
),
761
dbc.Row(
762
className="g-2",
763
children=[
764
dbc.Col(
765
children=[
766
html.Label("Direction:"),
767
dcc.Dropdown(
768
id="direction_dropdown",
769
options=[{"value": i, "label": i} for i in directions],
770
value=default_direction,
771
className="dropdown-control",
772
),
773
]
774
),
775
dbc.Col(
776
children=[
777
html.Label("Conflict Mode:"),
778
dcc.Dropdown(
779
id="conflict_mode_dropdown",
780
options=[{"value": i, "label": i} for i in conflict_modes],
781
value=default_conflict_mode,
782
className="dropdown-control",
783
),
784
]
785
),
786
],
787
),
788
dcc.Checklist(
789
id="sim_checklist",
790
options=[
791
{"label": "Allow signal accumulation", "value": "allow_accumulate"}
792
],
793
value=default_sim_options,
794
className="checklist",
795
),
796
html.Label("Number of random strategies to test against:"),
797
dbc.Row(
798
className="g-2",
799
children=[
800
dbc.Col(
801
children=[
802
dcc.Input(
803
id="n_random_strat_input",
804
className="input-control",
805
value=default_n_random_strat,
806
placeholder="Enter number...",
807
debounce=True,
808
type="number",
809
min=10,
810
max=1000,
811
step=1,
812
),
813
]
814
),
815
dbc.Col(),
816
],
817
),
818
dcc.Checklist(
819
id="prob_checklist",
820
options=[
821
{"label": "Mimic strategy by shuffling", "value": "mimic_strategy"}
822
],
823
value=default_prob_options,
824
className="checklist",
825
),
826
html.Div(
827
id="prob_settings",
828
hidden="mimic_strategy" in default_prob_options,
829
children=[
830
dbc.Row(
831
className="g-2",
832
children=[
833
dbc.Col(
834
children=[
835
html.Label("Entry probability (in %):"),
836
dcc.Input(
837
id="entry_prob_input",
838
className="input-control",
839
value=default_entry_prob,
840
placeholder="Enter number...",
841
debounce=True,
842
type="number",
843
min=0,
844
max=100,
845
),
846
]
847
),
848
dbc.Col(
849
children=[
850
html.Label("Exit probability (in %):"),
851
dcc.Input(
852
id="exit_prob_input",
853
className="input-control",
854
value=default_exit_prob,
855
placeholder="Enter number...",
856
debounce=True,
857
type="number",
858
min=0,
859
max=100,
860
),
861
]
862
),
863
],
864
),
865
],
866
),
867
dcc.Checklist(
868
id="stats_checklist",
869
options=[
870
{"label": "Include open trades in stats", "value": "incl_open"},
871
{
872
"label": "Use positions instead of trades in stats",
873
"value": "use_positions",
874
},
875
],
876
value=default_stats_options,
877
className="checklist",
878
),
879
],
880
),
881
],
882
),
883
],
884
),
885
],
886
),
887
html.Div(id="data_signal", style={"display": "none"}),
888
html.Div(id="index_signal", style={"display": "none"}),
889
html.Div(id="candle_settings_signal", style={"display": "none"}),
890
html.Div(id="stats_signal", style={"display": "none"}),
891
html.Div(id="window_width", style={"display": "none"}),
892
dcc.Location(id="url"),
893
],
894
)
895
896
app.clientside_callback(
897
"""
898
function(href) {
899
return window.innerWidth;
900
}
901
""",
902
Output("window_width", "children"),
903
[
904
Input("url", "href"),
905
],
906
)
907
908
909
@cache.memoize()
910
def fetch_data(symbol, period, interval, auto_adjust, back_adjust):
911
"""Fetch OHLCV data from Yahoo! Finance."""
912
df = yf.Ticker(symbol).history(
913
period=period,
914
interval=interval,
915
actions=False,
916
auto_adjust=auto_adjust,
917
back_adjust=back_adjust,
918
)
919
if df is None or df.empty:
920
raise ValueError("Empty data from yfinance")
921
return df
922
923
924
@app.callback(
925
[
926
Output("data_signal", "children"),
927
Output("index_signal", "children"),
928
],
929
[
930
Input("symbol_input", "value"),
931
Input("period_dropdown", "value"),
932
Input("interval_dropdown", "value"),
933
Input("yf_checklist", "value"),
934
],
935
)
936
def update_data(symbol, period, interval, yf_options):
937
"""Store data into a hidden DIV to avoid repeatedly calling Yahoo's API."""
938
auto_adjust = "auto_adjust" in yf_options
939
back_adjust = "back_adjust" in yf_options
940
df = fetch_data(symbol, period, interval, auto_adjust, back_adjust)
941
index_as_str = pd.to_datetime(df.index).astype(str).tolist()
942
return df.to_json(date_format="iso", orient="split"), index_as_str
943
944
945
@app.callback(
946
[
947
Output("date_slider", "min"),
948
Output("date_slider", "max"),
949
Output("date_slider", "value"),
950
],
951
[
952
Input("index_signal", "children"),
953
],
954
)
955
def update_date_slider(date_list):
956
"""Once index (dates) has changed, reset the date slider."""
957
return 0, len(date_list) - 1, [0, len(date_list) - 1]
958
959
960
@app.callback(
961
[
962
Output("custom_entry_dropdown", "options"),
963
Output("custom_exit_dropdown", "options"),
964
],
965
[
966
Input("index_signal", "children"),
967
Input("date_slider", "value"),
968
],
969
)
970
def update_custom_options(date_list, date_range):
971
"""Once dates have changed, update entry/exit dates in custom pattern section.
972
973
If selected dates cannot be found in new dates, they will be automatically removed."""
974
filtered_dates = np.asarray(date_list)[date_range[0] : date_range[1] + 1].tolist()
975
custom_options = [{"value": i, "label": i} for i in filtered_dates]
976
return custom_options, custom_options
977
978
979
@app.callback(
980
Output("entry_pattern_dropdown", "value"),
981
[
982
Input("entry_all_button", "n_clicks"),
983
Input("entry_random_button", "n_clicks"),
984
Input("entry_clear_button", "n_clicks"),
985
Input("reset_button", "n_clicks"),
986
],
987
[
988
State("entry_n_random_input", "value"),
989
],
990
)
991
def select_entry_patterns(_1, _2, _3, _4, n_random):
992
"""Select all/random entry patterns or clear."""
993
ctx = callback_context
994
if ctx.triggered:
995
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
996
if button_id == "entry_all_button":
997
return patterns
998
elif button_id == "entry_random_button":
999
return random.sample(patterns, n_random)
1000
elif button_id == "entry_clear_button":
1001
return []
1002
elif button_id == "reset_button":
1003
return default_entry_patterns
1004
return no_update
1005
1006
1007
@app.callback(
1008
[
1009
Output("exit_settings", "hidden"),
1010
Output("exit_n_random_input", "value"),
1011
Output("exit_pattern_dropdown", "value"),
1012
],
1013
[
1014
Input("exit_checklist", "value"),
1015
Input("exit_all_button", "n_clicks"),
1016
Input("exit_random_button", "n_clicks"),
1017
Input("exit_clear_button", "n_clicks"),
1018
Input("reset_button", "n_clicks"),
1019
Input("entry_n_random_input", "value"),
1020
Input("entry_pattern_dropdown", "value"),
1021
],
1022
[State("exit_n_random_input", "value")],
1023
)
1024
def select_exit_patterns(exit_options, _1, _2, _3, _4, entry_n_random, entry_patterns, exit_n_random):
1025
"""Select all/random exit patterns, clear, or configure the same way as entry patterns."""
1026
ctx = callback_context
1027
same_as_entry = "same_as_entry" in exit_options
1028
if ctx.triggered:
1029
control_id = ctx.triggered[0]["prop_id"].split(".")[0]
1030
if control_id == "exit_checklist":
1031
return same_as_entry, entry_n_random, entry_patterns
1032
elif control_id == "exit_all_button":
1033
return same_as_entry, exit_n_random, patterns
1034
elif control_id == "exit_random_button":
1035
return same_as_entry, exit_n_random, random.sample(patterns, exit_n_random)
1036
elif control_id == "exit_clear_button":
1037
return same_as_entry, exit_n_random, []
1038
elif control_id == "reset_button":
1039
default_same_as_entry = "same_as_entry" in default_exit_options
1040
return default_same_as_entry, default_exit_n_random, default_exit_patterns
1041
elif control_id in ("entry_n_random_input", "entry_pattern_dropdown"):
1042
if same_as_entry:
1043
return same_as_entry, entry_n_random, entry_patterns
1044
return no_update
1045
1046
1047
@app.callback(
1048
Output("candle_settings_signal", "children"),
1049
[
1050
Input("candle_settings_table", "data"),
1051
],
1052
)
1053
def set_candle_settings(data):
1054
"""Update candle settings in TA-Lib."""
1055
for d in data:
1056
AvgPeriod = d["AvgPeriod"]
1057
if isinstance(AvgPeriod, float) and float.is_integer(AvgPeriod):
1058
AvgPeriod = int(AvgPeriod)
1059
Factor = float(d["Factor"])
1060
_ta_set_candle_settings(
1061
getattr(CandleSettingType, d["SettingType"]),
1062
getattr(RangeType, d["RangeType"]),
1063
AvgPeriod,
1064
Factor,
1065
)
1066
1067
1068
@app.callback(
1069
[
1070
Output("ohlcv_graph", "figure"),
1071
Output("prob_settings", "hidden"),
1072
Output("entry_prob_input", "value"),
1073
Output("exit_prob_input", "value"),
1074
],
1075
[
1076
Input("window_width", "children"),
1077
Input("plot_type_dropdown", "value"),
1078
Input("data_signal", "children"),
1079
Input("date_slider", "value"),
1080
Input("entry_pattern_dropdown", "value"),
1081
Input("exit_pattern_dropdown", "value"),
1082
Input("candle_settings_signal", "children"),
1083
Input("custom_entry_dropdown", "value"),
1084
Input("custom_exit_dropdown", "value"),
1085
Input("prob_checklist", "value"),
1086
Input("reset_button", "n_clicks"),
1087
],
1088
[State("entry_prob_input", "value"), State("exit_prob_input", "value")],
1089
)
1090
def update_ohlcv(
1091
window_width,
1092
plot_type,
1093
df_json,
1094
date_range,
1095
entry_patterns,
1096
exit_patterns,
1097
_1,
1098
entry_dates,
1099
exit_dates,
1100
prob_options,
1101
_2,
1102
entry_prob,
1103
exit_prob,
1104
):
1105
"""Update OHLCV graph.
1106
1107
Also update probability settings, as they also depend upon conversion of patterns into signals."""
1108
df = pd.read_json(StringIO(df_json), orient="split")
1109
1110
# Filter by date
1111
df = df.iloc[date_range[0] : date_range[1] + 1]
1112
1113
# Run pattern recognition indicators and combine results
1114
talib_inputs = {
1115
"open": df["Open"].values,
1116
"high": df["High"].values,
1117
"low": df["Low"].values,
1118
"close": df["Close"].values,
1119
"volume": df["Volume"].values,
1120
}
1121
entry_patterns = list((entry_patterns or [])) + ["CUSTOM"]
1122
exit_patterns = list((exit_patterns or [])) + ["CUSTOM"]
1123
all_patterns = list(set(entry_patterns + exit_patterns))
1124
signal_df = pd.DataFrame.vbt.empty(
1125
(len(df.index), len(all_patterns)), fill_value=0.0, index=df.index, columns=all_patterns
1126
)
1127
for pattern in all_patterns:
1128
if pattern != "CUSTOM":
1129
signal_df[pattern] = abstract.Function(pattern)(talib_inputs)
1130
signal_df.loc[entry_dates, "CUSTOM"] += 100.0
1131
signal_df.loc[exit_dates, "CUSTOM"] += -100.0
1132
entry_signal_df = signal_df[entry_patterns]
1133
exit_signal_df = signal_df[exit_patterns]
1134
1135
# Entry patterns
1136
entry_df = entry_signal_df[(entry_signal_df > 0).any(axis=1)]
1137
entry_patterns = []
1138
for row_i, row in entry_df.iterrows():
1139
entry_patterns.append("<br>".join(row.index[row != 0]))
1140
entry_patterns = np.asarray(entry_patterns)
1141
1142
# Exit patterns
1143
exit_df = exit_signal_df[(exit_signal_df < 0).any(axis=1)]
1144
exit_patterns = []
1145
for row_i, row in exit_df.iterrows():
1146
exit_patterns.append("<br>".join(row.index[row != 0]))
1147
exit_patterns = np.asarray(exit_patterns)
1148
1149
# Prepare scatter data
1150
highest_high = df["High"].max()
1151
lowest_low = df["Low"].min()
1152
distance = (highest_high - lowest_low) / 5
1153
entry_y = df.loc[entry_df.index, "Low"] - distance
1154
entry_y.index = pd.to_datetime(entry_y.index)
1155
exit_y = df.loc[exit_df.index, "High"] + distance
1156
exit_y.index = pd.to_datetime(exit_y.index)
1157
1158
# Prepare signals
1159
entry_signals = pd.Series.vbt.empty_like(entry_y, True)
1160
exit_signals = pd.Series.vbt.empty_like(exit_y, True)
1161
1162
# Build graph
1163
height = int(9 / 21 * 2 / 3 * window_width)
1164
fig = df.vbt.ohlcv.plot(
1165
plot_type=plot_type,
1166
**merge_dicts(
1167
default_layout,
1168
dict(
1169
width=None,
1170
height=max(500, height),
1171
margin=dict(r=40),
1172
hovermode="closest",
1173
xaxis2=dict(title="Date"),
1174
yaxis2=dict(title="Volume"),
1175
yaxis=dict(
1176
title="Price",
1177
),
1178
),
1179
),
1180
)
1181
entry_signals.vbt.signals.plot_as_entry_markers(
1182
y=entry_y,
1183
trace_kwargs=dict(
1184
customdata=entry_patterns[:, None],
1185
hovertemplate="%{x}<br>%{customdata[0]}",
1186
name="Bullish signal",
1187
),
1188
add_trace_kwargs=dict(row=1, col=1),
1189
fig=fig,
1190
)
1191
exit_signals.vbt.signals.plot_as_exit_markers(
1192
y=exit_y,
1193
trace_kwargs=dict(
1194
customdata=exit_patterns[:, None],
1195
hovertemplate="%{x}<br>%{customdata[0]}",
1196
name="Bearish signal",
1197
),
1198
add_trace_kwargs=dict(row=1, col=1),
1199
fig=fig,
1200
)
1201
fig.update_xaxes(gridcolor=gridcolor)
1202
fig.update_yaxes(gridcolor=gridcolor, zerolinecolor=gridcolor)
1203
figure = dict(data=fig.data, layout=fig.layout)
1204
1205
mimic_strategy = "mimic_strategy" in prob_options
1206
ctx = callback_context
1207
if ctx.triggered:
1208
control_id = ctx.triggered[0]["prop_id"].split(".")[0]
1209
if control_id == "reset_button":
1210
mimic_strategy = "mimic_strategy" in default_prob_options
1211
entry_prob = default_entry_prob
1212
exit_prob = default_exit_prob
1213
if mimic_strategy:
1214
entry_prob = np.round(len(entry_df.index) / len(df.index) * 100, 4)
1215
exit_prob = np.round(len(exit_df.index) / len(df.index) * 100, 4)
1216
return figure, mimic_strategy, entry_prob, exit_prob
1217
1218
1219
def simulate_portfolio(
1220
df,
1221
interval,
1222
date_range,
1223
selected_data,
1224
entry_patterns,
1225
exit_patterns,
1226
entry_dates,
1227
exit_dates,
1228
fees,
1229
fixed_fees,
1230
slippage,
1231
direction,
1232
conflict_mode,
1233
sim_options,
1234
n_random_strat,
1235
prob_options,
1236
entry_prob,
1237
exit_prob,
1238
):
1239
"""Simulate portfolio of the main strategy, buy & hold strategy, and a bunch of random strategies."""
1240
# Filter by date
1241
df = df.iloc[date_range[0] : date_range[1] + 1]
1242
1243
# Run pattern recognition indicators and combine results
1244
talib_inputs = {
1245
"open": df["Open"].values,
1246
"high": df["High"].values,
1247
"low": df["Low"].values,
1248
"close": df["Close"].values,
1249
"volume": df["Volume"].values,
1250
}
1251
entry_patterns = list((entry_patterns or [])) + ["CUSTOM"]
1252
exit_patterns = list((exit_patterns or [])) + ["CUSTOM"]
1253
all_patterns = list(set(entry_patterns + exit_patterns))
1254
entry_i = [all_patterns.index(p) for p in entry_patterns]
1255
exit_i = [all_patterns.index(p) for p in exit_patterns]
1256
signals = np.full((len(df.index), len(all_patterns)), 0.0, dtype=np.float64)
1257
for i, pattern in enumerate(all_patterns):
1258
if pattern != "CUSTOM":
1259
signals[:, i] = abstract.Function(pattern)(talib_inputs)
1260
signals[np.flatnonzero(df.index.isin(entry_dates)), all_patterns.index("CUSTOM")] += 100.0
1261
signals[np.flatnonzero(df.index.isin(exit_dates)), all_patterns.index("CUSTOM")] += -100.0
1262
signals /= 100.0 # TA-Lib functions have output in increments of 100
1263
1264
# Filter signals
1265
if selected_data is not None:
1266
new_signals = np.full_like(signals, 0.0)
1267
for point in selected_data["points"]:
1268
if "customdata" in point:
1269
point_patterns = point["customdata"][0].split("<br>")
1270
pi = df.index.get_loc(point["x"])
1271
for p in point_patterns:
1272
pc = all_patterns.index(p)
1273
new_signals[pi, pc] = signals[pi, pc]
1274
signals = new_signals
1275
1276
# Generate size for main
1277
def _generate_size(signals):
1278
entry_signals = signals[:, entry_i]
1279
exit_signals = signals[:, exit_i]
1280
return np.where(entry_signals > 0, entry_signals, 0).sum(axis=1) + np.where(
1281
exit_signals < 0, exit_signals, 0
1282
).sum(axis=1)
1283
1284
main_size = np.empty((len(df.index),), dtype=np.float64)
1285
main_size[0] = 0 # avoid looking into future
1286
main_size[1:] = _generate_size(signals)[:-1]
1287
1288
# Generate size for buy & hold
1289
hold_size = np.full_like(main_size, 0.0)
1290
hold_size[0] = np.inf
1291
1292
# Generate size for random
1293
def _shuffle_along_axis(a, axis):
1294
idx = np.random.rand(*a.shape).argsort(axis=axis)
1295
return np.take_along_axis(a, idx, axis=axis)
1296
1297
rand_size = np.empty((len(df.index), n_random_strat), dtype=np.float64)
1298
rand_size[0] = 0 # avoid looking into future
1299
if "mimic_strategy" in prob_options:
1300
for i in range(n_random_strat):
1301
rand_signals = _shuffle_along_axis(signals, 0)
1302
rand_size[1:, i] = _generate_size(rand_signals)[:-1]
1303
else:
1304
entry_signals = pd.DataFrame.vbt.signals.generate_random(
1305
(rand_size.shape[0] - 1, rand_size.shape[1]), prob=entry_prob / 100
1306
).values
1307
exit_signals = pd.DataFrame.vbt.signals.generate_random(
1308
(rand_size.shape[0] - 1, rand_size.shape[1]), prob=exit_prob / 100
1309
).values
1310
rand_size[1:, :] = np.where(entry_signals, 1.0, 0.0) - np.where(exit_signals, 1.0, 0.0)
1311
1312
# Simulate portfolio
1313
def _simulate_portfolio(size, init_cash="autoalign"):
1314
return Portfolio.from_signals(
1315
close=df["Close"],
1316
entries=size > 0,
1317
exits=size < 0,
1318
price=df["Open"],
1319
size=np.abs(size),
1320
direction=direction,
1321
upon_dir_conflict=conflict_mode,
1322
accumulate="allow_accumulate" in sim_options,
1323
init_cash=init_cash,
1324
fees=float(fees) / 100,
1325
fixed_fees=float(fixed_fees),
1326
slippage=(float(slippage) / 100) * (df["High"] / df["Open"] - 1),
1327
freq=interval,
1328
)
1329
1330
# Align initial cash across main and random strategies
1331
aligned_portfolio = _simulate_portfolio(np.hstack((main_size[:, None], rand_size)))
1332
# Fixate initial cash for indexing
1333
aligned_portfolio = aligned_portfolio.replace(init_cash=aligned_portfolio.init_cash)
1334
# Separate portfolios
1335
main_portfolio = aligned_portfolio.iloc[0]
1336
rand_portfolio = aligned_portfolio.iloc[1:]
1337
1338
# Simulate buy & hold portfolio
1339
hold_portfolio = _simulate_portfolio(hold_size, init_cash=main_portfolio.init_cash)
1340
1341
return main_portfolio, hold_portfolio, rand_portfolio
1342
1343
1344
@app.callback(
1345
[
1346
Output("portfolio_graph", "figure"),
1347
Output("stats_table", "data"),
1348
Output("stats_signal", "children"),
1349
Output("metric_dropdown", "options"),
1350
Output("metric_dropdown", "value"),
1351
],
1352
[
1353
Input("window_width", "children"),
1354
Input("subplot_dropdown", "value"),
1355
Input("data_signal", "children"),
1356
Input("symbol_input", "value"),
1357
Input("interval_dropdown", "value"),
1358
Input("date_slider", "value"),
1359
Input("ohlcv_graph", "selectedData"),
1360
Input("entry_pattern_dropdown", "value"),
1361
Input("exit_pattern_dropdown", "value"),
1362
Input("candle_settings_signal", "children"),
1363
Input("custom_entry_dropdown", "value"),
1364
Input("custom_exit_dropdown", "value"),
1365
Input("fees_input", "value"),
1366
Input("fixed_fees_input", "value"),
1367
Input("slippage_input", "value"),
1368
Input("direction_dropdown", "value"),
1369
Input("conflict_mode_dropdown", "value"),
1370
Input("sim_checklist", "value"),
1371
Input("n_random_strat_input", "value"),
1372
Input("prob_checklist", "value"),
1373
Input("entry_prob_input", "value"),
1374
Input("exit_prob_input", "value"),
1375
Input("stats_checklist", "value"),
1376
Input("reset_button", "n_clicks"),
1377
],
1378
[State("metric_dropdown", "value")],
1379
)
1380
def update_stats(
1381
window_width,
1382
subplots,
1383
df_json,
1384
symbol,
1385
interval,
1386
date_range,
1387
selected_data,
1388
entry_patterns,
1389
exit_patterns,
1390
_1,
1391
entry_dates,
1392
exit_dates,
1393
fees,
1394
fixed_fees,
1395
slippage,
1396
direction,
1397
conflict_mode,
1398
sim_options,
1399
n_random_strat,
1400
prob_options,
1401
entry_prob,
1402
exit_prob,
1403
stats_options,
1404
_2,
1405
curr_metric,
1406
):
1407
"""Final stage where we calculate key performance metrics and compare strategies."""
1408
df = pd.read_json(StringIO(df_json), orient="split")
1409
1410
# Simulate portfolio
1411
main_portfolio, hold_portfolio, rand_portfolio = simulate_portfolio(
1412
df,
1413
interval,
1414
date_range,
1415
selected_data,
1416
entry_patterns,
1417
exit_patterns,
1418
entry_dates,
1419
exit_dates,
1420
fees,
1421
fixed_fees,
1422
slippage,
1423
direction,
1424
conflict_mode,
1425
sim_options,
1426
n_random_strat,
1427
prob_options,
1428
entry_prob,
1429
exit_prob,
1430
)
1431
1432
subplot_settings = dict()
1433
if "cum_returns" in subplots:
1434
subplot_settings["cum_returns"] = dict(
1435
benchmark_kwargs=dict(
1436
trace_kwargs=dict(line=dict(color=adjust_opacity(color_schema["yellow"], 0.5)), name=symbol)
1437
)
1438
)
1439
height = int(6 / 21 * 2 / 3 * window_width)
1440
fig = main_portfolio.plot(
1441
subplots=subplots,
1442
subplot_settings=subplot_settings,
1443
**merge_dicts(
1444
default_layout,
1445
dict(width=None, height=len(subplots) * max(300, height) if len(subplots) > 1 else max(350, height)),
1446
),
1447
)
1448
fig.update_traces(xaxis="x" if len(subplots) == 1 else "x" + str(len(subplots)))
1449
fig.update_xaxes(gridcolor=gridcolor)
1450
fig.update_yaxes(gridcolor=gridcolor, zerolinecolor=gridcolor)
1451
1452
def _chop_microseconds(delta):
1453
return delta - pd.Timedelta(microseconds=delta.microseconds, nanoseconds=delta.nanoseconds)
1454
1455
def _metric_to_str(x):
1456
if isinstance(x, float):
1457
return "%.2f" % x
1458
if isinstance(x, pd.Timedelta):
1459
return str(_chop_microseconds(x))
1460
return str(x)
1461
1462
incl_open = "incl_open" in stats_options
1463
use_positions = "use_positions" in stats_options
1464
main_stats = main_portfolio.stats(settings=dict(incl_open=incl_open, use_positions=use_positions))
1465
hold_stats = hold_portfolio.stats(settings=dict(incl_open=True, use_positions=use_positions))
1466
rand_stats = rand_portfolio.stats(settings=dict(incl_open=incl_open, use_positions=use_positions), agg_func=None)
1467
rand_stats_median = rand_stats.iloc[:, 3:].median(axis=0)
1468
rand_stats_mean = rand_stats.iloc[:, 3:].mean(axis=0)
1469
rand_stats_std = rand_stats.iloc[:, 3:].std(axis=0, ddof=0)
1470
stats_mean_diff = main_stats.iloc[3:] - rand_stats_mean
1471
1472
def _to_float(x):
1473
if pd.isnull(x):
1474
return np.nan
1475
if isinstance(x, float):
1476
if np.allclose(x, 0):
1477
return 0.0
1478
if isinstance(x, pd.Timedelta):
1479
return float(x.total_seconds())
1480
return float(x)
1481
1482
z = stats_mean_diff.apply(_to_float) / rand_stats_std.apply(_to_float)
1483
1484
table_data = pd.DataFrame(columns=stats_table_columns)
1485
table_data.iloc[:, 0] = main_stats.index
1486
table_data.iloc[:, 1] = hold_stats.apply(_metric_to_str).values
1487
table_data.iloc[:3, 2] = table_data.iloc[:3, 1]
1488
table_data.iloc[3:, 2] = rand_stats_median.apply(_metric_to_str).values
1489
table_data.iloc[:, 3] = main_stats.apply(_metric_to_str).values
1490
table_data.iloc[3:, 4] = z.apply(_metric_to_str).values
1491
1492
metric = curr_metric
1493
ctx = callback_context
1494
if ctx.triggered:
1495
control_id = ctx.triggered[0]["prop_id"].split(".")[0]
1496
if control_id == "reset_button":
1497
metric = default_metric
1498
if metric is None:
1499
metric = default_metric
1500
return (
1501
dict(data=fig.data, layout=fig.layout),
1502
table_data.to_dict("records"),
1503
json.dumps(
1504
{
1505
"main": {m: [_to_float(main_stats[m])] for m in main_stats.index[3:]},
1506
"hold": {m: [_to_float(hold_stats[m])] for m in main_stats.index[3:]},
1507
"rand": {m: rand_stats[m].apply(_to_float).values.tolist() for m in main_stats.index[3:]},
1508
}
1509
),
1510
[{"value": i, "label": i} for i in main_stats.index[3:]],
1511
metric,
1512
)
1513
1514
1515
@app.callback(
1516
Output("metric_graph", "figure"),
1517
[
1518
Input("window_width", "children"),
1519
Input("stats_signal", "children"),
1520
Input("metric_dropdown", "value"),
1521
],
1522
)
1523
def update_metric_stats(window_width, stats_json, metric):
1524
"""Once a new metric has been selected, plot its distribution."""
1525
stats_dict = json.loads(stats_json)
1526
height = int(9 / 21 * 2 / 3 * 2 / 3 * window_width)
1527
return dict(
1528
data=[
1529
go.Box(
1530
x=stats_dict["rand"][metric],
1531
quartilemethod="linear",
1532
jitter=0.3,
1533
pointpos=1.8,
1534
boxpoints="all",
1535
boxmean="sd",
1536
hoveron="points",
1537
hovertemplate="%{x}<br>Random",
1538
name="",
1539
marker=dict(
1540
color=color_schema["blue"],
1541
opacity=0.5,
1542
size=8,
1543
),
1544
),
1545
go.Box(
1546
x=stats_dict["hold"][metric],
1547
quartilemethod="linear",
1548
boxpoints="all",
1549
jitter=0,
1550
pointpos=1.8,
1551
hoveron="points",
1552
hovertemplate="%{x}<br>Buy & Hold",
1553
fillcolor="rgba(0,0,0,0)",
1554
line=dict(color="rgba(0,0,0,0)"),
1555
name="",
1556
marker=dict(
1557
color=color_schema["orange"],
1558
size=8,
1559
),
1560
),
1561
go.Box(
1562
x=stats_dict["main"][metric],
1563
quartilemethod="linear",
1564
boxpoints="all",
1565
jitter=0,
1566
pointpos=1.8,
1567
hoveron="points",
1568
hovertemplate="%{x}<br>Strategy",
1569
fillcolor="rgba(0,0,0,0)",
1570
line=dict(color="rgba(0,0,0,0)"),
1571
name="",
1572
marker=dict(
1573
color=color_schema["green"],
1574
size=8,
1575
),
1576
),
1577
],
1578
layout=merge_dicts(
1579
default_layout,
1580
dict(
1581
height=max(350, height),
1582
showlegend=False,
1583
margin=dict(l=60, r=20, t=40, b=20),
1584
hovermode="closest",
1585
xaxis=dict(gridcolor=gridcolor, title=metric, side="top"),
1586
yaxis=dict(gridcolor=gridcolor),
1587
),
1588
),
1589
)
1590
1591
1592
@app.callback(
1593
[
1594
Output("symbol_input", "value"),
1595
Output("period_dropdown", "value"),
1596
Output("interval_dropdown", "value"),
1597
Output("yf_checklist", "value"),
1598
Output("entry_n_random_input", "value"),
1599
Output("exit_checklist", "value"),
1600
Output("candle_settings_table", "data"),
1601
Output("custom_entry_dropdown", "value"),
1602
Output("custom_exit_dropdown", "value"),
1603
Output("fees_input", "value"),
1604
Output("fixed_fees_input", "value"),
1605
Output("slippage_input", "value"),
1606
Output("conflict_mode_dropdown", "value"),
1607
Output("direction_dropdown", "value"),
1608
Output("sim_checklist", "value"),
1609
Output("n_random_strat_input", "value"),
1610
Output("prob_checklist", "value"),
1611
Output("stats_checklist", "value"),
1612
],
1613
[
1614
Input("reset_button", "n_clicks"),
1615
],
1616
prevent_initial_call=True,
1617
)
1618
def reset_settings(_):
1619
"""Reset most settings. Other settings are reset in their callbacks."""
1620
return (
1621
default_symbol,
1622
default_period,
1623
default_interval,
1624
default_yf_options,
1625
default_entry_n_random,
1626
default_exit_options,
1627
default_candle_settings.to_dict("records"),
1628
default_entry_dates,
1629
default_exit_dates,
1630
default_fees,
1631
default_fixed_fees,
1632
default_slippage,
1633
default_conflict_mode,
1634
default_direction,
1635
default_sim_options,
1636
default_n_random_strat,
1637
default_prob_options,
1638
default_stats_options,
1639
)
1640
1641
1642
if __name__ == "__main__":
1643
app.run(host=HOST, port=PORT, debug=DEBUG)
1644
1645