株のシステムトレードをしよう - 1から始める株自動取引システムの作り方

株式をコンピュータに売買させる仕組みを少しずつ作っていきます。できあがってから公開ではなく、書いたら途中でも記事として即掲載して、後から固定ページにして体裁を整える方式で進めていきます。

Backtrader で KABU+ のデータを読めるようにする その5

昨日は元の呼び出し部分をそのままコピーしてきて、 KabuPlusJPCSVData クラスに再実装することで、確かにそのメソッド内で失敗することを確認した。

how-to-make-stock-trading-system.dogwood008.com

今日以降はそれを受けて、実際に呼び出し部で失敗しないように書き換えていく。まず完成形を共有する。

!pip install backtrader
import pandas as pd
import backtrader as bt

path_to_csv = '/content/drive/MyDrive/Project/kabu-plus/japan-stock-prices-2_2020_9143_adjc.csv'
csv = pd.read_csv(path_to_csv)
#############################################################
#    Copyright (C) 2020 dogwood008 (original author: Daniel Rodriguez; https://github.com/mementum/backtrader)
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.
#############################################################

import csv
import itertools
import io
from datetime import date, datetime
from backtrader.utils import date2num
from typing import Any

class KabuPlusJPCSVData(bt.feeds.YahooFinanceCSVData):
    '''
    Parses pre-downloaded KABU+ CSV Data Feeds (or locally generated if they
    comply to the Yahoo formatg)
    Specific parameters:
      - ``dataname``: The filename to parse or a file-like object
      - ``reverse`` (default: ``True``)
        It is assumed that locally stored files have already been reversed
        during the download process
      - ``round`` (default: ``True``)
        Whether to round the values to a specific number of decimals after
        having adjusted the close
      - ``roundvolume`` (default: ``0``)
        Round the resulting volume to the given number of decimals after having
        adjusted it
      - ``decimals`` (default: ``2``)
        Number of decimals to round to
      - ``swapcloses`` (default: ``False``)
        [2018-11-16] It would seem that the order of *close* and *adjusted
        close* is now fixed. The parameter is retained, in case the need to
        swap the columns again arose.
    '''
    DATE = 'date'
    OPEN = 'open'
    HIGH = 'high'
    LOW = 'low'
    CLOSE = 'close'
    VOLUME = 'volume'
    ADJUSTED_CLOSE = 'adjusted_close'
    
    params = (
        ('reverse', True),
        ('round', True),
        ('decimals', 2),
        ('roundvolume', False),
        ('swapcloses', False),
        ('headers', True),
        ('header_names', {  # CSVのカラム名と内部的なキーを変換する辞書
            DATE: 'date',
            OPEN: 'open',
            HIGH: 'high',
            LOW: 'low',
            CLOSE: 'close',
            VOLUME: 'volumes',
            ADJUSTED_CLOSE: 'adj_close',
        })
    )

    def _fetch_value(self, values: dict, column_name: str) -> Any:
        '''
        パラメタで指定された変換辞書を使用して、
        CSVで定義されたカラム名に沿って値を取得する。
        '''
        index = self._column_index(self.p.header_names[column_name])
        return values[index]

        
    def _column_index(self, column_name: str) -> int:
        '''
        与えたカラム名に対するインデックス番号を返す。
        見つからなければ ValueError を投げる。
        '''
        return self._csv_headers.index(column_name)

    # copied from https://github.com/mementum/backtrader/blob/0426c777b0abdfafbb0988f5c31347553256a2de/backtrader/feed.py#L666-L679
    def start(self):
        super(bt.feed.CSVDataBase, self).start()

        if self.f is None:
            if hasattr(self.p.dataname, 'readline'):
                self.f = self.p.dataname
            else:
                # Let an exception propagate to let the caller know
                self.f = io.open(self.p.dataname, 'r')

        if self.p.headers and self.p.header_names:
            _csv_reader = csv.reader([self.f.readline()])
            self._csv_headers = next(_csv_reader)

        self.separator = self.p.separator


    def _loadline(self, linetokens):
        while True:
            nullseen = False
            for tok in linetokens[1:]:
                if tok == 'null':
                    nullseen = True
                    linetokens = self._getnextline()  # refetch tokens
                    if not linetokens:
                        return False  # cannot fetch, go away

                    # out of for to carry on wiwth while True logic
                    break

            if not nullseen:
                break  # can proceed

        dttxt = self._fetch_value(linetokens, self.DATE)
        dt = date(int(dttxt[0:4]), int(dttxt[5:7]), int(dttxt[8:10]))
        dtnum = date2num(datetime.combine(dt, self.p.sessionend))

        self.lines.datetime[0] = dtnum
        o = float(self._fetch_value(linetokens, self.OPEN))
        h = float(self._fetch_value(linetokens, self.HIGH))
        l = float(self._fetch_value(linetokens, self.LOW))
        rawc = float(self._fetch_value(linetokens, self.CLOSE))
        self.lines.openinterest[0] = 0.0

        adjustedclose = float(self._fetch_value(linetokens, self.ADJUSTED_CLOSE))
        v = float(self._fetch_value(linetokens, self.VOLUME))

        if self.p.swapcloses:  # swap closing prices if requested
            rawc, adjustedclose = adjustedclose, rawc

        adjfactor = rawc / adjustedclose

        o /= adjfactor
        h /= adjfactor
        l /= adjfactor
        v *= adjfactor

        if self.p.round:
            decimals = self.p.decimals
            o = round(o, decimals)
            h = round(h, decimals)
            l = round(l, decimals)
            rawc = round(rawc, decimals)

        v = round(v, self.p.roundvolume)

        self.lines.open[0] = o
        self.lines.high[0] = h
        self.lines.low[0] = l
        self.lines.close[0] = rawc
        self.lines.volume[0] = v
        self.lines.adjclose[0] = adjustedclose

        return True
from logging import getLogger, StreamHandler, Formatter, DEBUG
# Create a Stratey
class TestStrategyWithLogger(bt.Strategy):
    def _log(self, txt, dt=None):
        ''' Logging function for this strategy '''
        dt = dt or self.datas[0].datetime.date(0)
        self._logger.debug('%s, %s' % (dt.isoformat(), txt))

    def __init__(self, loglevel):
        # Keep a reference to the "close" line in the data[0] dataseries
        self._dataclose = self.datas[0].close
        self._dataadjclose = self.datas[0].adjclose
        self._datavolume = self.datas[0].volume
        self._logger = getLogger(__name__)
        self.handler = StreamHandler()
        self.handler.setLevel(loglevel)
        self._logger.setLevel(loglevel)
        self._logger.addHandler(self.handler)
        self._logger.propagate = False
        self.handler.setFormatter(
                Formatter('[%(levelname)s] %(message)s'))

    def next(self):
        # Simply log the closing price of the series from the reference
        self._log('(Close, Adj. Close, Volume) = ({close:>5.2f}, {adjc:>5.2f}, {vol:>010.2f})'.format(
                  close=self._dataclose[0], adjc=self._dataadjclose[0], vol=self._datavolume[0]))
if __name__ == '__main__':
    cerebro = bt.Cerebro()

    data = KabuPlusJPCSVData(
        dataname=path_to_csv,
        fromdate=datetime(2020, 1, 1),
        todate=datetime(2020, 11, 30),
        reverse=False)

    cerebro.adddata(data)
    # Add a strategy
    IN_DEVELOPMENT = True  # このフラグにより、ログレベルを切り替えることで、本番ではWARN以上のみをログに出すようにする。
    # フラグの切り替えは、環境変数で行う事が望ましいが今は一旦先送りする。
    loglevel = DEBUG if IN_DEVELOPMENT else WARN
    cerebro.addstrategy(TestStrategyWithLogger, loglevel)

    cerebro.run()

出力(株式分割にも対応している)
出力(株式分割にも対応している)

詳細な数値は掲載できないが、株式分割があった10月28日から29日にかけて、終値は半分近くになっているが、調整後終値は同程度の水準である事を確認できる。

明日以降はこの変更内容を説明する。


現在技術書典10で電子書籍発売中です!是非ご覧ください。

techbookfest.org

(C) 2020 dogwood008 禁無断転載 不許複製 Reprinting, reproducing are prohibited.