0%

【Sunflower】Dash应用程序实践(三)

分栏左侧控制卡

下面是分栏左侧的控制卡部分,主要作用是选取回测组合测试,先选择回测账户,然后选择回测策略。然后从数据库中提取数据。用两个下拉框控件及一个按钮控件实现。

首先,我们需要创建 dcc.Store 来存储获取到的数据。在 App 的 layout 中增加以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pp.layout = dbc.Container(    
[
dcc.Location(id="url-refresh", refresh=True),
# Header
dbc.Container(
[
generate_header()
],
id="header-container",
fluid=True,
),
dbc.Row(
[
dbc.Col(id="left-column", children=generate_control_card(), md=2),
dbc.Col(id="right-column", children=generate_display_tabs(), md=10),
]

),
# 增加以下四行内容
dcc.Store(id="account-trade-history"),
dcc.Store(id="stock-day"),
dcc.Store(id="benchmark-day"),
dcc.Store(id='asset')


],
id="app-container",
fluid=True,
)

代码详解: 1. dcc.Store(id="account-trade-history")

- 这是一个存储组件,`id="account-trade-history"` 可以唯一标识这个存储单元。
- 用于存储与“回测账户交易历史”相关的数据,方便在多个回调函数间访问该数据而无需重新加载或处理。
  1. dcc.Store(id="stock-day")

    • 用于存储“每日股票日线数据”。
    • 这可以是一个 DataFrame 格式的数据表,包含每日股票的 OHLC 等信息。
  2. dcc.Store(id="benchmark-day")

    • 存储“基准日”数据,可能包含基准指数(如沪深300、上证 50、科创 50、中证 500)的日线数据,用于与投资组合的表现做对比。
  3. dcc.Store(id="asset")

    • 存储资产信息,包括投资组合中的当前资产信息。

然后创建两个下拉框及一个按钮控制数据的读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
def generate_control_card():
"""
:return: A Card containing controls(dropdown, button) in left column
"""
card = dbc.Card(
[

html.Div(
[
dbc.Label("回测账户"),
dcc.Dropdown(
id="account-select",
options=fetch_account_list(),
placeholder="Select Account",
),
]
),
html.Div(
[
dbc.Label("回测组合策略"),
dcc.Dropdown(
id="portfolio-select",
options=[],
placeholder="Select Portfolio",
),
]
),
html.Br(),
html.Div(
id="submit-btn-control-card",
children=dbc.Button("确认", id="submit-btn", n_clicks=0, className="mb-3", disabled=True),
),
],
id="control-card",
body=True,
style={"margin-top": "45px", "margin-left": "20px", "margin-right": "10px"}

)
return card

# get portfolio list after account selection
@app.callback(
Output('portfolio-select', 'options'),
Input("account-select", "value"),
prevent_initial_call=True
)
def update_dropdown_portfolio_select(account):
return fetch_portfolio_list(account)

# disable submit button before both account and portfolio selected
@app.callback(
Output('submit-btn', 'disabled'),
Input("account-select", "value"),
Input("portfolio-select", "value"),
prevent_initial_call=True
)
def update_btn_availibility(account, portfolio):
if account is None or portfolio is None:
return True
else:
return False

# get account trade history, stock daily data and benchmark daily data and put them into localstore
@app.callback(
Output("account-trade-history", "data"),
Output("stock-day", "data"),
Output("benchmark-day", "data"),
Output("asset", "data"),
State("account-select", "value"),
State("portfolio-select", "value"),
Input("submit-btn", "n_clicks"),
prevent_initial_call=True
)
def output_localstore(account, portfolio, submit_click):
acc = Account(account_cookie=account, portfolio_cookie=portfolio)
df = acc.history_trade.reset_index()
df_tmp = df[['date', 'code', 'price', 'direction', 'amount']].copy()
df_tmp['交易方向'] = np.where(df_tmp['direction'] == 1, '买入', '卖出')
df_tmp['成交价格'] = df_tmp['price'].round(2)
df_tmp['成交数量'] = df_tmp['amount'].abs()
df_tmp.drop(['direction', 'amount', 'price'], axis=1, inplace=True)
df_tmp = df_tmp.rename(columns={'date': '日期', 'code': '股票代码'})
account_trade_history = df_tmp.to_json(date_format='iso', orient='split')

## stock daily data
df_tmp = acc.stock_day.reset_index().copy()
stock_day = df_tmp.to_json(date_format='iso', orient='split')

## benchmark daily data(上证50(000016), 沪深300(000300), 中证500(000905), 科创50(000688))
df_tmp = fetch_index_day(['000016','000300','000905','000688'], acc.start_date, acc.end_date, dataset_type='pandas')
benchmark_day = df_tmp.to_json(date_format='iso', orient='split')

## backtest assets
asset = acc.assets.get('asset').reset_index().to_json(date_format='iso', orient='split')
dataset = {'init_cash':acc.init_cash, 'asset':asset, }
asset_dataset = json.dumps(dataset)
return account_trade_history, stock_day, benchmark_day, asset_dataset

# controls(dropdown) are disabled after submit button clicked, and button name is changed to 'Return'
@app.callback(
Output("submit-btn", "children"),
Output("account-select", "disabled"),
Output("portfolio-select", "disabled"),
Input("submit-btn", "n_clicks"),
prevent_initial_call=True
)
def disable_control_card(submit_click):
return "返回", True, True


以上代码定义了一个函数 generate_control_card(),用于创建并返回一个包含控制元素的卡片组件。这个卡片会在左侧列显示,包含两个下拉框(用于选择回测账户和回测组合策略)和一个确认按钮。下面是代码的逐步解释:

代码详解:

1. 函数定义和描述

  • generate_control_card(): 函数名,创建一个包含控制选项的卡片。
  • return: 表示函数返回值,返回一个包含控件的卡片。

2. 卡片结构

  • dbc.Card(): 创建一个卡片组件。
    • id="control-card":为卡片设置 ID control-card,便于其他代码引用。
    • body=True: 设置卡片的样式,使其外观与 Dash Bootstrap 的卡片格式一致。
    • style: 设置卡片的边距和顶部位置,使其在页面布局中稍微向右偏移。

3. 卡片内容

3.1 下拉框 - 选择账户

  • html.Div() 包裹第一个下拉框,用于选择回测账户。
    • dbc.Label("回测账户"):一个标签 回测账户,放在下拉框上方,用于标识控件的功能。
    • dcc.Dropdown():创建一个下拉选择框。
      • id="account-select":为下拉框指定 ID,便于后续回调函数和 JavaScript 操作。
      • options=fetch_account_list():使用 fetch_account_list() 函数生成的账户列表作为下拉框的可选项。
      • placeholder="Select Account":下拉框的提示文字。

3.2 下拉框 - 选择组合策略

  • 类似于账户选择框,生成一个新的下拉框用于选择回测组合策略。
    • id="portfolio-select":ID 为 portfolio-select
    • options=[]:初始时选项为空,可以在后续逻辑中更新。
    • placeholder="Select Portfolio":显示 Select Portfolio 作为提示文字。

3.3 确认按钮

  • html.Div() 包裹一个按钮。
    • dbc.Button("确认"): 按钮显示文字为 确认
      • id="submit-btn":按钮的 ID,便于控制和事件处理。
      • n_clicks=0:按钮被点击次数,初始化为 0
      • className="mb-3":按钮的底部留出 3 个单位的空白,避免与其他控件紧贴。
      • disabled=True:按钮在初始状态下禁用,后续可能通过回调函数启用。

3.4 样式

  • style={"margin-top": "45px", "margin-left": "20px", "margin-right": "10px"}:卡片的外边距,顶部 45 像素,左侧 20 像素,右侧 10 像素,保证卡片在布局中具有适当的空白边距。

以下是对涉及到的回调函数的详细解释,它们分别用于更新组合选择列表、控制“确认”按钮的可用状态,以及在按下“确认”按钮后将账户交易数据、股票日数据和基准数据存入到客户端本地存储 (dcc.Store) 中。

第一个回调函数:更新组合选择列表

1
2
3
4
5
6
7
8
@app.callback(
Output('portfolio-select', 'options'),
Input("account-select", "value"),
prevent_initial_call=True
)
def update_dropdown_portfolio_select(account):
return fetch_portfolio_list(account)

  1. 触发条件:当 account-select(账户选择下拉框)的值发生变化时触发回调。
  2. 功能:调用 fetch_portfolio_list(account) 函数,从数据库中获取指定账户的组合列表,具体代码这里不列出。更新 portfolio-select(组合选择下拉框)的 options 属性。
  3. 用途:动态加载组合策略的选项,使组合选择与账户选择保持一致。

第二个回调函数:控制“确认”按钮的可用状态

1
2
3
4
5
6
7
8
9
10
11
@app.callback(
Output('submit-btn', 'disabled'),
Input("account-select", "value"),
Input("portfolio-select", "value"),
prevent_initial_call=True
)
def update_btn_availibility(account, portfolio):
if account is None or portfolio is None:
return True
else:
return False
  1. 触发条件account-selectportfolio-select 的值发生变化时触发。
  2. 功能:根据是否选择了有效的账户和组合策略,来设置 submit-btn(确认按钮)的 disabled 属性。
    • 如果账户或组合为空,则返回 True,使按钮不可用。
    • 如果都选择了,则返回 False,使按钮可用。
  3. 用途:防止在未选择账户和组合时按下按钮。

第三个回调函数:获取数据并存入本地存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@app.callback(
Output("account-trade-history", "data"),
Output("stock-day", "data"),
Output("benchmark-day", "data"),
Output("asset", "data"),
State("account-select", "value"),
State("portfolio-select", "value"),
Input("submit-btn", "n_clicks"),
prevent_initial_call=True
)
def output_localstore(account, portfolio, submit_click):
acc = Account(account_cookie=account, portfolio_cookie=portfolio)
df = acc.history_trade.reset_index()
df_tmp = df[['date', 'code', 'price', 'direction', 'amount']].copy()
df_tmp['交易方向'] = np.where(df_tmp['direction'] == 1, '买入', '卖出')
df_tmp['成交价格'] = df_tmp['price'].round(2)
df_tmp['成交数量'] = df_tmp['amount'].abs()
df_tmp.drop(['direction', 'amount', 'price'], axis=1, inplace=True)
df_tmp = df_tmp.rename(columns={'date': '日期', 'code': '股票代码'})
account_trade_history = df_tmp.to_json(date_format='iso', orient='split')

## stock daily data
df_tmp = acc.stock_day.reset_index().copy()
stock_day = df_tmp.to_json(date_format='iso', orient='split')

## benchmark daily data(上证50(000016), 沪深300(000300), 中证500(000905), 科创50(000688))
df_tmp = fetch_index_day(['000016','000300','000905','000688'], acc.start_date, acc.end_date, dataset_type='pandas')
benchmark_day = df_tmp.to_json(date_format='iso', orient='split')

## backtest assets
asset = acc.assets.get('asset').reset_index().to_json(date_format='iso', orient='split')
dataset = {'init_cash':acc.init_cash, 'asset':asset, }
asset_dataset = json.dumps(dataset)

return account_trade_history, stock_day, benchmark_day, asset_dataset
  1. 触发条件:当 submit-btn 被点击时触发。
  2. 参数说明
    • accountportfolio 是从 State 中获取的,即选中的账户和组合策略。
    • submit_click 是点击事件计数器。
  3. 功能:根据选定账户和组合策略,执行以下操作:
    • 创建一个 Account 对象 acc
    • 获取交易历史 acc.history_trade 并重置索引。
    • 生成一个临时数据框 df_tmp,保留必要列,并进行处理:
      • 使用 np.where 判断交易方向(direction 为 1 则是买入,其他情况为卖出)。
      • 将价格保留两位小数,数量取绝对值。
      • 重命名列以适配最终展示格式。
    • 将处理后的数据转换为 JSON 格式 account_trade_history,以便存入 dcc.Store 中。
    • 获取基准指数数据:调用 fetch_index_day 函数,获取上证 50、沪深 300、中证 500 和科创 50 的每日数据,范围从账户开始日期至结束日期。
      • 转换为 JSON:将 df_tmp 转换成 JSON 格式 benchmark_day。
    • 提取资产数据:获取账户资产数据的 JSON 格式,并与账户初始现金一起放入 dataset 字典。
      • 转换为 JSON:将 dataset 转换成 JSON 格式 asset_dataset
    • 返回所有数据的 JSON 格式,将其存储在 dcc.Store 中供前端使用。

第四个回调函数:获取数据后,禁用下拉框的选择功能,并将按钮内容设置为“返回”。

1
2
3
4
5
6
7
8
9
@app.callback(
Output("submit-btn", "children"),
Output("account-select", "disabled"),
Output("portfolio-select", "disabled"),
Input("submit-btn", "n_clicks"),
prevent_initial_call=True
)
def disable_control_card(submit_click):
return "返回", True, True
  1. 触发条件:当 submit-btn 被点击时触发。
  2. 功能
    • 按钮按下后,将两个下拉选择框设为不可用(禁用状态)。并更改按钮的 children 内容为 "返回"

相应的 css 样式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.form-label {
color:#1a94bc;
font-size: 0.75rem;
font-weight: bold;
}

.Select-input{
height: 25px !important;
}

.Select-control{
height: 27px !important;
}

.Select-placeholder{
line-height: 25px !important;
}

.Select-value-label{
height: 27px !important;
display: flex !important;
align-items: center !important;
}

以下是对每个 CSS 类的解释,这些类用于控制表单标签和选择框的样式。

  1. .form-label
  • 目标: 控制表单标签的字体样式。
    • color: #1a94bc;: 标签字体颜色为蓝色 ( #1a94bc )。
    • font-size: 0.75rem;: 设置标签的字体大小为 0.75rem
    • font-weight: bold;: 标签字体加粗,增加视觉上的重要性。
  1. .Select-input
  • 目标: 控制选择框的输入框高度。
    • height: 25px !important;: 设置输入框的高度为 25px,使用 !important 确保该高度不会被其他样式覆盖。
  1. .Select-control
  • 目标: 控制选择框整体的高度。
    • height: 27px !important;: 将整个选择控件的高度设置为 27px,并使用 !important 以确保高度不受其他样式影响。
  1. .Select-placeholder
  • 目标: 控制选择框中占位符文本的行高。
    • line-height: 25px !important;: 设置占位符的行高为 25px,使占位文本在选择框中垂直居中。
  1. .Select-value-label
  • 目标: 控制选择框中已选择标签的样式。
    • height: 27px !important;: 将选择值标签的高度设置为 27px
    • display: flex !important;: 使用 Flexbox 布局,使内容可以在水平方向和垂直方向对齐。
    • align-items: center !important;: 将选择框内已选值的内容垂直居中。

总结

该函数返回一个包含账户选择、策略选择和确认按钮的卡片,整体样式整洁,适用于 Dash Bootstrap 项目中的侧边控制栏。

效果展示

|300