Python DjangoとMDBで作る株取引ノート  ⑧ダッシュボードページの作成

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 %}


このように表示がされるでしょう。



TOPページ