├── LICENSE ├── README.md ├── chapter2 ├── apply_divide_union_data.py ├── csv_to_db.py ├── csv_to_divide_union_data.py ├── get_brands.py ├── get_new_brands.py ├── pyquery_sample.py ├── selenium_sample.py └── yahoo_csv_download.py ├── chapter3 ├── buy_and_hold.py ├── get_price_dataframe.py ├── golden_core30.py ├── nikkei_tsumitate_trade.py ├── rating_trade.py └── simulator.py └── chapter4_5 ├── opincome_trade.py └── simulator.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 BOSUKE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 「株とPython ─ 自作プログラムでお金儲けを目指す本」 サンプルコードなど 2 | 3 | インプレスR&Dより出版されている[「株とPython ─ 自作プログラムでお金儲けを目指す本」](https://nextpublishing.jp/book/10319.html)に登場するサンプルソースコードや訂正・補足内容を記載しています。 4 | 5 | ## サンプルコード一覧 6 | 7 | | 章・節番号 | サンプルコード内容 | GitHubでのファイルパス | 備考 | 8 | |------------|-------------------|----------------------|---------| 9 | | 2.3.2 | 2. PyQueryの使い方 | [chapter2/pyquery_sample.py](chapter2/pyquery_sample.py) | [株探のHTMLファイル変更に対応](#株探のHTMLファイル変更) | 10 | | 2.3.3.| 2. seleniumの使い方 | [chapter2/selenium_sample.py](chapter2/selenium_sample.py) | | 11 | | 2.3.4 | リスト2.1 | [chapter2/get_brands.py](chapter2/get_brands.py )|[株探のHTMLファイル変更に対応](#株探のHTMLファイル変更) | 12 | | 2.4.1 | リスト2.2 | [chapter2/yahoo_csv_download.py](chapter2/yahoo_csv_download.py) | | 13 | | 2.4.1 | リスト2.3 | [chapter2/csv_to_db.py](chapter2/csv_to_db.py) || 14 | | 2.5.2 | リスト2.4 | [chapter2/get_new_brands.py](chapter2/get_new_brands.py)| | 15 | | 2.6.2 | リスト2.5 | [chapter2/csv_to_divide_union_data.py](chapter2/csv_to_divide_union_data.py) | | 16 | | 2.6.2 | リスト2.6 | [chapter2/apply_divide_union_data.py](chapter2/apply_divide_union_data.py) | | 17 | | 3.1 | リスト3.1 | [chapter3/get_price_dataframe.py](chapter3/get_price_dataframe.py)| | 18 | | 3.2 | リスト3.2 ~ リスト3.4 | [chapter3/simulator.py](chapter3/simulator.py)| | 19 | | 3.3 | リスト3.5 | [chapter3/golden_core30.py](chapter3/golden_core30.py) || 20 | | 3.3 | リスト3.6 | [chapter3/buy_and_hold.py](chapter3/buy_and_hold.py) || 21 | | 3.4 | リスト3.7 | [chapter3/rating_trade.py](chapter3/rating_trade.py) || 22 | | 3.4 | リスト3.8 | [chapter3/nikkei_tsumitate_trade.py](chapter3/nikkei_tsumitate_trade.py)|| 23 | | 4章 | リスト4.1 ~ リスト4.2 | [chapter4_5/simulator.py](chapter4_5/simulator.py) | 3章のsimulater.pyに指標関数を追加 | 24 | | 5.4 | リスト5.1 | [chapter4_5/opincome_trade.py](chapter4_5/opincome_trade.py) | | 25 | 26 | ## 訂正と補足 27 | 28 | ### 株探のHTMLファイル変更 29 | 株探(Kabutan.jp)のページ内容(HTMLデータ内容)が本書執筆時から変更されたため、本書内の一部のコードがそのままでは動作しません。動作しないコードの一覧は [サンプルコード一覧](#サンプルコード一覧)を参照してください。 30 | GitHub上のコードは本README.mdコミット時点で動作することを確認していますので、そちらを参考にしてください。 31 | 32 | ### JupyterLabの利用を推奨 33 | 本書内ではプログラムの実行環境として Jupyter Notebook を紹介していますが、 少なくとも 34 | Python 3.6.5 + Jupyter Notebook 5.7.0 + pandas-highcharts 0.5.2の環境にて、pandas-highchartsのdisplay_chartsにてグラフがNotebook上に表示されない現象を確認しています。 35 | 36 | Jupyter notebookの変わりにJupyterLab(0.35.4)上でプログラムを実行するとこの現象が発生しないことを確認しています(原因不明)。 37 | 38 | JupyterLabはJupyter Notebookの後継となるソフトウエアで、同じような操作感で利用できます。 39 | 40 | ### 受渡日が1日短縮 (2019年7月16日約定分より) 41 | 42 | 本書の 1.4 権利確定日・権利落ち日・権利付き最終日 では、 43 | 買った日(約定日)を含め4日後に株は受け渡されると記載していますが、 44 | 2019年7月16日約定分より受け渡しが1日短く、3日後に変更されます。 45 | 46 | 2019年7月16日より前 47 | 48 | | 25日(木) | 26日(金) | 27日(土) | 28(日) | 29日(月) | 30日(火) | 49 | |----------|----------|----------|--------|--------|--------| 50 | |権利付き最終日|権利落ち日| (休日) | (休日) | | 権利確定日 | 51 | 52 | 53 | 2019年7月16日以降 54 | 55 | | 25日(木) | 26日(金) | 27日(土) | 28(日) | 29日(月) | 30日(火) | 56 | |----------|----------|----------|--------|--------|--------| 57 | | |権利付き最終日| (休日) | (休日) | 権利落ち日| 権利確定日 | 58 | -------------------------------------------------------------------------------- /chapter2/apply_divide_union_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import sqlite3 4 | 5 | def apply_divide_union_data(db_file_name, date_of_right_allotment): 6 | conn = sqlite3.connect(db_file_name) 7 | 8 | # date_of_right_allotment 以前の分割・併合データで未適用のものを取得する 9 | sql = """ 10 | SELECT 11 | d.code, d.date_of_right_allotment, d.before, d.after 12 | FROM 13 | divide_union_data AS d 14 | WHERE 15 | d.date_of_right_allotment < ? 16 | AND NOT EXISTS ( 17 | SELECT 18 | * 19 | FROM 20 | applied_divide_union_data AS a 21 | WHERE 22 | d.code = a.code 23 | AND d.date_of_right_allotment = a.date_of_right_allotment 24 | ) 25 | ORDER BY 26 | d.date_of_right_allotment 27 | """ 28 | cur = conn.execute(sql, (date_of_right_allotment,)) 29 | divide_union_data = cur.fetchall() 30 | 31 | with conn: 32 | conn.execute('BEGIN TRANSACTION') 33 | for code, date_of_right_allotment, before, after in divide_union_data: 34 | 35 | rate = before / after 36 | inv_rate = 1 / rate 37 | 38 | conn.execute( 39 | 'UPDATE prices SET ' 40 | ' open = open * :rate, ' 41 | ' high = high * :rate, ' 42 | ' low = low * :rate, ' 43 | ' close = close * :rate, ' 44 | ' volume = volume * :inv_rate ' 45 | 'WHERE code = :code ' 46 | ' AND date <= :date_of_right_allotment', 47 | {'code' : code, 48 | 'date_of_right_allotment' : date_of_right_allotment, 49 | 'rate' : rate, 50 | 'inv_rate' : inv_rate}) 51 | 52 | conn.execute( 53 | 'INSERT INTO ' 54 | 'applied_divide_union_data(code, date_of_right_allotment) ' 55 | 'VALUES(?,?)', 56 | (code, date_of_right_allotment)) 57 | -------------------------------------------------------------------------------- /chapter2/csv_to_db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import csv 3 | import glob 4 | import datetime 5 | import os 6 | import sqlite3 7 | 8 | 9 | def generate_price_from_csv_file(csv_file_name, code): 10 | with open(csv_file_name, encoding="shift_jis") as f: 11 | reader = csv.reader(f) 12 | next(reader) # 先頭行を飛ばす 13 | for row in reader: 14 | d = datetime.datetime.strptime(row[0], '%Y/%m/%d').date() #日付 15 | o = float(row[1]) # 初値 16 | h = float(row[2]) # 高値 17 | l = float(row[3]) # 安値 18 | c = float(row[4]) # 終値 19 | v = int(row[5]) # 出来高 20 | yield code, d, o, h, l, c, v 21 | 22 | 23 | def generate_from_csv_dir(csv_dir, generate_func): 24 | for path in glob.glob(os.path.join(csv_dir, "*.T.csv")): 25 | file_name = os.path.basename(path) 26 | code = file_name.split('.')[0] 27 | for d in generate_func(path, code): 28 | yield d 29 | 30 | 31 | def all_csv_file_to_db(db_file_name, csv_file_dir): 32 | price_generator = generate_from_csv_dir(csv_file_dir, 33 | generate_price_from_csv_file) 34 | conn = sqlite3.connect(db_file_name) 35 | with conn: 36 | sql = """ 37 | INSERT INTO raw_prices(code,date,open,high,low,close,volume) 38 | VALUES(?,?,?,?,?,?,?) 39 | """ 40 | conn.executemany(sql, price_generator) 41 | -------------------------------------------------------------------------------- /chapter2/csv_to_divide_union_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import csv 3 | import glob 4 | import datetime 5 | import os 6 | import sqlite3 7 | 8 | 9 | def generater_devide_union_from_csv_file(csv_file_name, code): 10 | with open(csv_file_name, encoding="shift_jis") as f: 11 | reader = csv.reader(f) 12 | next(reader) # 先頭行を飛ばす 13 | 14 | def parse_recode(row): 15 | d = datetime.datetime.strptime(row[0], '%Y/%m/%d').date() #日付 16 | r = float(row[4]) # 調整前終値 17 | a = float(row[6]) # 調整後終値 18 | return d, r, a 19 | 20 | _, r_n, a_n = parse_recode(next(reader)) 21 | for row in reader: 22 | d, r, a = parse_recode(row) 23 | rate = (a_n * r) / (a * r_n) 24 | if abs(rate - 1) > 0.005: 25 | if rate < 1: 26 | before = round(1 / rate, 2) 27 | after = 1 28 | else: 29 | before = 1 30 | after = round(rate, 2) 31 | yield code, d, before, after 32 | r_n = r 33 | a_n = a 34 | 35 | 36 | def generate_from_csv_dir(csv_dir, generate_func): 37 | for path in glob.glob(os.path.join(csv_dir, "*.T.csv")): 38 | file_name = os.path.basename(path) 39 | code = file_name.split('.')[0] 40 | for d in generate_func(path, code): 41 | yield d 42 | 43 | 44 | def all_csv_file_to_divide_union_table(db_file_name, csv_file_dir): 45 | divide_union_generator = generate_from_csv_dir(csv_file_dir, 46 | generater_devide_union_from_csv_file) 47 | conn = sqlite3.connect(db_file_name) 48 | with conn: 49 | sql = """ 50 | INSERT INTO 51 | divide_union_data (code, date_of_right_allotment,before, after) 52 | VALUES(?,?,?,?) 53 | """ 54 | conn.executemany(sql, divide_union_generator) 55 | -------------------------------------------------------------------------------- /chapter2/get_brands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from pyquery import PyQuery 3 | import time 4 | import sqlite3 5 | 6 | 7 | def get_brand(code): 8 | url = 'https://kabutan.jp/stock/?code={}'.format(code) 9 | 10 | q = PyQuery(url) 11 | 12 | if len(q.find('div.company_block')) == 0: 13 | return None 14 | 15 | try: 16 | name = q.find('div.company_block > h3').text() 17 | code_short_name = q.find('#stockinfo_i1 > div.si_i1_1 > h2').text() 18 | short_name = code_short_name[code_short_name.find(" ") + 1:] 19 | market = q.find('span.market').text() 20 | unit_str = q.find('#kobetsu_left > table:nth-child(4) > tbody > tr:nth-child(6) > td').text() 21 | unit = int(unit_str.split()[0].replace(',', '')) 22 | sector = q.find('#stockinfo_i2 > div > a').text() 23 | except (ValueError, IndexError): 24 | return None 25 | 26 | return code, name, short_name, market, unit, sector 27 | 28 | def brands_generator(code_range): 29 | for code in code_range: 30 | brand = get_brand(code) 31 | if brand: 32 | yield brand 33 | time.sleep(1) 34 | 35 | def insert_brands_to_db(db_file_name, code_range): 36 | conn = sqlite3.connect(db_file_name) 37 | with conn: 38 | sql = 'INSERT INTO brands(code,name,short_name,market,unit,sector) ' \ 39 | 'VALUES(?,?,?,?,?,?)' 40 | conn.executemany(sql, brands_generator(code_range)) 41 | -------------------------------------------------------------------------------- /chapter2/get_new_brands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from pyquery import PyQuery 3 | import datetime 4 | import sqlite3 5 | 6 | def new_brands_generator(): 7 | url = 'http://www.jpx.co.jp/listing/stocks/new/index.html' 8 | q = PyQuery(url) 9 | for d, i in zip(q.find('tbody > tr:even > td:eq(0)'), 10 | q.find('tbody > tr:even span')): 11 | date = datetime.datetime.strptime(d.text, '%Y/%m/%d').date() 12 | yield (i.get('id'), date) 13 | 14 | def insert_new_brands_to_db(db_file_name): 15 | conn = sqlite3.connect(db_file_name) 16 | with conn: 17 | sql = 'INSERT INTO new_brands(code,date) VALUES(?,?)' 18 | conn.executemany(sql, new_brands_generator()) 19 | -------------------------------------------------------------------------------- /chapter2/pyquery_sample.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from pyquery import PyQuery 3 | 4 | q = PyQuery('https://kabutan.jp/stock/?code=7203') 5 | sector = q.find('#stockinfo_i2 > div > a')[0].text 6 | print(sector) 7 | -------------------------------------------------------------------------------- /chapter2/selenium_sample.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from selenium import webdriver 3 | 4 | driver = webdriver.Firefox() 5 | driver.get('http://jp.kabumap.com/servlets/kabumap/Action?SRC=basic/top/base&codetext=7203') 6 | unit = driver.find_element_by_css_selector('#minUnit').text 7 | print(unit) 8 | -------------------------------------------------------------------------------- /chapter2/yahoo_csv_download.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from selenium import webdriver 3 | from selenium.common.exceptions import NoSuchElementException 4 | 5 | def download_stock_csv(code_range, save_dir): 6 | 7 | # CSVファイルを自動で save_dir に保存するための設定 8 | profile = webdriver.FirefoxProfile() 9 | profile.set_preference("browser.download.folderList", 10 | 2) 11 | profile.set_preference("browser.download.manager.showWhenStarting", 12 | False) 13 | profile.set_preference("browser.download.dir", save_dir) 14 | profile.set_preference("browser.helperApps.neverAsk.saveToDisk", 15 | "text/csv") 16 | 17 | driver = webdriver.Firefox(firefox_profile=profile) 18 | driver.get('https://www.yahoo.co.jp/') 19 | 20 | # ここで手動でログインを行う。ログインしたら enter 21 | input('After login, press enter: ') 22 | 23 | for code in code_range: 24 | url = 'https://stocks.finance.yahoo.co.jp/stocks/history/?code={0}.T'.format(code) 25 | driver.get(url) 26 | 27 | try: 28 | driver.find_element_by_link_text('時系列データをダウンロード(CSV)').click() 29 | except NoSuchElementException: 30 | pass 31 | 32 | if __name__ == '__main__': 33 | import os 34 | download_stock_csv((7203, 9684), os.getcwd()) 35 | -------------------------------------------------------------------------------- /chapter3/buy_and_hold.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import simulator as sim 3 | from golden_core30 import create_stock_data 4 | 5 | def simulate_buy_and_hold(db_file_name, start_date, end_date, code, deposit): 6 | 7 | stocks = create_stock_data(db_file_name, (code,), 8 | start_date, end_date) 9 | 10 | def get_open_price_func(date, code): 11 | return stocks[code]['prices']['open'][date] 12 | 13 | def get_close_price_func(date, code): 14 | return stocks[code]['prices']['close'][date] 15 | 16 | def trade_func(date, portfolio): 17 | if date == start_date: 18 | return [sim.BuyMarketOrderAsPossible( 19 | code, stocks[code]['unit'])] 20 | return [] 21 | 22 | return sim.simulate(start_date, end_date, deposit, 23 | trade_func, 24 | get_open_price_func, get_close_price_func) -------------------------------------------------------------------------------- /chapter3/get_price_dataframe.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import pandas as pd 3 | 4 | def get_price_dataframe(db_file_name, code): 5 | conn = sqlite3.connect(db_file_name) 6 | return pd.read_sql('SELECT date, open, high, low, close, volume ' 7 | 'FROM prices ' 8 | 'WHERE code = ? ' 9 | 'ORDER BY date', 10 | conn, 11 | params=(code,), 12 | parse_dates=('date',), 13 | index_col='date') 14 | -------------------------------------------------------------------------------- /chapter3/golden_core30.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sqlite3 3 | import datetime 4 | from collections import defaultdict 5 | import pandas as pd 6 | import simulator as sim 7 | 8 | 9 | def create_stock_data(db_file_name, code_list, start_date, end_date): 10 | """指定した銘柄(code_list)それぞれの単元株数と日足(始値・終値)を含む辞書を作成 11 | """ 12 | stocks = {} 13 | tse_index = sim.tse_date_range(start_date, end_date) 14 | conn = sqlite3.connect(db_file_name) 15 | for code in code_list: 16 | unit = conn.execute('SELECT unit from brands WHERE code = ?', 17 | (code,)).fetchone()[0] 18 | prices = pd.read_sql('SELECT date, open, close ' 19 | 'FROM prices ' 20 | 'WHERE code = ? AND date BETWEEN ? AND ?' 21 | 'ORDER BY date', 22 | conn, 23 | params=(code, start_date, end_date), 24 | parse_dates=('date',), 25 | index_col='date') 26 | stocks[code] = {'unit': unit, 27 | 'prices': prices.reindex(tse_index, method='ffill')} 28 | return stocks 29 | 30 | def generate_cross_date_list(prices): 31 | """指定した日足データよりゴールデンクロス・デッドクロスが生じた日のリストを生成 32 | """ 33 | # 移動平均を求める 34 | sma_5 = prices.rolling(window=5).mean() 35 | sma_25 = prices.rolling(window=25).mean() 36 | 37 | # ゴールデンクロス・デッドクロスが発生した場所を得る 38 | sma_5_over_25 = sma_5 > sma_25 39 | cross = sma_5_over_25 != sma_5_over_25.shift(1) 40 | golden_cross = cross & (sma_5_over_25 == True) 41 | dead_cross = cross & (sma_5_over_25 == False) 42 | golden_cross.drop(golden_cross.head(25).index, inplace=True) 43 | dead_cross.drop(dead_cross.head(25).index, inplace=True) 44 | 45 | # 日付のリストに変換 46 | golden_list = [x.date() 47 | for x 48 | in golden_cross[golden_cross].index.to_pydatetime()] 49 | dead_list = [x.date() 50 | for x 51 | in dead_cross[dead_cross].index.to_pydatetime()] 52 | return golden_list, dead_list 53 | 54 | 55 | def simulate_golden_dead_cross(db_file_name, 56 | start_date, end_date, 57 | code_list, 58 | deposit, 59 | order_under_limit): 60 | """deposit: 初期の所持金 61 |   order_order_under_limit: ゴールデンクロス時の最小購入金額 62 | """ 63 | 64 | stocks = create_stock_data(db_file_name, code_list, start_date, end_date) 65 | 66 | # {ゴールデンクロス・デッドクロスが発生した日 : 発生した銘柄のリスト} 67 | # の辞書を作成 68 | golden_dict = defaultdict(list) 69 | dead_dict = defaultdict(list) 70 | for code in code_list: 71 | prices = stocks[code]['prices']['close'] 72 | golden, dead = generate_cross_date_list(prices) 73 | for l, d in zip((golden, dead), (golden_dict, dead_dict)): 74 | for date in l: 75 | d[date].append(code) 76 | 77 | def get_open_price_func(date, code): 78 | return stocks[code]['prices']['open'][date] 79 | 80 | def get_close_price_func(date, code): 81 | return stocks[code]['prices']['close'][date] 82 | 83 | def trade_func(date, portfolio): 84 | order_list = [] 85 | # Dead crossが発生していて持っている株があれば売る 86 | if date in dead_dict: 87 | for code in dead_dict[date]: 88 | if code in portfolio.stocks: 89 | order_list.append( 90 | sim.SellMarketOrder(code, 91 | portfolio.stocks[code].current_count)) 92 | # 保有していない株でgolden crossが発生していたら買う 93 | if date in golden_dict: 94 | for code in golden_dict[date]: 95 | if code not in portfolio.stocks: 96 | order_list.append( 97 | sim.BuyMarketOrderMoreThan(code, 98 | stocks[code]['unit'], 99 | order_under_limit)) 100 | return order_list 101 | 102 | return sim.simulate(start_date, end_date, 103 | deposit, 104 | trade_func, 105 | get_open_price_func, get_close_price_func) 106 | 107 | 108 | -------------------------------------------------------------------------------- /chapter3/nikkei_tsumitate_trade.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOSUKE/stock_and_python_book/626e828fad662e902b6b5fd0d9a6b50aa8d5349e/chapter3/nikkei_tsumitate_trade.py -------------------------------------------------------------------------------- /chapter3/rating_trade.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from dateutil.relativedelta import relativedelta 3 | import simulator as sim 4 | 5 | def simulate_rating_trade(db_file_name, start_date, 6 | end_date, deposit, reserve): 7 | conn = sqlite3.connect(db_file_name) 8 | 9 | def get_open_price_func(date, code): 10 | r = conn.execute('SELECT open FROM prices ' 11 | 'WHERE code = ? AND date <= ? ' 12 | 'ORDER BY date DESC LIMIT 1', 13 | (code, date)).fetchone() 14 | return r[0] 15 | 16 | def get_close_price_func(date, code): 17 | r = conn.execute('SELECT close FROM prices ' 18 | 'WHERE code = ? AND date <= ? ' 19 | 'ORDER BY date DESC LIMIT 1', 20 | (code, date)).fetchone() 21 | return r[0] 22 | 23 | def get_prospective_brand(date): 24 | """購入する銘柄を物色 購入すべき銘柄の(コード, 単元株数, 比率)を返す 25 | """ 26 | prev_month_day = date - relativedelta(months=1) 27 | sql = """ 28 | WITH last_date_t AS ( 29 | SELECT 30 | MAX(date) AS max_date, 31 | code, 32 | think_tank 33 | FROM 34 | ratings 35 | WHERE 36 | date BETWEEN :prev_month_day AND :day 37 | GROUP BY 38 | code, 39 | think_tank 40 | ), avg_t AS ( 41 | SELECT 42 | ratings.code, 43 | AVG(ratings.target) AS target_avg 44 | FROM 45 | ratings, 46 | last_date_t 47 | WHERE 48 | ratings.date = last_date_t.max_date 49 | AND ratings.code = last_date_t.code 50 | AND ratings.think_tank = last_date_t.think_tank 51 | GROUP BY 52 | ratings.code 53 | ) 54 | SELECT 55 | avg_t.code, 56 | brands.unit, 57 | (avg_t.target_avg / prices.close) AS rate 58 | FROM 59 | avg_t, 60 | prices, 61 | brands 62 | WHERE 63 | avg_t.code = prices.code 64 | AND prices.date = :day 65 | AND rate > 1.2 66 | AND prices.code = brands.code 67 | ORDER BY 68 | rate DESC 69 | LIMIT 70 | 1 71 | """ 72 | return conn.execute(sql, 73 | {'day': date, 74 | 'prev_month_day': prev_month_day}).fetchone() 75 | 76 | current_month = start_date.month - 1 77 | 78 | def trade_func(date, portfolio): 79 | nonlocal current_month 80 | if date.month != current_month: 81 | # 月初め => 入金 82 | portfolio.add_deposit(reserve) 83 | current_month = date.month 84 | 85 | order_list = [] 86 | 87 | # ±20パーセントで利確/損切り 88 | for code, stock in portfolio.stocks.items(): 89 | current = get_close_price_func(date, code) 90 | rate = (current / stock.average_cost) - 1 91 | if abs(rate) > 0.2: 92 | order_list.append( 93 | sim.SellMarketOrder(code, stock.current_count)) 94 | 95 | # 月の入金額以上持っていたら新しい株を物色 96 | if portfolio.deposit >= reserve: 97 | r = get_prospective_brand(date) 98 | if r: 99 | code, unit, _= r 100 | order_list.append(sim.BuyMarketOrderAsPossible(code, unit)) 101 | 102 | return order_list 103 | 104 | return sim.simulate(start_date, end_date, deposit, 105 | trade_func, 106 | get_open_price_func, get_close_price_func) 107 | -------------------------------------------------------------------------------- /chapter3/simulator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | import collections 4 | import pandas as pd 5 | import japandas 6 | 7 | 8 | def calc_tax(total_profit): 9 | """儲けに対する税金計算 10 | """ 11 | if total_profit < 0: 12 | return 0 13 | return int(total_profit * 0.20315) 14 | 15 | 16 | def calc_fee(total): 17 | """約定手数料計算(楽天証券の場合) 18 | """ 19 | if total <= 50000: 20 | return 54 21 | elif total <= 100000: 22 | return 97 23 | elif total <= 200000: 24 | return 113 25 | elif total <= 500000: 26 | return 270 27 | elif total <= 1000000: 28 | return 525 29 | elif total <= 1500000: 30 | return 628 31 | elif total <= 30000000: 32 | return 994 33 | else: 34 | return 1050 35 | 36 | 37 | def calc_cost_of_buying(count, price): 38 | """株を買うのに必要なコストと手数料を計算 39 | """ 40 | subtotal = int(count * price) 41 | fee = calc_fee(subtotal) 42 | return subtotal + fee, fee 43 | 44 | 45 | def calc_cost_of_selling(count, price): 46 | """株を売るのに必要なコストと手数料を計算 47 | """ 48 | subtotal = int(count * price) 49 | fee = calc_fee(subtotal) 50 | return fee, fee 51 | 52 | 53 | class OwnedStock(object): 54 | def __init__(self): 55 | self.total_cost = 0 # 取得にかかったコスト(総額) 56 | self.total_count = 0 # 取得した株数(総数) 57 | self.current_count = 0 # 現在保有している株数 58 | self.average_cost = 0 # 平均取得価額 59 | 60 | def append(self, count, cost): 61 | if self.total_count != self.current_count: 62 | self.total_count = self.current_count 63 | self.total_cost = self.current_count * self.average_cost 64 | self.total_cost += cost 65 | self.total_count += count 66 | self.current_count += count 67 | self.average_cost = math.ceil(self.total_cost / self.total_count) 68 | 69 | def remove(self, count): 70 | if self.current_count < count: 71 | raise ValueError("can't remove", self.total_cost, count) 72 | self.current_count -= count 73 | 74 | class Portfolio(object): 75 | 76 | def __init__(self, deposit): 77 | self.deposit = deposit # 現在の預り金 78 | self.amount_of_investment = deposit # 投資総額 79 | self.total_profit = 0 # 総利益(税引き前) 80 | self.total_tax = 0 # (源泉徴収)税金合計 81 | self.total_fee = 0 # 手数料合計 82 | self.stocks = collections.defaultdict(OwnedStock) # 保有銘柄 銘柄コード -> OwnedStock への辞書 83 | 84 | def add_deposit(self, deposit): 85 | """預り金を増やす (= 証券会社に入金) 86 | """ 87 | self.deposit += deposit 88 | self.amount_of_investment += deposit 89 | 90 | def buy_stock(self, code, count, price): 91 | """株を買う 92 | """ 93 | cost, fee = calc_cost_of_buying(count, price) 94 | if cost > self.deposit: 95 | raise ValueError('cost > deposit', cost, self.deposit) 96 | 97 | # 保有株数増加 98 | self.stocks[code].append(count, cost) 99 | 100 | self.deposit -= cost 101 | self.total_fee += fee 102 | 103 | def sell_stock(self, code, count, price): 104 | """株を売る 105 | """ 106 | subtotal = int(count * price) 107 | cost, fee = calc_cost_of_selling(count, price) 108 | if cost > self.deposit + subtotal: 109 | raise ValueError('cost > deposit + subtotal', 110 | cost, self.deposit + subtotal) 111 | 112 | # 保有株数減算 113 | stock = self.stocks[code] 114 | average_cost = stock.average_cost 115 | stock.remove(count) 116 | if stock.current_count == 0: 117 | del self.stocks[code] 118 | 119 | # 儲け計算 120 | profit = int((price - average_cost) * count - cost) 121 | self.total_profit += profit 122 | 123 | # 源泉徴収額決定 124 | current_tax = calc_tax(self.total_profit) 125 | withholding = current_tax - self.total_tax 126 | self.total_tax = current_tax 127 | 128 | self.deposit += subtotal - cost - withholding 129 | self.total_fee += fee 130 | 131 | def calc_current_total_price(self, get_current_price_func): 132 | """現在の評価額を返す 133 | """ 134 | stock_price = sum(get_current_price_func(code) 135 | * stock.current_count 136 | for code, stock in self.stocks.items()) 137 | return stock_price + self.deposit 138 | 139 | 140 | 141 | 142 | class Order(object): 143 | 144 | def __init__(self, code): 145 | self.code = code 146 | 147 | def execute(self, date, portfolio, get_price_func): 148 | pass 149 | 150 | @classmethod 151 | def default_order_logger(cls, order_type, date, code, count, price, before_deposit, after_deposit): 152 | print("{} {} code:{} count:{} price:{} deposit:{} -> {}".format( 153 | date.strftime('%Y-%m-%d'), 154 | order_type, 155 | code, 156 | count, 157 | price, 158 | before_deposit, 159 | after_deposit 160 | )) 161 | logger = default_order_logger 162 | 163 | class BuyMarketOrderAsPossible(Order): 164 | """残高で買えるだけ買う成行注文 165 | """ 166 | 167 | def __init__(self, code, unit): 168 | super().__init__(code) 169 | self.unit = unit 170 | 171 | def execute(self, date, portfolio, get_price_func): 172 | price = get_price_func(self.code) 173 | count_of_buying_unit = int(portfolio.deposit / price / self.unit) 174 | while count_of_buying_unit: 175 | try: 176 | count = count_of_buying_unit * self.unit 177 | prev_deposit = portfolio.deposit 178 | portfolio.buy_stock(self.code, count, price) 179 | self.logger("BUY", date, self.code, count, price, prev_deposit, portfolio.deposit) 180 | except ValueError: 181 | count_of_buying_unit -= 1 182 | else: 183 | break 184 | 185 | 186 | class BuyMarketOrderMoreThan(Order): 187 | """指定額以上で最小の株数を買う 188 | """ 189 | def __init__(self, code, unit, under_limit): 190 | super().__init__(code) 191 | self.unit = unit 192 | self.under_limit = under_limit 193 | 194 | def execute(self, date, portfolio, get_price_func): 195 | price = get_price_func(self.code) 196 | unit_price = price * self.unit 197 | if unit_price > self.under_limit: 198 | count_of_buying_unit = 1 199 | else: 200 | count_of_buying_unit = int(self.under_limit / unit_price) 201 | while count_of_buying_unit: 202 | try: 203 | count = count_of_buying_unit * self.unit 204 | prev_deposit = portfolio.deposit 205 | portfolio.buy_stock(self.code, count, price) 206 | self.logger("BUY", date, self.code, count, price, prev_deposit, portfolio.deposit) 207 | except ValueError: 208 | count_of_buying_unit -= 1 209 | else: 210 | break 211 | 212 | class SellMarketOrder(Order): 213 | """成行の売り注文 214 | """ 215 | def __init__(self, code, count): 216 | super().__init__(code) 217 | self.count = count 218 | 219 | def execute(self, date, portfolio, get_price_func): 220 | price = get_price_func(self.code) 221 | prev_deposit = portfolio.deposit 222 | portfolio.sell_stock(self.code, self.count, price) 223 | self.logger("SELL", date, self.code, self.count, price, prev_deposit, portfolio.deposit) 224 | 225 | def tse_date_range(start_date, end_date): 226 | tse_business_day = pd.offsets.CustomBusinessDay( 227 | calendar=japandas.TSEHolidayCalendar()) 228 | return pd.date_range(start_date, end_date, 229 | freq=tse_business_day) 230 | 231 | def simulate(start_date, end_date, deposit, trade_func, 232 | get_open_price_func, get_close_price_func): 233 | """ 234 | [start_date, end_date]の範囲内の売買シミュレーションを行う 235 | deposit: 最初の所持金 236 | trade_func: 237 | シミュレーションする取引関数 238 | (引数 date, portfolio でOrderのリストを返す関数) 239 | get_open_price_func: 240 | 指定銘柄コードの指定日の始値を返す関数 (引数 date, code) 241 | get_close_price_func: 242 | 指定銘柄コードの指定日の始値を返す関数 (引数 date, code) 243 | """ 244 | 245 | portfolio = Portfolio(deposit) 246 | 247 | total_price_list = [] 248 | profit_or_loss_list = [] 249 | def record(d): 250 | # 本日(d)の損益などを記録 251 | current_total_price = portfolio.calc_current_total_price( 252 | lambda code: get_close_price_func(d, code)) 253 | total_price_list.append(current_total_price) 254 | profit_or_loss_list.append(current_total_price 255 | - portfolio.amount_of_investment) 256 | 257 | def execute_order(d, orders): 258 | # 本日(d)において注文(orders)をすべて執行する 259 | for order in orders: 260 | order.execute(d, portfolio, 261 | lambda code: get_open_price_func(d, code)) 262 | 263 | order_list = [] 264 | date_range = [pdate.to_pydatetime().date() 265 | for pdate in tse_date_range(start_date, end_date)] 266 | for date in date_range[:-1]: 267 | execute_order(date, order_list) # 前日に行われた注文を執行 268 | order_list = trade_func(date, portfolio) # 明日実行する注文を決定する 269 | record(date) # 損益等の記録 270 | 271 | # 最終日に保有株は全部売却 272 | last_date = date_range[-1] 273 | execute_order(last_date, 274 | [SellMarketOrder(code, stock.current_count) 275 | for code, stock in portfolio.stocks.items()]) 276 | record(last_date) 277 | 278 | return portfolio, \ 279 | pd.DataFrame(data={'price': total_price_list, 280 | 'profit': profit_or_loss_list}, 281 | index=pd.DatetimeIndex(date_range)) 282 | -------------------------------------------------------------------------------- /chapter4_5/opincome_trade.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sqlite3 3 | import pandas as pd 4 | import numpy as np 5 | import simulator as sim 6 | 7 | def simulate_op_income_trade(db_file_name, 8 | start_date, 9 | end_date, 10 | deposit, 11 | growth_rate_threshold, 12 | minimum_buy_threshold, 13 | trading_value_threshold, 14 | profit_taking_threshold, 15 | stop_loss_threshold): 16 | """ 17 | 営業利益が拡大している銘柄を買う戦略のシミュレーション 18 | Args: 19 | db_file_name: DB(SQLite)のファイル名 20 | start_date: シミュレーション開始日 21 | end_date: シミュレーション終了日 22 | deposit: シミュレーション開始時点での所持金 23 | growth_rate_threshold : 購入対象とする銘柄の四半期成長率の閾値 24 | minimum_buy_threshold : 購入時の最低価格 25 | trading_value_threshold: 購入対象とする銘柄の出来高閾値 26 | profit_taking_threshold: 利確を行う閾値 27 | stop_loss_threshold: : 損切りを行う閾値 28 | """ 29 | conn = sqlite3.connect(db_file_name) 30 | 31 | def get_open_price_func(date, code): 32 | """date日におけるcodeの銘柄の初値の取得""" 33 | r = conn.execute('SELECT open FROM prices ' 34 | 'WHERE code = ? AND date <= ? ORDER BY date DESC LIMIT 1', 35 | (code, date)).fetchone() 36 | return r[0] 37 | 38 | def get_close_price_func(date, code): 39 | """date日におけるcodeの銘柄の終値の取得""" 40 | r = conn.execute('SELECT close FROM prices ' 41 | 'WHERE code = ? AND date <= ? ORDER BY date DESC LIMIT 1', 42 | (code, date)).fetchone() 43 | return r[0] 44 | 45 | def get_op_income_df(date): 46 | """date日の出来高が閾値以上であるに四半期決算を公開した銘柄の銘柄コード、 47 | 単元株、date日以前の四半期営業利益の情報、date日以前の四半期決算公表日を 48 | 取得する 49 | """ 50 | return pd.read_sql(""" 51 | WITH target AS ( 52 | SELECT 53 | code, 54 | unit, 55 | term 56 | FROM 57 | quarterly_results 58 | JOIN prices 59 | USING(code, date) 60 | JOIN brands 61 | USING(code) 62 | WHERE 63 | date = :date 64 | AND close * volume > :threshold 65 | AND op_income IS NOT NULL 66 | ) 67 | SELECT 68 | code, 69 | unit, 70 | op_income, 71 | results.term as term 72 | FROM 73 | target 74 | JOIN quarterly_results AS results 75 | USING(code) 76 | WHERE 77 | results.term <= target.term 78 | """, 79 | conn, 80 | params={"date": date, 81 | "threshold": trading_value_threshold}) 82 | 83 | 84 | def check_income_increasing(income): 85 | """3期分の利益の前年同期比が閾値以上かつ単調増加であるかを判断。 86 | 閾値以上単調増加である場合は3期分の前年同期比の平均値を返す。 87 | 条件を満たさない場合は0を返す 88 | """ 89 | if len(income) < 7 or any(income <= 0): 90 | return 0 91 | 92 | t1 = (income.iat[0] - income.iat[4]) / income.iat[4] 93 | t2 = (income.iat[1] - income.iat[5]) / income.iat[5] 94 | t3 = (income.iat[2] - income.iat[6]) / income.iat[6] 95 | 96 | if (t1 > t2) and (t2 > t3) and (t3 > growth_rate_threshold): 97 | return np.average((t1, t2, t3)) 98 | else: 99 | return 0 100 | 101 | def choose_best_stock_to_buy(date): 102 | """date日の購入対象銘柄の銘柄情報・単位株を返す""" 103 | df = get_op_income_df(date) 104 | found_code = None 105 | found_unit = None 106 | max_rate = 0 107 | for code, f in df.groupby("code"): 108 | income = f.sort_values("term", ascending=False)[:7] 109 | rate = check_income_increasing(income["op_income"]) 110 | if rate > max_rate: 111 | max_rate = rate 112 | found_code = code 113 | found_unit = income["unit"].iat[0] 114 | 115 | return found_code, found_unit 116 | 117 | def trade_func(date, portfolio): 118 | """date日の次の営業日の売買内容を決定する関数""" 119 | order_list = [] 120 | 121 | # 売却する銘柄の決定 122 | for code, stock in portfolio.stocks.items(): 123 | current = get_close_price_func(date, code) 124 | rate = (current / stock.average_cost) - 1 125 | if rate >= profit_taking_threshold or rate <= -stop_loss_threshold: 126 | order_list.append( 127 | sim.SellMarketOrder(code, stock.current_count)) 128 | 129 | # 購入する銘柄の決定 130 | code, unit, = choose_best_stock_to_buy(date) 131 | if code: 132 | order_list.append(sim.BuyMarketOrderMoreThan(code, 133 | unit, 134 | minimum_buy_threshold)) 135 | 136 | return order_list 137 | 138 | # シミュレータの呼び出し 139 | return sim.simulate(start_date, end_date, deposit, 140 | trade_func, get_open_price_func, get_close_price_func) 141 | -------------------------------------------------------------------------------- /chapter4_5/simulator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | import sys 4 | import collections 5 | import pandas as pd 6 | import japandas 7 | 8 | 9 | def calc_tax(total_profit): 10 | """儲けに対する税金計算 11 | """ 12 | if total_profit < 0: 13 | return 0 14 | return int(total_profit * 0.20315) 15 | 16 | 17 | def calc_fee(total): 18 | """約定手数料計算(楽天証券の場合) 19 | """ 20 | if total <= 50000: 21 | return 54 22 | elif total <= 100000: 23 | return 97 24 | elif total <= 200000: 25 | return 113 26 | elif total <= 500000: 27 | return 270 28 | elif total <= 1000000: 29 | return 525 30 | elif total <= 1500000: 31 | return 628 32 | elif total <= 30000000: 33 | return 994 34 | else: 35 | return 1050 36 | 37 | 38 | def calc_cost_of_buying(count, price): 39 | """株を買うのに必要なコストと手数料を計算 40 | """ 41 | subtotal = int(count * price) 42 | fee = calc_fee(subtotal) 43 | return subtotal + fee, fee 44 | 45 | 46 | def calc_cost_of_selling(count, price): 47 | """株を売るのに必要なコストと手数料を計算 48 | """ 49 | subtotal = int(count * price) 50 | fee = calc_fee(subtotal) 51 | return fee, fee 52 | 53 | def calc_max_drawdown(prices): 54 | """最大ドローダウンを計算して返す 55 | """ 56 | cummax_ret = prices.cummax() 57 | drawdown = cummax_ret - prices 58 | max_drawdown_date = drawdown.idxmax() 59 | return drawdown[max_drawdown_date] / cummax_ret[max_drawdown_date] 60 | 61 | 62 | def calc_sharp_ratio(returns): 63 | """シャープレシオを計算して返す 64 | """ 65 | # .meanは平均値(=期待値)を求めるメソッド 66 | return returns.mean() / returns.std() 67 | 68 | 69 | def calc_information_ratio(returns, benchmark_retruns): 70 | """インフォメーションレシオを計算して返す 71 | """ 72 | excess_returns = returns - benchmark_retruns 73 | return excess_returns.mean() / excess_returns.std() 74 | 75 | 76 | 77 | def calc_sortino_ratio(returns): 78 | """ソルティノレシオを計算して返す 79 | """ 80 | tdd = math.sqrt(returns.clip_upper(0).pow(2).sum() / returns.size) 81 | return returns.mean() / tdd 82 | 83 | def calc_sortino_bench(returns, benchmark_retruns): 84 | excess_returns = returns - benchmark_retruns 85 | return calc_sortino_ratio(excess_returns) 86 | 87 | def calc_calmar_ratio(prices, returns): 88 | """カルマ―レシオを計算して返す 89 | """ 90 | return returns.mean() / calc_max_drawdown(prices) 91 | 92 | 93 | class OwnedStock(object): 94 | def __init__(self): 95 | self.total_cost = 0 # 取得にかかったコスト(総額) 96 | self.total_count = 0 # 取得した株数(総数) 97 | self.current_count = 0 # 現在保有している株数 98 | self.average_cost = 0 # 平均取得価額 99 | 100 | def append(self, count, cost): 101 | if self.total_count != self.current_count: 102 | self.total_count = self.current_count 103 | self.total_cost = self.current_count * self.average_cost 104 | self.total_cost += cost 105 | self.total_count += count 106 | self.current_count += count 107 | self.average_cost = math.ceil(self.total_cost / self.total_count) 108 | 109 | def remove(self, count): 110 | if self.current_count < count: 111 | raise ValueError("can't remove", self.total_cost, count) 112 | self.current_count -= count 113 | 114 | class Portfolio(object): 115 | 116 | def __init__(self, deposit): 117 | self.deposit = deposit # 現在の預り金 118 | self.amount_of_investment = deposit # 投資総額 119 | self.total_profit = 0 # 総利益(税引き前/損失分相殺済みの値) 120 | self.total_tax = 0 # (源泉徴収)税金合計 121 | self.total_fee = 0 # 手数料合計 122 | self.count_of_trades = 0 # トレード総数 123 | self.count_of_wins = 0 # 勝ちトレード数 124 | self.total_gains = 0 # 総利益(損失分の相殺無しの値) 125 | self.total_losses = 0 # 総損出 126 | 127 | self.stocks = collections.defaultdict(OwnedStock) # 保有銘柄 銘柄コード -> OwnedStock への辞書 128 | 129 | def add_deposit(self, deposit): 130 | """預り金を増やす (= 証券会社に入金) 131 | """ 132 | self.deposit += deposit 133 | self.amount_of_investment += deposit 134 | 135 | def buy_stock(self, code, count, price): 136 | """株を買う 137 | """ 138 | cost, fee = calc_cost_of_buying(count, price) 139 | if cost > self.deposit: 140 | raise ValueError('cost > deposit', cost, self.deposit) 141 | 142 | # 保有株数増加 143 | self.stocks[code].append(count, cost) 144 | 145 | self.deposit -= cost 146 | self.total_fee += fee 147 | 148 | def sell_stock(self, code, count, price): 149 | """株を売る 150 | """ 151 | subtotal = int(count * price) 152 | cost, fee = calc_cost_of_selling(count, price) 153 | if cost > self.deposit + subtotal: 154 | raise ValueError('cost > deposit + subtotal', 155 | cost, self.deposit + subtotal) 156 | 157 | # 保有株数減算 158 | stock = self.stocks[code] 159 | average_cost = stock.average_cost 160 | stock.remove(count) 161 | if stock.current_count == 0: 162 | del self.stocks[code] 163 | 164 | # 儲け計算 165 | profit = int((price - average_cost) * count - cost) 166 | self.total_profit += profit 167 | 168 | # トレード結果保存 169 | self.count_of_trades += 1 170 | if profit >= 0: 171 | self.count_of_wins += 1 172 | self.total_gains += profit 173 | else: 174 | self.total_losses += -profit 175 | 176 | 177 | # 源泉徴収額決定 178 | current_tax = calc_tax(self.total_profit) 179 | withholding = current_tax - self.total_tax 180 | self.total_tax = current_tax 181 | 182 | self.deposit += subtotal - cost - withholding 183 | self.total_fee += fee 184 | 185 | def calc_current_total_price(self, get_current_price_func): 186 | """現在の評価額を返す 187 | """ 188 | stock_price = sum(get_current_price_func(code) 189 | * stock.current_count 190 | for code, stock in self.stocks.items()) 191 | return stock_price + self.deposit 192 | 193 | def calc_winning_percentage(self): 194 | """勝率を返す""" 195 | return (self.count_of_wins / self.count_of_trades) * 100 196 | 197 | def calc_payoff_ratio(self): 198 | """ペイオフレシオを返す 199 | """ 200 | loss = self.count_of_trades - self.count_of_wins 201 | if self.count_of_wins and loss: 202 | ave_gain = self.total_gains / self.count_of_wins 203 | ave_losses = self.total_losses / loss 204 | return ave_gain / ave_losses 205 | else: 206 | return sys.float_info.max 207 | 208 | def calc_profit_factor(self): 209 | """プロフィットファクターを返す 210 | """ 211 | if self.total_losses: 212 | return self.total_gains / self.total_losses 213 | else: 214 | return sys.float_info.max 215 | 216 | 217 | class Order(object): 218 | 219 | def __init__(self, code): 220 | self.code = code 221 | 222 | def execute(self, date, portfolio, get_price_func): 223 | pass 224 | 225 | @classmethod 226 | def default_order_logger(cls, order_type, date, code, count, price, before_deposit, after_deposit): 227 | print("{} {} code:{} count:{} price:{} deposit:{} -> {}".format( 228 | date.strftime('%Y-%m-%d'), 229 | order_type, 230 | code, 231 | count, 232 | price, 233 | before_deposit, 234 | after_deposit 235 | )) 236 | logger = default_order_logger 237 | 238 | class BuyMarketOrderAsPossible(Order): 239 | """残高で買えるだけ買う成行注文 240 | """ 241 | 242 | def __init__(self, code, unit): 243 | super().__init__(code) 244 | self.unit = unit 245 | 246 | def execute(self, date, portfolio, get_price_func): 247 | price = get_price_func(self.code) 248 | count_of_buying_unit = int(portfolio.deposit / price / self.unit) 249 | while count_of_buying_unit: 250 | try: 251 | count = count_of_buying_unit * self.unit 252 | prev_deposit = portfolio.deposit 253 | portfolio.buy_stock(self.code, count, price) 254 | self.logger("BUY", date, self.code, count, price, prev_deposit, portfolio.deposit) 255 | except ValueError: 256 | count_of_buying_unit -= 1 257 | else: 258 | break 259 | 260 | 261 | class BuyMarketOrderMoreThan(Order): 262 | """指定額以上で最小の株数を買う 263 | """ 264 | def __init__(self, code, unit, under_limit): 265 | super().__init__(code) 266 | self.unit = unit 267 | self.under_limit = under_limit 268 | 269 | def execute(self, date, portfolio, get_price_func): 270 | price = get_price_func(self.code) 271 | unit_price = price * self.unit 272 | if unit_price > self.under_limit: 273 | count_of_buying_unit = 1 274 | else: 275 | count_of_buying_unit = int(self.under_limit / unit_price) 276 | while count_of_buying_unit: 277 | try: 278 | count = count_of_buying_unit * self.unit 279 | prev_deposit = portfolio.deposit 280 | portfolio.buy_stock(self.code, count, price) 281 | self.logger("BUY", date, self.code, count, price, prev_deposit, portfolio.deposit) 282 | except ValueError: 283 | count_of_buying_unit -= 1 284 | else: 285 | break 286 | 287 | class SellMarketOrder(Order): 288 | """成行の売り注文 289 | """ 290 | def __init__(self, code, count): 291 | super().__init__(code) 292 | self.count = count 293 | 294 | def execute(self, date, portfolio, get_price_func): 295 | price = get_price_func(self.code) 296 | prev_deposit = portfolio.deposit 297 | portfolio.sell_stock(self.code, self.count, price) 298 | self.logger("SELL", date, self.code, self.count, price, prev_deposit, portfolio.deposit) 299 | 300 | def tse_date_range(start_date, end_date): 301 | tse_business_day = pd.offsets.CustomBusinessDay( 302 | calendar=japandas.TSEHolidayCalendar()) 303 | return pd.date_range(start_date, end_date, 304 | freq=tse_business_day) 305 | 306 | def simulate(start_date, end_date, deposit, trade_func, 307 | get_open_price_func, get_close_price_func): 308 | """ 309 | [start_date, end_date]の範囲内の売買シミュレーションを行う 310 | deposit: 最初の所持金 311 | trade_func: 312 | シミュレーションする取引関数 313 | (引数 date, portfolio でOrderのリストを返す関数) 314 | get_open_price_func: 315 | 指定銘柄コードの指定日の始値を返す関数 (引数 date, code) 316 | get_close_price_func: 317 | 指定銘柄コードの指定日の始値を返す関数 (引数 date, code) 318 | """ 319 | 320 | portfolio = Portfolio(deposit) 321 | 322 | total_price_list = [] 323 | profit_or_loss_list = [] 324 | def record(d): 325 | # 本日(d)の損益などを記録 326 | current_total_price = portfolio.calc_current_total_price( 327 | lambda code: get_close_price_func(d, code)) 328 | total_price_list.append(current_total_price) 329 | profit_or_loss_list.append(current_total_price 330 | - portfolio.amount_of_investment) 331 | 332 | def execute_order(d, orders): 333 | # 本日(d)において注文(orders)をすべて執行する 334 | for order in orders: 335 | order.execute(d, portfolio, 336 | lambda code: get_open_price_func(d, code)) 337 | 338 | order_list = [] 339 | date_range = [pdate.to_pydatetime().date() 340 | for pdate in tse_date_range(start_date, end_date)] 341 | for date in date_range[:-1]: 342 | execute_order(date, order_list) # 前日に行われた注文を執行 343 | order_list = trade_func(date, portfolio) # 明日実行する注文を決定する 344 | record(date) # 損益等の記録 345 | 346 | # 最終日に保有株は全部売却 347 | last_date = date_range[-1] 348 | execute_order(last_date, 349 | [SellMarketOrder(code, stock.current_count) 350 | for code, stock in portfolio.stocks.items()]) 351 | record(last_date) 352 | 353 | return portfolio, \ 354 | pd.DataFrame(data={'price': total_price_list, 355 | 'profit': profit_or_loss_list}, 356 | index=pd.DatetimeIndex(date_range)) 357 | --------------------------------------------------------------------------------