見出し画像

やっぱりPython AIモデル用の学習データ作成プログラムをバージョンアップした編


これまではExcelを使用してAIモデル用の学習データおよび評価データを作成していました。

しかし、手作業による煩雑さから徐々にやる気が低下してしまいました。

そこで、気を取り直して、PythonによるAIモデル用の学習データ作成プログラムを作りました。

詳細は、下記の記事を参照ください。

今回の記事の内容は、前回作成したPythonプログラムのカスタマイズに関するものです。

  • カスタマイズの内容

    • 今後の要素追加を考慮し、拡張性を向上させました

    • 新規要素として一目均衡表、RSI, ストキャスティクスを追加しました

    • 株価データのダウンロード方法をStooqおよびYahoo Financeから実施するように変更しました

株価データのダウンロード方法を変更した理由については、下記の記事を参照ください。

新規要素を追加する方法

新規要素を追加する場合には、Pythonプログラムのソースコードを修正する必要があります。

しかし、修正する内容は最小限となるように工夫しました。

  • 新規要素追加のためのPythonプログラムの修正方法

    1. 新規要素を算出する関数を追加

    2. 変数EnableListに新規要素の名前を追加

    3. 変数FuncListに新規要素の名前、関数名、新規要素の構成パラメータ数を追加

    4. --elmのヘルプメッセージに新規要素を追加(無視しても動作に影響なし)

詳細を下記に記載します。

1. 新規要素を算出する関数を追加

新規要素を算出する関数(下記の例ではFuncXXX)を以下のように追加してください。

def FuncXXX(ColName, Data):
    a = ...
    b = ...

    return pd.concat([Data, a, b], axis = 1)

上記の例では、新規に追加したのはaおよびbであるため、構成パラメータ数2となります。

また、以下の制約を満たす必要があります。

  • 新規要素を算出する関数に対する制約

    • 新規要素はDataFrame型であること

    • 新規要素のカラム名はColNameであること

    • 新規要素を算出する関数を追加する位置はmain関数よりも上であること

2. 変数EnableListに新規要素の名前を追加

下記の例では、新規要素の名前をXXXとしてEnableListの最後に追加しています。

# 各テクニカル分析指標のイネーブル設定
EnableList = [
    'SMA',
    'BB',
    'MACD',
    'ICHIMOKU',
    'XXX'
]

EnableListを最後の要素として追加する場合は、それまで最後の要素だった項目の最後にカンマを追加してください。

上記の例では、'ICHIMOKU'の後にカンマが挿入されています。

また、最後の要素の後にはカンマは不要ですのでご注意ください。

上記の例では、'XXX'が最後の要素となります。

3. 変数FuncListに新規要素の名前、関数名、新規要素の構成パラメータ数を追加

下記の例では、新規要素の名前をXXX, 関数名をFuncXXX, 構成パラメータ数を2としてFuncListの最後に追加しています。

# 関数リスト
# 名前, 関数名, 列数(要素数)
FuncList = [
    # SMA(Short, Middle, Long)
    ['SMA', FuncSMA, 3],

    # Bollinger Band(-2Sig, -Sig, SMA, +Sig, +2Sig)
    ['BB', FuncBB, 5],

    # MACD(MACD, MACDSignal)
    ['MACD', FuncMACD, 2],

    # 一目均衡表(基準線, 転換線, 先行スパン1, 先行スパン2, 遅行スパン)
    ['ICHIMOKU', FuncICHIMOKU, 5],

    # XXX
    ['XXX', FuncXXX, 2]
]

FuncListについても、EnableList同様カンマの挿入にご注意ください。

4. --elmのヘルプメッセージに新規要素を追加(無視しても動作に影響なし)

最後は、動作に影響するものではありませんが、念のための修正となります。

parser.add_argument('--elm', required = False, nargs = '+', help = 'Element to add to training data(Default: SMA BB MACD ICHIMOKU XXX)')

--elmオプションのヘルプメッセージにXXXを追加してください。

以上で、新規要素の追加に伴うPythonプログラムのソースコード修正は完了です。

Pythonプログラムの実行方法

Pythonプログラムの実行方法は、前回の記事で紹介したものと基本的には同じです。

基本的には同じなのですが、--elmオプションに関して微妙に差が生じたため、説明します。

> python filename.py --elm SMA BB
> python filename.py --elm BB SMA

上記の2パターンは、--elmオプションでSMAとBB(ボリンジャーバンド)を選択した状態でPythonプログラムを実行したものです。

違いは、SMAとBBの順序です。

今回のPythonプログラムのバージョンアップにより、--elmオプションで指定する要素の順番が考慮されることになりました。

具体的には、次の通りです。

  • 学習データおよび評価データファイル内のデータの並び

    • "--elm SMA BB"の場合

      • Open, High, Low, Close, SMA5, SMA25, SMA75, BB-2Sig, BB-Sig, BBSMA, BB+Sig, BB+2Sig

    • "--elm BB SMA"の場合

      • Open, High, Low, Close, BB-2Sig, BB-Sig, BBSMA, BB+Sig, BB+2Sig, SMA5, SMA25, SMA75

つまり、--elmで指定した順序通りに構成パラメータが並ぶことになります。

この点にご注意ください。

Pythonプログラムのソースコード

以下に、今回の修正を加えたPythonプログラムのソースコードを記載します。

'''
AIモデル向けに株価データに基づいた学習データを作成します
ラベルの出力形式は二値分類とします

学習データの作成手順
1. StooqおよびYahooから株価データをダウンロード(事前にダウンロードした株価データも使用可能)
2. 学習データの作成

実行方法の一例
    python filename.py
    python filename.py --csv StockData
    python filename.py --elm BB MACD --rnn 3

株価データのフォーマット
    * 株価データのフォーマットは、下記のフォーマットとします
    * 各データ間はカンマ区切りとします
    * 時系列は昇順でも降順でもどちらでも良く、本プログラム内で昇順に並べ替えます

    Date,Open,High,Low,Close,Volume
    2023-01-31,27458.56,27494.17,27302.22,27327.11,745973100.0
    ...
'''
# -*- coding: utf-8 -*-

import pandas as pd
import pandas_datareader.data as web
import datetime
import yfinance as yf
import argparse
import talib as ta


# Remove exception date from stock data
def FuncRemoveExceptionDate(Data):
    TmpData = Data.copy()

    # Exception date list
    ExceptionDateList = [
        '1997-07-20', # 海の日
        '1999-07-20',
        '1999-09-15',
        '1999-09-23',
        '1999-10-11',
        '1999-11-03',
        '1999-11-23',
        '1999-12-23',
        '1999-12-31',
        '2000-01-03',
        '2000-01-10',
        '2000-02-11',
        '2000-03-20',
        '2000-05-03',
        '2000-05-04',
        '2000-05-05',
        '2000-07-20',
        '2000-09-15',
        '2000-10-09',
        '2000-11-03',
        '2000-11-23',
        '2001-01-01',
        '2001-01-02',
        '2001-01-03',
        '2001-01-08',
        '2001-02-12',
        '2001-03-20',
        '2001-04-30',
        '2001-05-03',
        '2001-05-04',
        '2001-07-20',
        '2001-09-24',
        '2001-10-08',
        '2001-11-23',
        '2001-12-24',
        '2001-12-31',
        '2002-01-01',
        '2002-01-02',
        '2002-01-03',
        '2002-01-14',
        '2002-02-11',
        '2002-03-21',
        '2002-04-29',
        '2002-05-03',
        '2002-05-06',
        '2002-09-16',
        '2002-09-23',
        '2002-10-14',
        '2002-11-04',
        '2002-12-23',
        '2002-12-31',
        '2003-01-01',
        '2003-01-02',
        '2003-01-03',
        '2003-01-13',
        '2003-02-11',
        '2003-03-21',
        '2003-04-29',
        '2003-05-05',
        '2003-07-21',
        '2003-09-15',
        '2003-09-23',
        '2003-10-13',
        '2003-11-03',
        '2003-11-24',
        '2003-12-23',
        '2003-12-31',
        '2004-01-01',
        '2004-01-02',
        '2004-01-12',
        '2004-02-11',
        '2004-04-29',
        '2004-05-03',
        '2004-05-04',
        '2004-05-05',
        '2004-07-19',
        '2004-09-20',
        '2004-09-23',
        '2004-10-11',
        '2004-11-03',
        '2004-11-23',
        '2004-12-23',
        '2004-12-31',
        '2005-01-03',
        '2005-01-10',
        '2005-02-11',
        '2005-03-21',
        '2005-04-29',
        '2005-05-03',
        '2005-05-04',
        '2005-05-05',
        '2005-07-18',
        '2005-09-19',
        '2005-09-23',
        '2005-10-10',
        '2005-11-03',
        '2005-11-23',
        '2005-12-23',
        '2006-01-02',
        '2006-01-03',
        '2006-01-09',
        '2006-03-21',
        '2006-05-03',
        '2006-05-04',
        '2006-05-05',
        '2006-07-17',
        '2006-09-18',
        '2006-10-09',
        '2006-11-03',
        '2006-11-23',
        '2009-09-21',
        '2017-07-17',
        '2017-08-11',
        '2017-09-18',
        '2017-10-09',
        '2017-11-03',
        '2017-11-23',
        '2018-01-01',
        '2018-01-02',
        '2018-01-03',
        '2018-01-08',
        '2018-02-12',
        '2018-03-21',
        '2018-04-30',
        '2018-05-03',
        '2018-05-04',
        '2018-07-16', # 海の日
        '2018-09-17',
        '2018-09-24',
        '2018-10-08',
        '2018-11-23',
        '2018-12-24',
        '2018-12-31',
        '2020-10-01' # 東証システム障害により全銘柄の売買を終日停止
    ]

    for TmpDay in ExceptionDateList:
        TmpData = TmpData[TmpData.index != TmpDay]

    return TmpData


# Stock data download from Stooq
def FuncDLStockDataStooq(Symbol, StartDate, EndDate):
    # データ(行単位)削除フラグ
    RemoveDateFlag = False

    # シンボル変換
    if 'N225' == Symbol:
        TmpSymbol = '^NKX'
        RemoveDateFlag = True
    elif 'TOPIX' == Symbol: TmpSymbol = '^TPX'
    elif 'DOW' == Symbol: TmpSymbol = '^DJI'
    elif 'SP500' == Symbol: TmpSymbol = '^SPX'
    elif 'NAS' == Symbol: TmpSymbol = '^NDQ'
    #elif 'FTSE' == Symbol: TmpSymbol = '^UK100' # NG
    #elif 'USD' == Symbol: TmpSymbol = 'USDJPY' # NG
    #elif 'EUR' == Symbol: TmpSymbol = 'EURJPY' # NG
    else:
        TmpSymbol = Symbol[0] + '.JP'
        RemoveDateFlag = True

    # Stooqから株価データをダウンロード
    Data = web.DataReader(TmpSymbol, 'stooq', StartDate, EndDate)

    # インデックス(日付け)を昇順に並べ替え
    Data = Data.sort_index()
    
    # Volumeの列を削除
    if 'Volume' in Data.columns.values: Data = Data.drop('Volume', axis = 1)

    # 例外の日付けの行を削除
    if RemoveDateFlag: Data = FuncRemoveExceptionDate(Data)

    # カラム(列)名を変更
    if not ('N225' == Symbol or type(Symbol) == list):
        Data = Data.rename(
            columns = {
                'Open': Symbol,
                'High': Symbol,
                'Low': Symbol,
                'Close': Symbol
            })

    return Data


# Stock data download from Yahoo
def FuncDLStockDataYahoo(Symbol, StartDate, EndDate):
    # データ(行単位)削除フラグ
    RemoveDateFlag = False

    # シンボル変換
    if 'N225' == Symbol:
        TmpSymbol = '^N225'
        RemoveDateFlag = True
    #elif 'TOPIX' == Symbol: TmpSymbol = '998405' # NG
    elif 'DOW' == Symbol: TmpSymbol = '^DJI'
    elif 'SP500' == Symbol: TmpSymbol = '^GSPC'
    elif 'NAS' == Symbol: TmpSymbol = '^IXIC'
    elif 'FTSE' == Symbol: TmpSymbol = '^FTSE'
    elif 'HSI' == Symbol: TmpSymbol = '^HSI' # 香港ハンセン指数
    elif 'SHA' == Symbol: TmpSymbol = '000001.SS' # 上海総合指数
    elif 'VIX' == Symbol: TmpSymbol = '^VIX'
    elif 'USD' == Symbol: TmpSymbol = 'USDJPY=X'
    elif 'EUR' == Symbol: TmpSymbol = 'EURJPY=X'
    elif 'CNY' == Symbol: TmpSymbol = 'CNYJPY=X' # 元(中国)
    elif 'IRX' == Symbol: TmpSymbol = '^IRX' # 米13週国債
    elif 'TNX' == Symbol: TmpSymbol = '^TNX' # 米10年国債
    else:
        TmpSymbol = Symbol[0] + '.T'
        RemoveDateFlag = True

    # Yahooは最終日が1日前にズレるため、補正が必要
    EndDate4Yahoo = (pd.to_datetime(EndDate) + datetime.timedelta(days = 1)).strftime('%Y-%m-%d')

    # Yahooから株価データをダウンロード
    Data = yf.download(TmpSymbol, StartDate, EndDate4Yahoo)

    # Adj Closeの列を削除
    Data = Data.drop('Adj Close', axis = 1)

    # Volumeの列を削除
    if 'Volume' in Data.columns.values: Data = Data.drop('Volume', axis = 1)

    # 例外の日付けの行を削除
    if RemoveDateFlag: Data = FuncRemoveExceptionDate(Data)

    # カラム(列)名を変更
    if not ('N225' == Symbol or type(Symbol) == list):
        Data = Data.rename(
            columns = {
                'Open': Symbol,
                'High': Symbol,
                'Low': Symbol,
                'Close': Symbol
            })

    return Data


# Stock data download
def FuncDLStockData(Symbol, StartDate, EndDate):
    # Stooqから株価データをダウンロード
    DataStooq = FuncDLStockDataStooq(Symbol, StartDate, EndDate)

    # Yahooから株価データをダウンロード
    DataYahoo = FuncDLStockDataYahoo(Symbol, StartDate, EndDate)

    # Yahooの株価データを基準に、Stooqの株価データを合成
    Data = pd.concat([DataYahoo, DataStooq])

    # 重複したインデックス(日付け)に対して最初のデータを残す
    Data = Data[~Data.index.duplicated(keep = 'first')]

    # インデックス(日付け)を昇順に並べ替え
    Data = Data.sort_index()

    return Data


# SMA
def FuncSMA(ColName, Data):
    RefCol = 'Close'

    WinShort: int = 5
    WinMiddle: int = 25
    WinLong: int = 75

    SMA1 = Data[RefCol].rolling(window = WinShort).mean().to_frame(ColName)
    SMA2 = Data[RefCol].rolling(window = WinMiddle).mean().to_frame(ColName)
    SMA3 = Data[RefCol].rolling(window = WinLong).mean().to_frame(ColName)

    return pd.concat([Data, SMA1, SMA2, SMA3], axis = 1)


# Bollinger Band
def FuncBB(ColName, Data):
    RefCol = 'Close'

    WinBB: int = 20

    SMABB = Data[RefCol].rolling(window = WinBB).mean()
    StdDevBB = Data[RefCol].rolling(window = WinBB).std(ddof = 0)

    BBM2Sig = (SMABB - 2 * StdDevBB).to_frame(ColName)
    BBM1Sig = (SMABB - StdDevBB).to_frame(ColName)
    BBSMA = SMABB.to_frame(ColName)
    BBP1Sig = (SMABB + StdDevBB).to_frame(ColName)
    BBP2Sig = (SMABB + 2 * StdDevBB).to_frame(ColName)

    return pd.concat([Data, BBM2Sig, BBM1Sig, BBSMA, BBP1Sig, BBP2Sig], axis = 1)


# MACD
def FuncMACD(ColName, Data):
    RefCol = 'Close'

    SpanShort: int = 12
    SpanLong: int = 26
    WinSignal: int = 9

    EMAShort = Data[RefCol].ewm(span = SpanShort).mean()
    EMALong = Data[RefCol].ewm(span = SpanLong).mean()

    # MACD
    MACD = (EMAShort - EMALong).to_frame(ColName)

    # MACD Signal
    MACDSig = MACD[ColName].rolling(window = WinSignal).mean().to_frame(ColName)

    return pd.concat([Data, MACD, MACDSig], axis = 1)


# 一目均衡表
def FuncICHIMOKU(ColName, Data, NoLagFlag = False):
    HighCol = 'High'
    LowCol = 'Low'
    CloseCol = 'Close'

    WinBase: int = 26
    WinConv: int = 9
    Span1Shift: int = 25 # 26?
    WinSpan2: int = 52
    Span2Shift: int = 25 # 26?
    DelayShift: int = -25 # -26?

    # 基準線
    MaxBaseLine = Data[HighCol].rolling(WinBase).max()
    MinBaseLine = Data[LowCol].rolling(WinBase).min()

    BaseLineData = ((MaxBaseLine + MinBaseLine) / 2).to_frame(ColName)

    # 転換線
    MaxConvLine = Data[HighCol].rolling(WinConv).max()
    MinConvLine = Data[LowCol].rolling(WinConv).min()

    ConvLineData = ((MaxConvLine + MinConvLine) / 2).to_frame(ColName)

    # 先行スパン1
    Span1Data = ((BaseLineData[ColName] + ConvLineData[ColName]) / 2).shift(Span1Shift).to_frame(ColName)

    # 先行スパン2
    MaxSpan2Line = Data[HighCol].rolling(WinSpan2).max()
    MinSpan2Line = Data[LowCol].rolling(WinSpan2).min()

    Span2Data = ((MaxSpan2Line + MinSpan2Line) / 2).shift(Span2Shift).to_frame(ColName)

    # 遅行スパン
    LagginSpanData = Data[CloseCol].shift(DelayShift).to_frame(ColName)

    # 遅行スパンのNaNに最後の終値をコピー
    LastCloseRow: int =  len(LagginSpanData) + DelayShift - 1
    for i in range(len(LagginSpanData) + DelayShift, len(LagginSpanData)):
        LagginSpanData.iat[i, 0] = LagginSpanData.iat[LastCloseRow, 0]

    if NoLagFlag:
        # 遅行スパンなし
        return pd.concat([Data, BaseLineData, ConvLineData, Span1Data, Span2Data], axis = 1)
    else:
        # 遅行スパンあり
        return pd.concat([Data, BaseLineData, ConvLineData, Span1Data, Span2Data, LagginSpanData], axis = 1)


# 一目均衡表 遅行スパンなし
def FuncICHIMOKUNoLag(ColName, Data):
    return FuncICHIMOKU(ColName, Data, True)


# RSI
def FuncRSI(ColName, Data):
    RefCol = 'Close'

    SpanRSI: int = 14
    WinSignal: int = 9

    # RSI
    RSI = ta.RSI(Data[RefCol], timeperiod = SpanRSI).to_frame(ColName)

    # RSI Signal
    RSISig = RSI[ColName].rolling(window = WinSignal).mean().to_frame(ColName)

    return pd.concat([Data, RSI, RSISig], axis = 1)


# Stochastics
def FuncSTOCH(ColName, Data):
    HighCol = 'High'
    LowCol = 'Low'
    CloseCol = 'Close'

    WinK: int = 9
    WinD: int = 3
    WinSlowD: int = 3

    # %K
    MaxData = Data[HighCol].rolling(window = WinK).max()
    MinData = Data[LowCol].rolling(window = WinK).min()
    KData = 100 * (Data[CloseCol] - MinData) / (MaxData - MinData)
    KData = KData.to_frame(ColName)

    # %D
    DData = KData[ColName].rolling(window = WinD).mean().to_frame(ColName)

    # Slow%D
    SlowDData = DData[ColName].rolling(window = WinSlowD).mean().to_frame(ColName)

    return pd.concat([Data, KData, DData, SlowDData], axis = 1)


# ラベルを作成(二値分類用)
def FuncLabel2Class(Data):
    # Closeの列を抽出
    TmpClose = Data['Close']

    if 0 == len(TmpClose.dtypes):
        # SeriesをDataFrameに変換
        TmpClose = TmpClose.to_frame('Label')
    else:
        # カラム(列)名を変更
        TmpClose = TmpClose.rename(
            columns = {
                'Close': 'Label'
            })

    # 最後のCloseの列を抽出
    TmpClose = TmpClose.iloc[:, -1]

    # TmpCloseをコピー
    TmpTmpClose = TmpClose.copy()

    # TmpTmpCloseを上方向に1行シフト
    TmpTmpClose = TmpTmpClose.shift(-1)

    # 翌営業日(TmpTmpClose)と当日(TmpClose)の終値の差分を抽出
    # NaNの情報が埋もれてしまうのを避けるため、ここで一旦NaNを削除
    LabelData = (TmpTmpClose - TmpClose).dropna()

    # 終値の差分が0以上ならラベル1、それ以外はラベル0
    # 上記でNaNを削除しないと、ここでNaNがFalseに変換されてしまう
    LabelData = (0 <= LabelData) * 1

    # ラベルデータを最も左の列として結合
    OutData = pd.concat([Data, LabelData], axis = 1)

    # NaNを含む行を削除
    OutData = OutData.dropna()

    return OutData


def main():
    # コマンドライン引数の処理
    parser = argparse.ArgumentParser(description = 'Stock data to Training data tool for AI model')
    parser.add_argument('--csv', required = False, help = 'Stock data file name(csv format)')
    parser.add_argument('--code', required = False, nargs = 1, help = 'Stock code number')
    parser.add_argument('--dl', required = False, help = 'csv file name(Download stock data to csv file)')
    parser.add_argument('--elm', required = False, nargs = '+', help = 'Element to add to training data(Option: SMA BB MACD ICHIMOKU RSI STOCH TOPIX DOW SP500 NAS FTSE HSI SHA VIX USD EUR CNY IRX TNX)')
    parser.add_argument('--rnn', required = False, type = int, default = 1, help = 'Repeat parameter for RNN(Default: 1)')
    parser.add_argument('--s', required = False, default = '1986-01-01', help = 'Start date(Default: 1986-01-01)')
    parser.add_argument('--e', required = False, default = '2024-01-31', help = 'End date(Default: 2024-01-31)')
    parser.add_argument('--vnum', required = False, type = int, default = 250, help = 'Number of rows for validation data(Default: 250)')
    parser.add_argument('--t', required = False, default = 'training.csv', help = 'Training data file name(Default: training.csv)')
    parser.add_argument('--v', required = False, default = 'validation.csv', help = 'Validation data file name(Default: validation.csv)')
    parser.add_argument('--nolabel', required = False, action = 'store_true', help = 'No label output mode(Default output file: nolabel.csv)')
    parser.add_argument('nolabelfile', nargs = '?', default = 'nolabel.csv')
    parser.add_argument('--nostd', required = False, action = 'store_true', help = 'No standardization(Default: Excecute standardization)')
    parser.add_argument('--debug', required = False, action = 'store_true', help = 'Debug mode')

    args = parser.parse_args()


    # 指定期間の表示
    DayStart = pd.to_datetime(args.s).strftime('%Y-%m-%d')
    DayEnd = pd.to_datetime(args.e).strftime('%Y-%m-%d')

    print('Start day:', DayStart)
    print('End day:', DayEnd)


    # 株価データの取得先を選択(csv file or Website)
    if args.csv:
        print('csv file:', args.csv)

        # csvファイルのデータをDataFrameに入力
        StockData = pd.read_csv(args.csv, encoding = 'utf-8')

        # DataFrameのインデックスをDateに変更し、オリジナルのDataFrameも更新
        StockData.set_index('Date', inplace = True)

        # 指定期間のみ抽出
        StockData = StockData[DayStart : DayEnd]

        # Volumeの列を削除
        if 5 == len(StockData.columns): StockData = StockData.drop('Volume', axis = 1)

        # 日付けを昇順に並べ替え(csvファイルによる株価データの使用を想定)
        StockData = StockData.sort_index(ascending = True)
    else:
        if not args.code:
            # 日経平均株価をダウンロード
            StockData = FuncDLStockData('N225', DayStart, DayEnd)
        else:
            # --codeオプションで指定された銘柄の株価をダウンロード
            StockData = FuncDLStockData(args.code, DayStart, DayEnd)


    # 学習データの作成処理
    if args.dl:
        print('Output stock data to', args.dl)

        # 株価データをcsvファイルに出力
        StockData.to_csv(args.dl)
    else:
        # 各テクニカル分析指標のイネーブル設定
        EnableList = []

        if args.elm: EnableList = args.elm

        print('Training data element:', ', '.join(EnableList))


        # 関数リスト
        # 名前, 関数名, 列数(要素数)
        FuncList = [
            # SMA(Short, Middle, Long)
            ['SMA', FuncSMA, 3],

            # Bollinger Band(-2Sig, -Sig, SMA, +Sig, +2Sig)
            ['BB', FuncBB, 5],

            # MACD(MACD, MACDSignal)
            ['MACD', FuncMACD, 2],

            # 一目均衡表(基準線, 転換線, 先行スパン1, 先行スパン2, 遅行スパン)
            ['ICHIMOKU', FuncICHIMOKU, 5],

            # 一目均衡表(基準線, 転換線, 先行スパン1, 先行スパン2)
            ['ICHIMOKUNOLAG', FuncICHIMOKUNoLag, 4],

            # RSI(RSI)
            ['RSI', FuncRSI, 2],

            # Stochastics(K, D, SlowD)
            ['STOCH', FuncSTOCH, 3]
        ]


        # ダウンロードリスト
        # 名前, 関数名, 列数(要素数)
        DlList = [
            # TOPIX
            ['TOPIX', FuncDLStockDataStooq, 4],

            # DOW
            ['DOW', FuncDLStockData, 4],

            # S&P500
            ['SP500', FuncDLStockData, 4],

            # NASDAQ
            ['NAS', FuncDLStockData, 4],

            # VIX
            ['VIX', FuncDLStockDataYahoo, 4],

            # FTSE100
            ['FTSE', FuncDLStockDataYahoo, 4],

            # 香港ハンセン指数
            ['HSI', FuncDLStockDataYahoo, 4],

            # 上海総合指数
            ['SHA', FuncDLStockDataYahoo, 4],

            # USDJPY
            ['USD', FuncDLStockDataYahoo, 4],

            # EURJPY
            ['EUR', FuncDLStockDataYahoo, 4],

            # 元(中国)
            ['CNY', FuncDLStockDataYahoo, 4],

            # 米13週国債
            ['IRX', FuncDLStockDataYahoo, 4],

            # 米10年国債
            ['TNX', FuncDLStockDataYahoo, 4]
        ]


        # 学習データに要素を追加
        I_Name: int = 0
        I_Func: int = 1
        I_ENum: int = 2

        for EID in EnableList:
            # 関数を実行
            for Func in FuncList:
                if EID == Func[I_Name]:
                    StockData = Func[I_Func](Func[I_Name], StockData)

            # 株価データのダウンロードを実行
            for Dl in DlList:
                if EID == Dl[I_Name]:
                    TmpData = Dl[I_Func](EID, DayStart, DayEnd)

                    # StockDataを基準に、ダウンロードした株価データを合成
                    # 欠損値は前営業日の値をコピー
                    # ★ 1日目の欠損値は前日の値をコピーできないため削除される
                    # ★ 削除を避けるには--sオプションで開始日を前に調整すること
                    StockData = StockData.join(TmpData).ffill()


        # RNN向けに指定分だけ各データを横に並べる処理
        RNNNum = int(args.rnn)

        # 出力用のDataFrame変数
        OutData = StockData.copy()

        if 1 < RNNNum:
            print('RNN params:', RNNNum)

        for i in range(RNNNum - 1):
            # StockDataをコピー
            TmpData = StockData.copy()

            # TmpDataを上方向にi + 1行シフト(i = 0, 1, ...)
            TmpData = TmpData.shift(-(i + 1))

            # OutDataの右側に結合
            OutData = pd.concat([OutData, TmpData], axis = 1)

        # NaNを含む行を削除
        OutData = OutData.dropna()


        # ラベルを作成する処理
        if not args.nolabel:
            OutData = FuncLabel2Class(OutData)
        else:
            print('No label output!!')


        # 各データを標準化する処理
        if not args.nostd:
            # 標準化用のDataFrame変数
            TmpOutData = OutData.copy()

            # カラム(列)名を変更
            TmpOutData = TmpOutData.rename(
                columns = {
                    'Open': 'Data',
                    'High': 'Data',
                    'Low': 'Data',
                    'Close': 'Data'
                })

            # 平均値と標準偏差を格納するためのDataFrame
            StdData = pd.DataFrame()

            # 標準偏差が0となるのを避けるため、結果に影響しないであろう小さな値を加算
            StdZero: float = 1.0e-15

            # 日経平均株価に対する平均と標準偏差を算出
            StdData['MeanData'] = TmpOutData['Data'].mean(axis = 1).to_frame('MeanData')
            StdData['StdData'] = TmpOutData['Data'].std(ddof = 0, axis = 1).to_frame('StdData') + StdZero

            # イネーブルリストに対する平均と標準偏差を算出
            for EID in EnableList:
                # FuncList参照
                for Func in FuncList:
                    if EID == Func[I_Name]:
                        StdData['Mean' + EID] = TmpOutData[EID].mean(axis = 1).to_frame('Mean' + EID)
                        StdData['Std' + EID] = TmpOutData[EID].std(ddof = 0, axis = 1).to_frame('Std' + EID) + StdZero

                # DlList参照
                for Dl in DlList:
                    if EID == Dl[I_Name]:
                        StdData['Mean' + EID] = TmpOutData[EID].mean(axis = 1).to_frame('Mean' + EID)
                        StdData['Std' + EID] = TmpOutData[EID].std(ddof = 0, axis = 1).to_frame('Std' + EID) + StdZero

            # 各列に対して標準化を実施
            ColNum: int = 0 # 列番号

            for i in range(RNNNum):
                for j in range(4):
                    OutData.iloc[:, ColNum] = (OutData.iloc[:, ColNum] - StdData['MeanData']) / StdData['StdData']

                    ColNum += 1

                for EID in EnableList:
                    # FuncList参照
                    for Func in FuncList:
                        if EID == Func[I_Name]:
                            for j in range(Func[I_ENum]):
                                OutData.iloc[:, ColNum] = (OutData.iloc[:, ColNum] - StdData['Mean' + EID]) / StdData['Std' + EID]

                                ColNum += 1

                    # DlList参照
                    for Dl in DlList:
                        if EID == Dl[I_Name]:
                            for j in range(Dl[I_ENum]):
                                OutData.iloc[:, ColNum] = (OutData.iloc[:, ColNum] - StdData['Mean' + EID]) / StdData['Std' + EID]

                                ColNum += 1
        else:
            print('No standardization!!')


        # Neural Network Console用のヘッダを設定
        # ラベルありなし用ヘッダ数を調整
        HeaderAdj: int = 1
        if args.nolabel: HeaderAdj = 0

        # 説明変数用のヘッダを用意
        NNCHeader = 'x__' + pd.Series(range(0, len(OutData.columns) - HeaderAdj), dtype = 'str')

        # 二値分類向け目的変数(ラベル)用のヘッダを追加
        if not args.nolabel:
            NNCHeader[len(NNCHeader)] = 'y:label;D;U'

        # 学習データのヘッダを上書き
        if not args.debug: OutData.columns = [NNCHeader]

        # データの分割処理
        if not args.nolabel:
            if len(OutData) < args.vnum:
                print('\nError!!')
                print('Too few rows of data:', len(OutData))
                print('Please adjust with --vnum option or --s and --e option')
            else:
                # 学習データをトレーニングデータとバリデーションデータに分割
                print('Number of rows for validation data:', args.vnum)

                TrainingData = OutData[: -args.vnum]

                print('Training data period:', TrainingData.index[0], '-', TrainingData.index[-1])

                ValidationData = OutData[len(OutData) - args.vnum :]

                print('Validation data period:', ValidationData.index[0], '-', ValidationData.index[-1])
        else:
            # ラベル無しの場合はデータを分割しない
            print('No label data period:', OutData.index[0], '-', OutData.index[-1])


        # Debug mode
        if args.debug:
            if not args.nolabel:
                print('\nTraining data')
                print(TrainingData)

                print('\nValidation data')
                print(ValidationData)
            else:
                print('\nNo label data')
                print(OutData)


        # 各種データをcsvファイルに出力
        # ただし、Debug mode時はファイル出力しない
        if not args.debug:
            if not args.nolabel:
                if args.vnum < len(OutData):
                    # トレーニングデータをcsvファイルに出力
                    print('Training data csv file:', args.t)
                    TrainingData.to_csv(args.t, index = False, float_format = '%.4f')

                    # バリデーションデータをcsvファイルに出力
                    print('Validation data csv file:', args.v)
                    ValidationData.to_csv(args.v, index = False, float_format = '%.4f')
            else:
                # ラベル無しデータをcsvファイルに出力
                print('No label data csv file:', args.nolabelfile)
                OutData.to_csv(args.nolabelfile, index = False, float_format = '%.4f')


    print('\nDone.')


if __name__ == '__main__':
    main()

RSIの算出方法について

上記のPythonプログラムでは、RSIを算出するために「TA-Lib」ライブラリを使用しています。

当初は、「TA-Lib」ライブラリを使用することなくRSIの算出を試みたのですが、なぜか、HYPER SBI 2で表示させたRSIの数値と一致しませんでした。

その時のPythonプログラムのソースコードを下記に示します。

# RSI
def FuncRSI(ColName, Data):
    RefCol = 'Close'

    SpanRSI: int = 14
    WinSignal: int = 9

    # 株価の差分
    DiffData = Data[RefCol].diff()

    # 値上がり合計
    UpData = DiffData.where(0 < DiffData, 0) # 正のみ抽出、NaNは0
    UpData = UpData.ewm(span = SpanRSI, adjust = False).mean()

    # 値下がり合計
    DownData = DiffData.where(DiffData < 0, 0).abs() # 負のみ抽出、NaNは0, 絶対値
    DownData = DownData.ewm(span = SpanRSI, adjust = False).mean()

    # RSI
    RSI = (UpData / (UpData + DownData)) * 100
    RSI = RSI.to_frame(ColName)

    # RSI Signal
    RSISig = RSI[ColName].rolling(window = WinSignal).mean().to_frame(ColName)

    return pd.concat([Data, RSI, RSISig], axis = 1)

デバッグ中に気になったのは、ewm関数のadjustパラメータをTrueにしても、Falseにしても結果が変わらなかった点です。

原因をアレコレ調べていたところで「TA-Lib」ライブラリの存在を知りました。

「TA-Lib」ライブラリを試したところ、数値がHYPER SBI 2のそれと一致したので、採用することにしました。


一目均衡表の遅行スパンに関する取扱いについて(2024年4月7日)

下記の記事にて新たに発見された、一目均衡表の遅行スパンに関する問題に対応するため、Pythonプログラムをバージョンアップしました。

バージョンアップした内容は、下記の通りです。

  • バージョンアップ内容

    • 学習データおよび評価データから全ての遅行スパンを削除する機能を追加

    • 遅行スパンのデータが欠落している箇所に対して最後の終値をコピーする機能を追加

    • 上記の機能を含め、選択可能とした

学習データおよび評価データから全ての遅行スパンを削除する場合は--elmオプションにてICHIMOKUNOLAGを指定してください。

また、遅行スパンのデータが欠落している箇所に対して最後の終値をコピーする場合は、--elmオプションにてICHIMOKUを指定してください。

--elmオプションで特に何も指定しない(つまり、デフォルトで使用する)場合は、遅行スパンのデータが欠落している箇所に対して最後の終値をコピーする機能が有効になります。


学習済みAIモデルの推論に使用するデータの出力機能を追加しました(2024年4月11日)

Neural Network Consoleで学習させたAIモデルに推論をさせるためのデータを出力する機能をPythonプログラムに追加しました。

上記ソースコードを保存したファイル名をfilename.pyとした場合、Pythonプログラムの実行方法は以下の通りです。

> python filename.py --s 2023-10-7 --e 2024-3-31 --nolabel outdata.csv
Start day: 2023-10-07
End day: 2024-03-31
[*********************100%%**********************]  1 of 1 completed
Training data element: SMA, BB, MACD, ICHIMOKU, RSI, STOCH
No label output!!
No label data period: 2024-02-01 00:00:00 - 2024-03-29 00:00:00
No label data csv file: outdata.csv

Done.

--nolabeオプションを指定すると、学習済みAIモデルの推論に使用するデータを出力することができます。

上記の実行例では、推論用のデータを保存するファイル名としてoutdata.csvを指定しています。

outdata.csvを指定しなかった場合は、デフォルトであるnolabel.csvファイルに推論用のデータが保存されます。

注意点として、上記の実行例では、--sおよび--eオプションで取得データの期間を2023年10月7日から2024年3月31日としています。

しかし、推論に使用するデータの期間は2024年2月1日から2024年3月29日となっています。

この差分が生まれる理由は、SMA(単純移動平均)の最長期間が75日であるため、前半の74日分のデータが使用できないからです。

また、2024年3月30日と31日は土日で、株式市場が休場のため株価データが存在しません。


学習データおよび評価データにダウ、S&P500, ナスダックを追加しました(2024年4月17日)

学習データおよび評価データを作成するPythonプログラムにダウ、S&P500, ナスダックの各指数を出力する機能を追加しました。

バージョンアップした内容は、下記の通りです。

  • バージョンアップ内容

    • ダウ、S&P500, ナスダックの各指数を出力する機能を追加

    • 単純に日経平均株価のみを出力する機能を追加(おまけ)

ダウ、S&P500, ナスダックの各指数は、日経平均株価と同様に、StooqおよびYahooからダウンロードしたデータを組み合わせて作成しています。

ダウ、S&P500, ナスダックの各指数を学習データおよび評価データに加える場合は、--elmオプションで次のように指定します。

> python filename.py --elm DOW SP500 NAS

ここで、注意すべき点が一つあります。

日本とアメリカの株式市場では、休場する日が異なります。

このため、過去の株価データをダウンロードした場合に、日経平均株価のデータは取得できても、ダウ、等のアメリカ市場の株価データは取得できない日があります。

今回作成したPythonプログラムでは、アメリカ市場の株価データが取得できない日は前日の株価データをコピーする仕様としました。

一方で、日本の市場が休場、かつ、ダウ、等のアメリカ市場の株価データが取得できた日は、株価データを捨てる仕様としました。

また、日経平均株価のみを出力する場合は、--elmオプションで定義されていない文字列を与えます。

2024年4月17日現在、--elmオプションで定義されている文字列は以下の通りです。

  • --elmオプションで定義されている文字列

    • SMA, BB, MACD, ICHIMOKU, RSI, STOCH, DOW, SP500, NAS

今後、--elmオプションで指定する文字列が追加された場合は、その文字列も避けてください。

例えば、下記のように実行します。

> python filename.py --elm NONE

NONEは--elmオプションに定義されていないので、日経平均株価のローソク足データのみが学習データおよび評価データとして出力されます。

現状、--elmオプションでNONEの文字列を使用する予定はないですし、今後もできるだけ使用しないようにするつもりです。


学習データおよび評価データに東証株価指数、FTSE100種総合株価指数, 香港ハンセン株価指数、上海総合指数、VIX, 為替(ドル円、ユーロ円、元円), 米国債金利(13週、10年)を追加しました(2024年4月24日)

学習データおよび評価データを作成するPythonプログラムに東証株価指数、FTSE100種総合株価指数, 香港ハンセン株価指数、上海総合指数、VIX, 為替(ドル円、ユーロ円、元円), 米国債金利(13週、10年)を出力する機能を追加しました。

これらは全て--elmオプションで指定します。

--elmオプションで指定可能な項目が増えたので、以下に指定可能な指数、等の種類と指定する場合の名前を記載します。

  • --elmオプションで指定可能な指数、等の種類と名前

    • 日経平均株価(デフォルトのため指定不要)

      • 移動平均(5, 25, 75日): SMA

      • ボリンジャーバンド: BB

      • MACD: MACD

      • 一目均衡表: ICHIMOKU

      • RSI: RSI

      • ストキャスティクス: STOCH

    • 東証株価指数: TOPIX

    • ダウ平均株価: DOW

    • S&P500: SP500

    • ナスダック総合指数: NAS

    • FTSE100種総合株価指数: FTSE

    • 香港ハンセン株価指数: HSI

    • 上海総合指数: SHA

    • VIX指数: VIX

    • ドル円: USD

    • ユーロ円: EUR

    • 元円: CNY

    • 米13週国債金利: IRX

    • 米10年国債金利: TNX

今回のPythonプログラムに対するバージョンアップに伴い、--elmオプションのデフォルト設定を見直し、全てを--elmオプションで明示的に指定するようにしました。

つまり、--elmオプションを指定せずにPythonプログラムを実行した場合は、日経平均株価のみが使用されることになります。

また、各指数、等をダウンロードする参照先ですが、東証株価指数のみStooqで、それ以外はYahooとなっています。

ダウンロード先がそれぞれ一つとなっている理由は、そこからしかダウンロードできなかったからです。

最後に、ダウンロードされた各指数、等に対するデータ処理方法は、以下の通りです。

  • ダウンロードされた各指数、等に対するデータ処理方法

    • 日経平均株価が存在しない日のデータは捨てる

    • 日経平均株価が存在するのに各指数、等が存在しない場合は前日の値をコピーする


学習データおよび評価データとして日経平均株価以外の株価を指定する機能を追加しました(2024年6月2日)

これまでは、日経平均株価(+ ○○)という組み合わせのみに対応していましたが、銘柄(+ ○○)という組み合わせにも対応しました。

例えば、ファーストリテイリング(9983)を指定したい場合は、下記のように実行します。

> python filename.py --code 9983

また、銘柄の過去データをダウンロードする場合は、下記のように実行します。

> python filename.py --code 9983 --s 2023-6-1 --e 2024-5-31 --dl 9983.csv

上記の実行例では、2023年6月1日から2024年5月31日までのファーストリテイリング(9983)の過去データを9983.csvというファイルにダウンロードしています。


--codeオプションで銘柄を指定した場合に余分なデータが追加される問題に対応しました(2024年6月6日)

--codeオプションで指定した銘柄の株価をYahooからダウンロードした場合に、余分なデータが追加される問題が確認されました。

例えば、1月1日は休場のはずですが、なぜか、Yahooから株価をダウンロードすると1月1日のデータが含まれていました。

このため、余分なデータを自動で削除する機能を追加しました。


余分なデータが追加される問題が新たに見つかり、その問題に対応しました(2024年6月8日)

2024年6月6日に対応したはずの日経平均株価をYahooおよびStooqからダウンロードした場合に余分なデータが追加される問題ですが、修正が不十分でした。

改めて修正をしましたが、1999年4月よりも古いデータをダウンロードした場合は、余分なデータが追加される場合があるかもしれません。

根本的な問題は、YahooやStooqからダウンロードされるデータそのものに余分なデータが含まれている点にあるので、人手による確認が必要となります。

1999年4月6日以降は、トヨタ自動車(7203)の株価データを参照しつつ、検証を済ませました。

しかし、それ以前は、検証するための良いサンプルを見つけることができず、未確認の状態です。

仮に、余分なデータが追加されていた場合は、FuncRemoveExceptionDate関数のExceptionDateListに対象の年月日(YYYY-MM-DD)を追加することで削除することができます。


--rnnオプション使用時のバグを修正しました(2024年6月17日)

リカレントニューラルネットワーク(RNN)向けに学習データおよび評価データを出力する機能にバグが見つかり、これを修正しました。

  • 正しいデータの並び

    • 1日目, 2日目, 3日目, 4日目, 5日目, …

  • バグのあった状態でのデータの並び

    • 1日目, 2日目, 2日目, 2日目, 2日目, …

バグのあった状態では、--rnnオプションで3以上を設定した場合、正しくは3日目以降のデータが並ぶべきところを、2日目のデータが繰り返されていました。


いいなと思ったら応援しよう!