もともとProtraはバックテストだけの目的で、手法が確立したら実践ではPythonをを使ってシステムトレードの仕組みを実装する計画でした。
そのために重要なのは、Protraライブラリ「TIlib.pt(テクニカル指標のAPIを提供)」の移植です。
これに変わるライブラリは存在しており「TA-Lib」を使う事を考えていました。
「TA-Lib」とは(T)テクニカル(A)アナリシス-(Lib)ライブラリの略です。
実装すると、次のようなコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
##talibのインポート import talib ##単純移動平均線を計算する ema=talib.EMA(price) ##指数移動平均線を計算する sma=talib.SMA(price) ##ボリンジャーバンドを計算する bbands=talib.BBANDS(price) ##モメンタムを計算する momentam=talib.MOM(price,timeperiod=10) ##MACDを計算する macd=talib.MACD(price,fastperiod=12, slowperiod=26, signalperiod=5) ##RSIを計算する rsi = talib.RSI(price, timeperiod=14) |
このアプローチに対し、シストレを既に実践されている方より、次のようなコメントをもらいました。
目的(株の自動売買)に対して、手段(ProtraをPythonに移植する)が入れ替わっているのでは?
毎日、上司に言われている指摘を受けました・・・。
おっしゃる通りです。
素直にProtra(Windowsアプリ)を自動化する事を考えます・・・
【最初の挑戦】Protraの自動化
Protraはオープンソースのアプリです。
であれば、CUIコマンドで必要な処理をできるようにソースコード書き換えすれば良いじゃん?
と試みました。
が・・・断念。GUI依存度が高すぎました。
【次の挑戦】PyAutoGUIでWindowsアプリの操作自動化
Windowsアプリの操作自動化は、簡単です。
学生時代より、UWSCを使ってWindows画面の操作を自動化しています。
ただ、最近は、RPA(Robotic Process Automation /ロボティック・プロセス・オートメーション)とか自動テストの影響で、多くのツールの選択肢があります。
簡単にまとめてみました。
ツール名 | 簡単な説明 |
---|---|
VBA + UIAutomation | UIAutomationをCOM経由でVBAで実行して画面操作 |
PowerShell + UIAutomation | UIAutomationを.NET経由でPowerShellを使って実行 |
WinAppDriver | Microsoftが開発したSeleniumライクの自動操作を実現するツール |
Friendly | テストツール。操作対象のアプリにテスト用のDLLをインジェクションし、タブの中の要素を画像認識を使わずに操作 |
PyAutoGUI | Pythonでの自動操作を実現。基本的に画像認識で操作。MacでもLinuxでも動作。 |
UWSC | 昔のツール。UIAutomationが認識できる要素を解析可能。公式サイトは閉鎖 |
sikulix | 画像認識に特化したツール。Javaで動作してどこでも動くうえ、スクリプト自体はPythonやRuby,JavaScriptで記載できる。 |
RocketMouse | 昔からある自動操作ツール。画像認識はできますが、オブジェクトの認識はWin32で作ったものしかできない |
UIPath | 2018年のforresterの調査でRPAのリーダーと言わしめた製品。高価格帯ですが、Community版なら個人や小規模事業では使用可能。 |
HiMacroEx | マウスの動きやキーボードを記録して再生することができるフリーソフト |
他にも、BizRobo/BasicRobo(Kofax Kapow)、blueprism、Automation Anywhere、Nice、Win Actor、Autoブラウザ名人、ipaS、NEC Software Robot Solution、RPA Express(WorkFusion)、Seleniumなどがある。
今回は汎用性の高そうなPythonライブラリの「PyAutoGUI」を使ってみます。
「PyAutoGUI」は、Windows, Linux, Mac OS対応、マルチプラットフォームで稼働するPython Gui Automation Libraryです。
PyAutoGUIのインストール
pip を使って行うのが手っ取り早くて便利です。
ウインドウの場所を識別するために、COM制御系のモジュールもインストールします。
また、クリップボードも利用するので、pyperclipも必要です。
1 2 3 4 5 6 7 8 9 |
C:\Python36\Scripts\pip3.6.exe install pyautogui C:\Python36\Scripts\pip3.6.exe install pywin32 C:\Python36\Scripts\pip3.6.exe install win32gui C:\Python36\Scripts\pip3.6.exe install Image C:\Python36\Scripts\pip3.6.exe install pillow C:\Python36\Scripts\pip3.6.exe install pyscreeze C:\Python36\Scripts\pip3.6.exe install PyTweening C:\Python36\Scripts\pip3.6.exe install opencv_python C:\Python36\Scripts\pip3.6.exe install pyperclip |
今回のサンプル実装では、opencv_pythonなどは、インストール無くても動くはずです。
ソースコード
自動売買の処理は次の通りです。
- Protraを起動して最新株価を取得
- PtSimを起動して、バックテスト実施
- バックテスト結果をファイルに保存(デバッグ目的含む)
- 本日(明日)買う・売る銘柄を抽出
- SBI銀行にログインして成行き売り・買い
それをコードにすると次のようになります。
なお、SBI証券へのログインは「SBIcomm」を使いました。
ただし、私は自動売買可能なロジックが出来てないので、スクリプト全体としては動作未確認です。
また、タイムアウト処理などカスタマイズ必要な部分が多々あるので、理解できる方のみお使い下さい。
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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# -*- coding: utf-8 -*- # c:\Python36\python.exe mouse_action.py import pyautogui import pyperclip ##プロセスを制御するためにOS周りのモジュール import re import os import sys import time import array import subprocess from datetime import datetime, timedelta ##Win32のUI情報と制御用モジュール import win32api import win32gui import win32con ## SBI証券での売買 import SBIcomm #Global PATH = "C:\\<任意のパス>\\Protra" # 昨日の年月日を2桁で表示 YESTERDAY = (datetime.today() - timedelta(1)) FILE = "text" + YESTERDAY.strftime("_%y_%m_%d") + ".txt" def pressMenu(a1, a2, b1): for tryCount in range(10): try: pyautogui.hotkey(a1, a2) pyautogui.press(b1) return True except TypeError: time.sleep(1) return False def openApplication(appName): cmd = "start " + PATH + "\\" + appName + ".exe" subprocess.Popen(cmd, shell=True) time.sleep(1) parent_handle = win32gui.FindWindow(None, appName) if parent_handle == 0 : print("アプリ起動に失敗") # sys.exit() def clickButton(png, timeout): nsec = 0 while True: try: # 30%程度のスピードアップ position = pyautogui.locateCenterOnScreen(png, grayscale=True) if position is not None: pyautogui.click(position, duration = 0.2) return True except TypeError: time.sleep(1) # print("count="+str(nsec)) nsec += 1 if nsec > timeout: print("タイムアウト") break return False # Protra操作 def updateStockPrice(): time.sleep(1) openApplication("Protra") time.sleep(1) if (not pressMenu('alt', 'd','u')): print("株価更新に失敗。中断") return False time.sleep(1) if (not clickButton('ok.png', 20)): print("OK ボタン押に失敗。中断") # 画像一致が座標が変わると一致しない場合もある # return False time.sleep(1) if (not pressMenu('alt', 'f','x')): print("Protra終了に失敗。中断") return False return True # バックテスト完了チェック def getStockList(timeout): nsec = 0 while True: for i in range(10): pyautogui.hotkey("shift", "left") time.sleep(0.01) pyautogui.hotkey('ctrl', 'c') clipboard = pyperclip.paste() success = "". join(clipboard[-1 * int(10):]) # 終了文字が出ていたら完了 if (success in "正常に終了しました。"): return True time.sleep(5) # print("count="+str(nsec)) nsec += 1 if nsec > (timeout/ 5): print("タイムアウト") break return False # PtSim操作 def doBackTest(): time.sleep(1) openApplication("PtSim") # 過去のバックテスト処理削除 pyautogui.press("del", interval=0.01) pyautogui.press("enter", interval=0.01) time.sleep(1) pyautogui.press("tab", presses=6, interval=0.01) pyautogui.press("enter", interval=0.01) # バックテスト終了確認 pyautogui.hotkey("shift", "tab") time.sleep(1) if (not getStockList(6 * 60 * 60)): print("バックテスト失敗。中断") return False # 株価の取得(30行まで) for i in range(30): pyautogui.hotkey("shift", "up") time.sleep(0.01) # クリップボードの保存 time.sleep(1) pyautogui.hotkey('ctrl', 'c') clipboard = pyperclip.paste() # PtSim終了 time.sleep(1) pyautogui.hotkey("alt", "f4") # ファイルに保存 f = open(FILE, 'w') lines = clipboard.split('\r\n') for line in lines: f.write(str(line)+ "\n") f.close() return True # 購入銘柄リスト作成(カスタマイズ必要) # 例)次のような文字列配置の場合 # 9984 ソフトバンクグループ 18/12/26 3473円 200株 買 def buySellStock(): buylist = list() selllist = list() # ファイルの呼び出し f = open(FILE, 'r') lines = f.readlines() for line in lines: # 今回のデモは購入できる銘柄がないのでサンプル # if (YESTERDAY.strftime("%y/%m/%d") in line): if ("18/12/26" in line): if ("株 買" in line): buylist.append(line.split(None)) elif ("株 売" in line): selllist.append(line.split(None)) f.close() # 株売買 # SBI証券ログイン # sbi = SBIcomm.SBIcomm("usrname", "pass") # 株価の売買 for line in buylist: code = line[0] quantity = re.sub(r'\D' ,'', line[4]) print("code="+code+" quantity="+quantity) # 買い注文 # sbi.buy_order(code, quantity) for line in selllist: code = line[0] quantity = re.sub(r'\D' ,'', line[4]) print("code="+code+" quantity="+quantity) # 売り注文 # sell_order(code, quantity) return True #以下、メインルーチン if __name__ == "__main__": #画面サイズの取得 screen_x,screen_y = pyautogui.size() #マウスを(1,1)に移動しておく pyautogui.moveTo(1, 1, duration=0) # 最新株価取得 if (not updateStockPrice()): print("Protra処理失敗。中断") sys.exit() # バックテスト実施 if (not doBackTest()): print("PtSim処理失敗。中断") sys.exit() # 銘柄売買 if (not buySellStock()): print("銘柄売買失敗。中断") sys.exit() |
このソースコードでは、画像比較をしており、正解画像が必要です。
こちらの画像を同じフォルダに入れて下さい。
また、バックテストで、翌日購入銘柄は自分でPrintlog関数などを入れて結果出力が必要です。
それをパースして、購入銘柄を抽出します。
エラーでハマった内容まとめ
簡単に作れる・・と思ったけど、色々ハマッた。
トライ&エラーになり、関数化を断念したものが沢山ある・・
【問題1】TypeError: ‘NoneType’ object is not subscriptable
もっとも多くの人が、ハマってるエラーが「pyautogui.locateCenterOnScreen」が一致せずスクリプトがエラー吐いて死ぬというもの。
1 2 3 |
line 404, in center return (coords[0] + int(coords[2] / 2), coords[1] + int(coords[3] / 2)) TypeError: 'NoneType' object is not subscriptable |
これは条件分岐ではなく、例外処理で対応が必要です。
1 2 3 4 5 6 7 8 9 10 11 |
while True: try: position = pyautogui.locateCenterOnScreen(png, grayscale=True) if position is not None: pyautogui.click(position, duration = 0.2) return True except TypeError: time.sleep(1) nsec += 1 if nsec > timeout: break |
【問題2】画像判断(locateCenterOnScreen)が動かない
こちらは致命的。
PyAutoGUIは、スクリーン上の画像を検索して一致したら指定のアクションを起こす仕組みがあります。
ただ、その画像の画面位置が異なると一致しませんでした・・・。
閾値(confidence)を指定しても動作せず、できる限りキーボード操作に差し替えました。
ただ、後学のために「株価の更新」部分は、画像を使って完了処理を確認しています。
タイムアウトをセットしているので、画像一致が動作しなくても問題はありませんし、この部分は、スクリーン位置を動かしても認識しました。
PtSimのバックテスト完了に関しては、「正常に終了しました。」が表示されたら終了としています。
当初は「実行」ボタン有無で判断してました。
しかし上記理由で、locateCenterOnScreenが動かないので、苦肉の策としました。
結果
YouTubeに動画を上げておきました。
単にUIが動いているだけで、何も面白いところは無いです。
まとめ
あくまで実際に自動でGUI操作を自動化するだけなので、処理スピードとしては遅いです。
また、各イベントを座標・ショートカットキー・カーソル移動で指定していくので、ソースコードが見づらくなりがちです。
また、処理が動いているときは対象のPCが操作できません(プログラムの処理に影響あり)。
この辺りは、昔から何も変わってはいませんね。
とりあえず、私の場合は自動化より、はやく売買ロジックを見つける必要があります・・・
参考)PyAutoGUIリファレンス
殆んどPyAutoGUIが使われてないのか情報が少ないので、後学を兼ねて載せておきます(色々なサイトに転載させておりオリジナルが誰か分からず)。
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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
#画面解像度の取得 #要素別にとる max_x,max_y = pyautogui.size() #配列にとる pos = pyautogui.size() #マウス現在位置取得 cur_x,cur_y = pyautogui.position() pos = pyautogui.position() # 画像認識 img_x,img_y = pyautogui.locateCenterOnScreen('search.png') pos = pyautogui.locateCenterOnScreen('search.png') #マウス移動、移動先の(x,y)と移動にかける時間(duration)を秒で指定 #注意点としては、左上の頂点は(0,0)ではなく(1,1)なのを忘れない pyautogui.moveTo(img_x, img_y, duration=2) #現在位置からの相対移動、かける時間(duration)を秒で指定は同じ pyautogui.moveRel(xOffset, yOffset, duration=num_seconds) #ドラッグ&ドロップ #マウス移動、ドラッグ元の(x,y)と移動にかける時間(duration)を秒で指定 #もう一回指定すると、ドロップする pyautogui.dragTo(x, y, duration=num_seconds) #現在位置からの相対移動、ドラッグ&ドロップする位置を指定 #もう一回指定すると、ドロップする pyautogui.dragRel(xOffset, yOffset, duration=num_seconds) #マウスクリック通常設定であれば、左クリック、システムに依存するので、右手用設定時は要注意 pyautogui.click(x,y) #マウスクリック、連射設定、場所の指定以外にクリック数、クリック間隔、ボタンの指定ができる pyautogui.click(x,y, clicks=num_of_clicks, interval=secs_between_clicks, button='left') #その他のマウスクリック pyautogui.rightClick(x, y) pyautogui.middleClick(x, y) pyautogui.doubleClick(x, y) pyautogui.tripleClick(x, y) #マウスホイール pyautogui.scroll(amount_to_scroll, x, y) #押しっぱなしと解放 pyautogui.mouseDown(x, y, button='left') pyautogui.mouseUp(x, y, button='left') # ショートカットを設定するとき pyautogui.hotkey('ctrl', 'c') pyautogui.hotkey('ctrl', 'v') # キーを押下・押上するとき pyautogui.keyDown(key_name) pyautogui.keyUp(key_name) #■■■■記述サンプル■■■■ # 画像が画面内にあるか if pyautogui.onScreen(img_x, img_y): pyautogui.moveTo(img_x, img_y, duration=2) # pyautogui.moveRel(xOffset, yOffset, duration=num_seconds) if pyautogui.locateCenterOnScreen('search.png'): # 画像があったらD&D pyautogui.dragTo(x, y, duration=num_seconds) #pyautogui.dragRel(xOffset, yOffset, duration=num_seconds) if pyautogui.locateCenterOnScreen('search.png'): # 画像があったらクリック pyautogui.click(img_x, img_y) #pyautogui.click(x, y, clicks=num_of_clicks, interval=secs_between_clicks, button='left') #pip install opencv_python でopencvモジュールがインストールされている場合に限り #閾値(confidence)が指定できる、以下の場合95%同じだったらTrue #また、grayscale=Trueを指定すると、グレースケールによる判定を実施し30%前後速度が向上する if pyautogui.locateCenterOnScreen('search.png',grayscale=True,confidence=0.950): # 画像があったらクリック pyautogui.click(img_x, img_y) #2.5秒待ちを入れる pyautogui.PAUSE = 2.5 #スクリーンの[0,0]座標にカーソルを持っていくと、FailSafeExceptionを任意に発生させることができる pyautogui.FAILSAFE = True #マウス移動 pyautogui.moveTo(700, 500) #X, Y pyautogui.moveTo(None, 100, 2) #700, 500へ2秒で pyautogui.moveRel(-50, 0) #相対座標 pyautogui.dragTo(415, 508, button = 'left') #現在地から指定座標までドラッグして離す pyautogui.dragRel(-20, -20, button = 'left') pyautogui.click() pyautogui.click(button='right', clicks=2, interval=0.25) pyautogui.doubleClick() pyautogui.mouseDown(button='left') pyautogui.mouseUp(button='left') pyautogui.scroll(10) pyautogui.hscroll(10) # インターバル0.5秒でshiftを2回押す pyautogui.press('shift', presses=2, interval=0.5) pyautogui.keyDown('shift') pyautogui.keyUp('shift') pyautogui.hotkey('ctrl','c') #同時押し pyautogui.typewrite('hello world!') #指定できるキー名 KEYBOAD_KEYS = ['\t', '\n', '\r', ' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e','f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', 'accept', 'add', 'alt', 'altleft', 'altright', 'apps', 'backspace', 'browserback', 'browserfavorites', 'browserforward', 'browserhome', 'browserrefresh', 'browsersearch', 'browserstop', 'capslock', 'clear', 'convert', 'ctrl', 'ctrlleft', 'ctrlright', 'decimal', 'del', 'delete', 'divide', 'down', 'end', 'enter', 'esc', 'escape', 'execute', 'f1', 'f10', 'f11', 'f12', 'f13', 'f14', 'f15', 'f16', 'f17', 'f18', 'f19', 'f2', 'f20', 'f21', 'f22', 'f23', 'f24', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'final', 'fn', 'hanguel', 'hangul', 'hanja', 'help', 'home', 'insert', 'junja', 'kana', 'kanji', 'launchapp1', 'launchapp2', 'launchmail', 'launchmediaselect', 'left', 'modechange', 'multiply', 'nexttrack', 'nonconvert', 'num0', 'num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8', 'num9', 'numlock', 'pagedown', 'pageup', 'pause', 'pgdn', 'pgup', 'playpause', 'prevtrack', 'print', 'printscreen', 'prntscrn', 'prtsc', 'prtscr', 'return', 'right', 'scrolllock', 'select', 'separator', 'shift', 'shiftleft', 'shiftright', 'sleep', 'space', 'stop', 'subtract', 'tab', 'up', 'volumedown', 'volumemute', 'volumeup', 'win', 'winleft', 'winright', 'yen', 'command', 'option', 'optionleft', 'optionright'] ## メッセージボックス操作 alert(text='', title='', button='OK') confirm(text='', title='', buttons=['OK', 'Cancel']) prompt(text='', title='' , default='') password(text='', title='', default='', mask='*') ## スクリーンショット作成 # 全体 pyautogui.screenshot('my_screenshot.png') # 矩形範囲指定 pyautogui.screenshot(region=(0,0, 300, 400)) ## 画像認識 #それぞれの要素をとる pos_x,pos_y,size_x,size_y = pyautogui.locateOnScreen('target.png') #配列に格納してとる pos = pyautogui.locateOnScreen('target.png') #画像の中心座標を取る pos_x,pos_y = pyautogui.center('target.png') ## サーチ範囲(region)指定 # pyautogui.locateOnScreen('someButton.png', region=(0,0, 300, 400)) ## 色の無視(grayscale) # pyautogui.locateOnScreen('someButton.png', grayscale=True, region=(0,0, 300, 400)) ## マッチ誤差(tolerance)[0-100%] # pyautogui.locateOnScreen('someButton.png', grayscale=True,tolerance=10 ,region=(0,0, 300, 400)) |