Python DjangoとMDBで作る株取引ノートシリーズの8つ目の記事です。
今回は入力したデータを表示する簡単なダッシュボードページを作成していきます。
グラフの表示についてはChart.jsを使用していきます。
完成系のイメージです。
※リスクリワード比率とは、損小利大を達成するための数値で、1以上だったら損小利大で、1より小さければ損大利小となる
django querysetをpandas DataFrame化できるdjango-pandasを使用していきます。
requirements.txtに追記をしてpip installしましょう。
django~=3.2.1
django-mdeditor~=0.1.18
Markdown~=3.3.6
# 追加
django-pandas~=0.6.6
pip install -r requirements.txt
URLパターンに追加します。
# note/urls.py
urlpatterns = [
path('', views.TransactionList.as_view(), name='transaction_list'),
path('detail/<int:pk>/', views.TransactionDetail.as_view(), name='detail'),
path('history_create/<int:pk>/', views.HistoryCreate.as_view(), name='history_create'),
path('history_delete/<int:pk>/', views.HistoryDelete.as_view(), name='history_delete'),
path('transaction_create/', views.TransactionCreate.as_view(), name='transaction_create'),
path('transaction_update/<int:pk>/', views.TransactionUpdate.as_view(), name='transaction_update'),
path('dashboard/', views.DashBoard.as_view(), name='dashboard'), # 追加
]
次にヘッダーにもリンクを追加しておきましょう。
<!-- note/templates/note/components/header.html -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/">Trade Note</a>
<button class="navbar-toggler" type="button" data-mdb-toggle="collapse" data-mdb-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/admin">Admin</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'note:transaction_create' %}">
<i class="fa fa-plus"></i>
New
</a>
</li>
<!-- 追加 -->
<li class="nav-item">
<a class="nav-link" href="{% url 'note:dashboard' %}">
<i class="fa fa-chart-line"></i>
Dashboard
</a>
</li>
</ul>
</div>
</div>
</nav>
次にviews.pyの編集です。
計算は後でしますが、とりあえず使用ライブラリをimportするようにしましょう。
# note/views.py
...
# 追加
from django_pandas.io import read_frame
import pandas as pd
import numpy as np
...
class DashBoard(generic.TemplateView):
model = Transaction
template_name = 'note/dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['breadcrumbs_list'] = [{'name': 'Dashboard',
'url': ''}]
return context
次にhtmlファイルを作成します。
<!-- note/templates/note/dashboard.html -->
{% extends "note/base.html" %}
{% load humanize %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header text-white bg-secondary text-center ls-widest font-weight-bold">
Total Benefit
</div>
<div class="card-body">
<h4 class="card-title text-center">
通算損益
</h4>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header text-white bg-secondary text-center ls-widest font-weight-bold">
Risk Reward Ratio
</div>
<div class="card-body">
<h4 class="card-title text-center">
ここにリスクリワード比率
</h4>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9">
<div class="card" style="height:100%">
<div class="card-header text-white bg-primary text-center ls-widest font-weight-bold">
Benefit Transition
</div>
<div class="card-body">
月間損益推移グラフ
</div>
</div>
</div>
<div class="col-md-3">
<div class="card mb-3">
<div class="card-header text-white bg-info text-center ls-widest font-weight-bold">
Average Profit and Loss
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<p class="text-end">Profit : </p>
</div>
<div class="col-md-8">
<h4 class="text-success text-end">
平均利益
</h4>
</div>
</div>
<div class="row mt-2">
<div class="col-md-4">
<p class="text-end">Loss : </p>
</div>
<div class="col-md-8">
<h4 class="text-danger text-end">
平均損失
</h4>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header text-white bg-info text-center ls-widest font-weight-bold">
Win Ratio
</div>
<div class="card-body">
勝率(円グラフ)
</div>
</div>
</div>
</div>
{% endblock %}
とりあえず計算は後にしてガワだけを作っているイメージです。
この時点でこのように表示がされます。
それではさっそく計算をして値を表示をさせていきましょう。
viewを以下のように編集します。
ちょっと長いです。
class DashBoard(generic.TemplateView):
model = Transaction
template_name = 'note/dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['breadcrumbs_list'] = [{'name': 'Dashboard',
'url': ''}]
# Closeしているもののみを評価 ※Closeはidが2
queryset = Transaction.objects.filter(status=2)
# 何もない場合はここで終了
if not queryset:
return context
# pandas DataFrameにクエリセットを変換
df = read_frame(queryset, fieldnames=['date_close', 'result', 'status', 'benefit'])
# ===通算損益の計算=== #
total_benefit = df['benefit'].sum()
context['total_benefit'] = total_benefit
# ===勝率の計算=== #
total_trade_count = len(df)
win_count = len(df[df['result'] == 'Win'])
# 勝ち回数が0の場合は0
win_ratio = round(win_count / total_trade_count, 2) if win_count else 0
lose_ratio = 1 - win_ratio
context['win_ratio'] = win_ratio
context['lose_ratio'] = lose_ratio
# ===平均利益・平均損失の計算 === #
# {'Lose': {'benefit': 値}, 'Win': {'benefit': 値}}という辞書になる
dic = pd.pivot_table(df, values='benefit', columns='result', aggfunc=np.sum).to_dict()
if dic.get('Win'):
total_profit = dic.get('Win')['benefit']
avg_profit = int(round(total_profit / win_count, 0))
else:
avg_profit = 0
if dic.get('Lose'):
lose_count = total_trade_count - win_count
total_loss = dic.get('Lose')['benefit']
avg_loss = int(round(total_loss / lose_count, 0))
else:
avg_loss = 0
context['avg_profit'] = avg_profit
context['avg_loss'] = avg_loss
# === リスクリワード比率の計算 === #
# 一回も勝ってない場合は0とする
risk_reward_ratio = None
if not avg_profit:
risk_reward_ratio = 0
elif avg_profit and avg_loss:
# 平均損失は負の値になっているので絶対値に変換する
risk_reward_ratio = round(avg_profit / abs(avg_loss), 2)
context['risk_reward_ratio'] = risk_reward_ratio
# === 月間の損益推移グラフの素材計算 === #
# クローズ日から月の列を作る
df['date_close'] = pd.to_datetime(df['date_close'])
df['month'] = df['date_close'].dt.strftime('%y-%m')
# pivot集計
df_pivot = pd.pivot_table(df, index='month', values='benefit', aggfunc=np.sum)
# X軸は月の情報、Y軸は損益額
context['month_list'] = [month for month in df_pivot.index.values]
context['benefit_list'] = [val[0] for val in df_pivot.values]
return context
合計損益は単純に合計しているだけですし、勝率はresultがWin
になっているものを数えているだけです。
平均利益と平均損失が少し複雑でpivot集計をして値を算出しています。
まずpivot集計したDataFrameをテストプリントしてみます。
print(pd.pivot_table(df, values='benefit', columns='result', aggfunc=np.sum))
>>>
result Lose Win
benefit -23240 107530
カラムのresultのパターンが入り、valueをbenefitで合計が表示されます。
これをto_dict()すると以下のような辞書になります。
print(pd.pivot_table(df, values='benefit', columns='result', aggfunc=np.sum).to_dict())
>>>
{'Lose': {'benefit': -23240}, 'Win': {'benefit': 107530}}
なのでdic.get('Win')['benefit']
のようにして合計値にアクセスできます。
記録の初期段階ですと値が何もない場合もあるので、if文で分岐をさせています。
if dic.get('Win'):
total_profit = dic.get('Win')['benefit']
avg_profit = round(total_profit / win_count, 0)
リスクリワード比率については、単純に平均利益 ÷ 平均損失を計算するだけですが、損失は負の値にしています。
なのでabsメソッドで絶対値に変換したうえで割り返しています。
最後にグラフデータの計算ですが、まずクローズ日を元に月を表す列を付け加えています。
df['date_close'] = pd.to_datetime(df['date_close'])
df['month'] = df['date_close'].dt.strftime('%y-%m')
ここもテストprintしてみましょう。
print(df)
>>>
date_close result status benefit month
0 2021-11-02 Lose Close -9240 21-11
1 2021-10-19 Lose Close -14000 21-10
2 2021-12-16 Win Close 107530 21-12
このように列が追加されます。
後はmonthをインデックスにしてpivot集計をすれば月ごとの損益合計値が割り出せます。
df_pivot = pd.pivot_table(df, columns='month', values='benefit', aggfunc=np.sum)
print(df_pivot)
>>>
benefit
month
21-10 -14000
21-11 -9240
21-12 107530
あとはリストに変換すればグラフの作成に必要なX軸データとY軸データを用意できます。
# X軸は月の情報、Y軸は損益額
context['month_list'] = [month for month in df_pivot.index.values]
context['benefit_list'] = [val[0] for val in df_pivot.values]
最後に計算したcontextの値を渡してダッシュボードページを完成させましょう。
Djangoテンプレート上でのChart.jsの使用方法は、DjangoでChart.jsを使う方法を参照願います。
長いです。
<!-- note/templates/note/dashboard.html -->
{% extends "note/base.html" %}
{% load humanize %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header text-white bg-secondary text-center ls-widest font-weight-bold">
Total Benefit
</div>
<div class="card-body">
<!-- 変更 -->
{% if total_benefit >= 0 %}
<h4 class="card-title text-center text-primary">+{{ total_benefit|intcomma }} ¥</h4>
{% elif total_benefit < 0 %}
<h4 class="card-title text-center text-danger">{{ total_benefit|intcomma }} ¥</h4>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header text-white bg-secondary text-center ls-widest font-weight-bold">
Risk Reward Ratio
</div>
<div class="card-body">
<!-- 変更 -->
{% if risk_reward_ratio >= 1 %}
<h4 class="card-title text-center text-primary">{{ risk_reward_ratio }}</h4>
{% else %}
<h4 class="card-title text-center text-danger">{{ risk_reward_ratio }}</h4>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9">
<div class="card" style="height:100%">
<div class="card-header text-white bg-primary text-center ls-widest font-weight-bold">
Benefit Transition
</div>
<div class="card-body">
<!-- 変更 -->
<canvas id="lineChart"></canvas>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card mb-3">
<div class="card-header text-white bg-info text-center ls-widest font-weight-bold">
Average Profit and Loss
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<p class="text-end">Profit : </p>
</div>
<div class="col-md-8">
<h4 class="text-success text-end">
<!-- 変更 -->
{{ avg_profit|intcomma }}
</h4>
</div>
</div>
<div class="row mt-2">
<div class="col-md-4">
<p class="text-end">Loss : </p>
</div>
<div class="col-md-8">
<h4 class="text-danger text-end">
<!-- 変更 -->
{{ avg_loss|intcomma }}
</h4>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header text-white bg-info text-center ls-widest font-weight-bold">
Win Ratio
</div>
<div class="card-body">
<!-- 変更 -->
<canvas id="donutChart"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- 追加 -->
{% block extrajs %}
<!-- スクリプトタグは適宜公式サイトからコピーしてください -->
<!-- https://cdnjs.com/libraries/Chart.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.2/chart.min.js" integrity="sha512-tMabqarPtykgDtdtSqCL3uLVM0gS1ZkUAVhRFu1vSEFgvB73niFQWJuvviDyBGBH22Lcau4rHB5p2K2T0Xvr6Q==" crossorigin="anonymous" referrerpolicy="no-referrer">
</script>
<script>
const lineChartCtx = document.getElementById('lineChart').getContext('2d');
// X軸データ
const lineChartLabels = [
{% for month in month_list %}
'{{ month }}',
{% endfor %}
];
const lineChartData = {
labels: lineChartLabels,
datasets: [{
label: 'Benefit',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(54, 162, 235)',
// Y軸データ
data: [
{% for benefit in benefit_list %}
{{ benefit }},
{% endfor %}
],
}]
};
const lineChart = new Chart(lineChartCtx, {
type: 'line',
data: lineChartData,
options: {
responsive: true,
legend: {
display: false
},
scales: {
x: {
grid: {
display: false,
}
},
y: {
grid: {
display: true,
},
},
}
},
});
const donutChartCtx = document.getElementById('donutChart').getContext('2d');
const donutChartLabels=[
'Win','Lose'
]
const donutChartData = {
labels: donutChartLabels,
datasets: [{
label: 'Result Ratio',
data: [{{ win_ratio }},{{ lose_ratio }}],
backgroundColor: [
'rgb(75, 192, 192)',
'rgb(255, 159, 64)',
],
}]
};
const donutChart = new Chart(donutChartCtx, {
type: 'doughnut',
data: donutChartData,
});
</script>
{% endblock %}
このように表示がされるでしょう。