turtlechanのブログ

無知の私がLinuxをいじりながら書いていくブログ

Alpha Chart の株価更新に無尽蔵の日足データを使いたい

Alpha Chart の試用期間が終わるとデータ更新が「ダウンロード済みデータで更新」しかなくなります。
株価データ倉庫 の日足データが使えるが、更新が週一(日曜)なため毎日更新できない。
そこで、毎日更新している 無尽蔵 のデータを使えればいいなという話。

 

先に今回作ったやつ載せておきます。
ファイル: turtlechan_apcconv_v1.zip
解凍して '$ python apcconv.py' で起動。無尽蔵 の zipファイル を指定すればいいです。

起動画面

※ Python2 でも 3 でも動くはず。

はじめに

Alpha Chart で読み込める 株価データ倉庫 のtxtファイルを見てみたいと思います。
2020/01/06 のデータを参考にしています。

株価データ倉庫

ファイル名は 'y200106.txt' となっている。'y%y%m%d.txt' のようです。

中身はtsv形式のようです。タブで要素が区切られているやつですね。
一行目が、'20200106' なので '%Y%m%d'
二行目以降は

銘柄コード\t銘柄名\t始値\t高値\t安値\t終値\t出来高\r\n

 

以上の形式が Alpha Chart で読み込めるので、無尽蔵のデータをこのように整形すれば読み込める。
ついでに、無尽蔵 のcsvファイルも見ておきます。

 

無尽蔵

ファイル名は 'T200106.csv' なので 'T%y%m%d.csv'。

中身はcsv形式。カンマで要素が区切られているやつ。
一行目から以下の形式。

日付,銘柄コード,取引所別の数字,銘柄コード 銘柄名,始値,高値,安値,終値,出来高,取引所名\r\n

 

これを整形すれば良いのです。ちょろいですね。
無尽蔵の方には先物とか含まれていないけど、ないものはしかたないので無視。

今回作ったやつ

apcconv.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import zipfile
# Python のバージョンによって tk の分岐
if sys.version_info.major == 2:
    import Tkinter as tk
    import tkFileDialog as tkfd
elif sys.version_info.major == 3:
    import tkinter as tk
    import tkinter.filedialog as tkfd


class Converter(tk.Frame):

    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.tk.call('wm', 'iconphoto', master._w, tk.PhotoImage(file='favicon.gif'))
        master.title('Turtlechan apcConv(仮)')
        master.geometry('+50+50')
        self.pack()
        self.create_widget()

    def create_widget(self):
        # ラベル
        self.lbl = tk.Label(self, text='入力:')
        self.strv_status = tk.StringVar()
        self.lbl_status = tk.Label(self, fg='#888', bd=1, relief=tk.SUNKEN, anchor=tk.E, textvariable=self.strv_status)
        # テキストボックス
        self.strv = tk.StringVar()
        self.ent = tk.Entry(self, width=60, textvariable=self.strv)
        self.strv.set(os.path.abspath(os.path.dirname(__file__)))
        # ボタン
        self.btn_fd = tk.Button(self, text='ファイル選択', command=self.push_fd)
        self.btn_conv = tk.Button(self, text='Convert', command=self.push_conv)
        # 配置
        self.lbl.grid(row=0, column=0)
        self.ent.grid(row=0, column=1)
        self.btn_fd.grid(row=0, column=2)
        self.btn_conv.grid(row=100, column=1)
        self.lbl_status.grid(row=102, column=0, columnspan=3, sticky=tk.W + tk.E + tk.N + tk.S)

    def push_fd(self):
        # ファイル選択ウィンドウ
        self.fp = tkfd.askopenfilename(filetypes=[('ZIPアーカイブ', '*.zip')])
        if len(self.fp) == 0:
            return
        else:
            self.strv.set(self.fp)

    def push_conv(self):
        # zipアーカイブの読み込み
        with zipfile.ZipFile(self.strv.get(), 'r') as zf:
            # Python のバージョンによって分岐
            data = [row.decode('shift-jis').split(',') if sys.version_info.major == 3 else row.split(',') for row in zf.open(zf.namelist()[0])]
        data = ['{0}\t{1}\t{2[0]}\t{2[1]}\t{2[2]}\t{2[3]}\t{3}'.format(row[1], row[3][4:].replace(' ', ''), row[4:8], float(row[8]) / 10000000 if int(row[1]) < 1300 else float(row[8]) / 1000) for row in data if row[8] != '0']
        # 書き出し
        with open(os.path.join(os.path.dirname(self.strv.get()), os.path.basename(self.strv.get()).split('.')[0].replace('T', 'y') + '.txt'), 'w') as f:
            f.write(os.path.basename(self.strv.get()).split('.')[0].replace('T', '20') + '\n')
            f.write('\r\n'.join(data))
        self.strv_status.set(self.strv.get() + '\t変換完了')


def main():
    root = tk.Tk()
    converter = Converter(root)
    converter.mainloop()


if __name__ == '__main__':
    main()

おわりに

Python3 で作ったファイルは Alpha Chart に読み込ませるとき銘柄名が文字化けします。
更新自体に問題はなさそうです。Python3 の文字列の扱い分からないんだよなぁ。。。

 

何かの参考になれば幸いです。

株価データをダウンロードする Pythonスクリプト 書いたよ!

今年の GW ももう終わりですね。
時間があったので、株価データをダウンロードする Pythonスクリプト 書いてみました。
私はまともに GUI を作ったことがなかったので、苦戦しましたがとりあえず出来たのでここで紹介させて下さい。

※ Python2 で書いています。

はじめに

Python2 で書いているので Python3 で動かすには一部書き直す必要があります。
Linux でしか動作確認していません。そしてデバッグしていないので不具合がある可能性が高いです。

依存モジュール(サードパーティ)は以下。

requests は pip でも使ってインストールして下さい。
Tkinter は 'apt-get install tk-dev' とかでインストール出来たかと思います。

作ったやつ

ファイル: turtlechan_price_dl_v1.zip

もし興味のある人がいればダウンロードして使ってみて下さい。
zip で圧縮しているので解凍して使ってください。
解凍先に移動して以下のコマンドで起動できるはずです。

$ python downloader.py

とりあえず GUI は以下。

起動時

ウィンドウ01

チェックボックスにチェックを入れて「Download」を押すと確認ダイアログが表示される。
OK でダウンロードが開始される。

ウィンドウ02

ダウンロード中は赤線引いたところが更新されていくので動いているのが確認できると思う。

ウィンドウ03

保存先は指定できません、downloader.py と同じディレクトリに mujinzouディレクトリ等を作成して、その中に保存する感じです。
すでにダウンロードファイルが存在していた場合にはダウンロード処理はパスします。

営業日の判定は workday_txt ディレクトリ内にあるテキストファイルを読み込んで判定しています。

wokday_txt に置いておきます。
turtlechan_price_dl_v1.zip には含めてあるのでわざわざダウンロードする必要はないです。

ソースコード

誰もダウンロードして中を覗いてくれないと寂しいので、ソースコードを書いておきます。

workday_txt

workday_txt ディレクトリ内のファイルは営業日が書かれているだけ。
以下に例を書いておく、ファイル名は '西暦.txt' である必要がある。

2000.txt
2000-01-04
2000-01-05
2000-01-06
2000-01-07
2000-01-11
~ 省略 ~
2000-12-26
2000-12-27
2000-12-28
2000-12-29

Python スクリプト

dataservice.py は、各サイトのダウンロードURLとかを生成してもらうためのもの。
ちなみに株価データ倉庫の zip ファイル名の先頭文字が 'd' のやつには対応できてない。

dataservice.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import os
import datetime
import zipfile


class Mujinzou(object):
    NAME = 'mujinzou'
    DOMAIN = 'http://mujinzou.com'
    EXTEND = 'zip'
    ARCHIVE = 'T{0:%y%m%d}.{1}'
    PATH = '/k_data/{0:%Y}/{0:%y}_{0:%m}/{1}'
    DIR = os.path.join(os.path.dirname(__file__), NAME)

    def __init__(self, **kwargs):
        if 'date' in kwargs:
            self.date = kwargs['date']
            extend = self.__class__.EXTEND if self.date.year > 2014 else 'lzh'  # 2014年までlzh形式
            self.archive = self.__class__.ARCHIVE.format(self.date, extend)
        elif 'name' in kwargs:
            fn = kwargs['name'].split('.')
            self.date = datetime.datetime.strptime(fn[0][1:], '%y%m%d').date()
            extend = fn[-1]
            self.archive = kwargs['name']
        self.path = self.__class__.PATH.format(self.date, self.archive)
        self.domain = self.__class__.DOMAIN if self.date.year > 2018 else 'http://souba-data.com'  # 2019年からドメインが変更

    @property
    def url(self):
        return self.domain + self.path

    def exists(self):
        return os.path.isfile(os.path.join(self.__class__.DIR, self.archive))

    def load(self):
        with zipfile.ZipFile(os.path.join(self.__class__.DIR, self.archive), 'r') as zf:
            data = [row.split(',') for row in zf.open(zf.namelist()[0])]
        return {row[1]: tuple(row[4:9]) for row in data}


class Stock_databox(object):
    NAME = 'stock-databox'
    DOMAIN = 'http://stock-databox.net'
    EXTEND = 'zip'
    ARCHIVE = 'y{0:%y%m%d}.{1}'
    PATH = '/{1}'
    DIR = os.path.join(os.path.dirname(__file__), NAME)

    def __init__(self, **kwargs):
        if 'date' in kwargs:
            self.date = kwargs['date']
            self.archive = self.__class__.ARCHIVE.format(self.date, self.__class__.EXTEND)
        elif 'name' in kwargs:
            fn = kwargs['name'].split('.')
            self.date = datetime.datetime.strptime(fn[0][1:], '%y%m%d').date()
            self.archive = kwargs['name']
        self.path = self.__class__.PATH.format(self.date, self.archive)

    @property
    def url(self):
        return self.__class__.DOMAIN + self.path

    def exists(self):
        return os.path.isfile(os.path.join(self.__class__.DIR, self.archive))

    def load(self):
        with zipfile.ZipFile(os.path.join(self.__class__.DIR, self.archive), 'r') as zf:
            data = [row.strip().split('\t') for row in zf.open(zf.namelist()[0])]
        return {row[0]: (row[2], row[3], row[4], row[5], '{0:.0f}'.format(float(row[6])*100)) for row in data[1:]}

downloader.py は、営業日の確認処理担当の Workdayクラス、GUI生成とダウンロード処理担当の Downloaderクラス。

downloader.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import os
from time import sleep
import datetime
import threading
import requests
import Tkinter as tk
import tkMessageBox as tkmsg
from dataservice import Mujinzou, Stock_databox


SAVE_DIR = os.path.dirname(__file__)  # 保存先のディレクトリ
WAIT_TIME = 1  # サーバーへの負荷を考慮
USER_AGENT = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'}  # User-Agent 多分なんでもいい


class Downloader(tk.Frame):

    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.tk.call('wm', 'iconphoto', master._w, tk.PhotoImage(file='favicon.gif'))
        master.title('Turtlechan Price Downloader(仮)')
        master.geometry('200x200+50+50')
        master.minsize(width=200, height=200)
        master.maxsize(width=200, height=200)
        self.pack()
        self.create_widget()
        self.ssn = requests.Session()
        self.ssn.headers.update(USER_AGENT)

    def create_widget(self):
        # ラベル
        self.lbl_start = tk.Label(self, text='開始日')
        self.lbl_end = tk.Label(self, text='終了日')
        self.strv_status = tk.StringVar()
        self.lbl_status = tk.Label(self, text='取得先を選択してください。', fg='#888', bd=1, relief=tk.SUNKEN, anchor=tk.E, textvariable=self.strv_status)
        # テキストボックス
        self.ent_start = tk.Entry(self)
        self.ent_end = tk.Entry(self)
        self.ent_start.insert(tk.END, str(datetime.date.today() - datetime.timedelta(days=20)))
        self.ent_end.insert(tk.END, str(datetime.date.today()))
        # チェックボックス
        self.frame_service = tk.LabelFrame(self, text='取得先')
        self.boolv_muji = tk.IntVar()
        self.boolv_stkdb = tk.IntVar()
        self.boolv_muji.set(0)
        self.boolv_stkdb.set(0)
        self.chk_muji = tk.Checkbutton(self.frame_service, text='無尽蔵', variable=self.boolv_muji)
        self.chk_stkdb = tk.Checkbutton(self.frame_service, text='株価データ倉庫', variable=self.boolv_stkdb)
        self.chk_muji.grid(row=0, column=0)
        self.chk_stkdb.grid(row=0, column=1)
        # ボタン
        self.btn_quit = tk.Button(self, text='Quit', command=self.quit)
        self.btn_dl = tk.Button(self, text='Download', command=self.push_dl)
        # 配置
        self.lbl_start.grid(row=0, column=0)
        self.ent_start.grid(row=0, column=1)
        self.ent_end.grid(row=1, column=1)
        self.lbl_end.grid(row=1, column=0)
        self.frame_service.grid(row=2, column=0, pady=10, columnspan=2)
        self.btn_dl.grid(row=100, column=1, sticky=tk.E)
        self.btn_quit.grid(row=101, column=1, sticky=tk.E)
        self.lbl_status.grid(row=102, column=0, columnspan=2, pady=5, sticky=tk.W + tk.E + tk.N + tk.S)

    def __dl(self, Dataservice_obj):
        if Dataservice_obj.exists():
            return 0
        if not os.path.isdir(Dataservice_obj.DIR):
            os.makedirs(Dataservice_obj.DIR)
        ds = Dataservice_obj
        res = self.ssn.get(ds.url)
        try:  # ダウンロード先が存在しなかった場合
            res.raise_for_status()
        except requests.exceptions.HTTPError:
            self.strv_status.set('{0} は存在しないみたい。'.format(ds.url))
            return 0
        self.strv_status.set('Downloading...\t{0} '.format(Dataservice_obj.archive))
        with open(os.path.join(ds.DIR, ds.archive), 'wb') as f:
            f.write(res.content)
        return 1

    def __dl_loop(self, start, end, muji, stkdb):
        ''' 並列処理のために作った関数、他にいい方法ないの? '''
        workday = Workday()
        wait = float(WAIT_TIME) / (muji + stkdb)
        for date in workday.xrange(start, end):
            tmp = 0
            tmp += self.__dl(Mujinzou(date=date)) if muji else 0
            tmp += self.__dl(Stock_databox(date=date)) if stkdb else 0
            sleep(wait * tmp)
        self.strv_status.set('ダウンロード完了。')

    def push_dl(self):
        start = datetime.datetime.strptime(self.ent_start.get(), '%Y-%m-%d').date()
        end = datetime.datetime.strptime(self.ent_end.get(), '%Y-%m-%d').date()
        muji = self.boolv_muji.get()
        stkdb = self.boolv_stkdb.get()
        if not muji and not stkdb:  # チェックボックスがからの場合何もしない
            return
        if not tkmsg.askokcancel('Download', '{0} 〜 {1}\nダウンロードを開始します。'.format(str(start), str(end))):  # 確認ダイアログ
            return
        # 時間の掛かる処理は並列処理しないとウィンドウが更新されない
        thread = threading.Thread(target=self.__dl_loop, args=(start, end, muji, stkdb))
        thread.start()


class Workday(object):
    TXT_DIR = os.path.join(os.path.dirname(__file__), 'workday_txt')

    def __init__(self):
        self.wd = set()
        for txtfile in os.listdir(self.__class__.TXT_DIR):
            with open(os.path.join(self.__class__.TXT_DIR, txtfile), 'rb') as f:
                self.wd.update(row.strip() for row in f.readlines())

    def work(self, Date_obj):
        return str(Date_obj) in self.wd

    def near(self, Date_obj, old=True):
        dt = Date_obj
        if not self.work(dt):
            while True:
                dt = dt - datetime.timedelta(days=1) if old else dt + datetime.timedelta(days=1)
                if self.work(dt):
                    break
        return dt

    def xrange(self, start, end):
        wd_sorted = sorted(self.wd)
        return (datetime.datetime.strptime(date, '%Y-%m-%d').date() for date in wd_sorted[wd_sorted.index(str(self.near(start, old=False))): wd_sorted.index(str(self.near(end, old=True))) + 1])


def main():
    root = tk.Tk()
    dlr = Downloader(root)
    dlr.mainloop()


if __name__ == '__main__':
    main()

おわりに

ソースコード見たけど、その書き方おかしい」とかあったら教えてください。
また、「使ってみたよ」とか「とりあえず読んだ」、「動かない」とか何でもいいんで教えてくださるとうれしいです。
Pythonプログラミング勉強中なので、誰かの干渉があると励みになります。よろしくお願いします。

何かの参考になれば幸いです。

 

【Linux】Virtualbox で XPモード を動かす

少し古い話題ですが、Virtualbox で XP Mode を動かせるんですよね。
Windows を用いなくても出来そうだったので、Linux上で行う操作をまとめてみます。
ちなみに、Linux上で XP Mode を動かすのはライセンス違反に当たります。やらないように。

Linuxのみでも XP Mode を動かせるんじゃね?という妄想記事です。

はじめに

Virtualbox で XP Mode を動かすっていうと、Windows上での操作ばっかりなんですよね。まぁ、それ以外はライセンス違反なんで当たり前なんですけど。

しかし、当然 Linux にも Virtualbox はあるわけで、wine というWindowsエミュもあるのでイメージファイルの抽出もできないことはないだろう。

準備

Linux上で行うにあたり必要になるものは以下であろう。

  1. Virtualbox
  2. XP Mode のインストーラ
  3. p7zip-full
  4. wine

Virtualbox

Virtualbox で XP Mode を動かすのだから当然必要。

インストール方法は検索すればいくらでも出てくる。
バージョンは 5.2 でいいだろう。

Debian 9 Stretch でインストールする場合(ざっくり)。

$ wget https://www.virtualbox.org/download/oracle_vbox_2016.asc
$ wget https://www.virtualbox.org/download/oracle_vbox.asc
$ sudo apt-key add oracle_vbox_2016.asc
$ sudo apt-key add oracle_vbox.asc
$ sudo sh -c "echo 'deb https://download.virtualbox.org/virtualbox/debian stretch contrib' >> /etc/apt/sources.list.d/virtualbox.list"
$ sudo apt-get update
$ sudo apt-get install -y virtualbox-5.2

XP Mode のインストーラ

こいつから XP Mode のイメージファイルを抽出するので当然必要。

今現在は、まだ以下からダウンロードすることができる。
Download Windows XP Mode from Official Microsoft Download Center

ホームディレクトリ(~)にでも保存しておけばよいだろう。
以降、'~/WindowsXPMode_ja-jp.exe' に保存したと想定して書きます。

p7zip-full

XP Mode のインストーラーを展開(解凍)するのに 7za コマンドを使うのでインストールしておこう。

$ sudo apt-get install -y p7zip-full

もしかしたら、デフォでインストール済かもしれない。

wine

XP Mode のインストーラー展開後、msi ファイルを展開するのに wine が必要。
バージョンは何でもよいだろう。エミュ環境は 32bit の方が都合が良さそうだが、64bit ですでに構築していても大丈夫だと思われる。

$ sudo apt-get install -y wine

今回は以下のコマンドで 32bit 環境を作ったと想定して進める。

$ WINEARCH=win32 winecfg

Virtualbox で動く XP Mode のイメージファイル作成

Linux上で XP Mode を動かす妄想を始めます。

XP Mode のイメージファイル抽出

まず XP Mode のインストーラーを展開(解凍)する(カレントディレクトリ下に展開される)。

$ 7za x ~/WindowsXPMode_ja-jp.exe

展開されて出てきた sources ディレクトリ内の xpminstl32.msi を展開する。

$ msiexec /a ./sources/xpminstl32.msi /qn

'err:mscoree:LoadLibraryShim error reading registry key for installroot'
などと表示されるだろうが気にしない。

目的のファイル(イメージファイル)は 'Windows XP Mode base.vhd' だ。
きっと '~/.wine/drive_c/Program Files/Windows XP Mode/' 内に存在するだろう。

目的のファイルをホームディレクトリ(~)に移動する。その際ファイル名にスペースがあると面倒なのでファイル名も変更しておく。

$ mv ~/.wine/drive_c/Program\ Files/Windows\ XP\ Mode/Windows\ XP\ Mode\ base.vhd ~/Windows_XP_Mode_base.vhd

その他のファイルは不要なので削除しておく。

$ rm -rf ~/.wine/drive_c/Program\ Files/Windows\ XP\ Mode

イメージファイルを変換

上記で抽出したイメージファイル(vhd)を Virtualbox用(vdi) に変換する必要がある。
vboxmange コマンドで行う。Virtualboxをインストールしていれば使える。

$ vboxmanage clonehd ~/Windows_XP_Mode_base.vhd ~/Windows_XP_Mode_base.vdi --format vdi

ホームディレクトリ(~)内に'Windows_XP_Mode_base.vdi'が出来るはずだ。

XP Mode 仮想マシン

Virtualbox仮想マシン作成で「すでにある仮想ハードディスクファイルを使用する」を選択。
上記で作成したイメージファイルを指定してあげればいい。

これについては検索すればいくらでも出てくる。

XP Mode 起動時やるべきこと

仮想マシンを起動すると、XPのセットアップが起動するが Virtual PC統合コンポーネント が悪さをしてちょいちょいフリーズする。
何度もリセットしながら進める以外ない。セットアップが終わったら即座に Virtual PC統合コンポーネント をアンインストールするべきだろう。

そして再起動時に F8 連打でセーフモードに入り Virtualbox の Guest Additions をインストールしておこう。

アクティベーション

bios内の特定の位置に特定の文字列があれば認証されるらしい。

Virtualboxに対応する bios が必要そうなのでVirtualbox内に Linux をインストールして抜き出す。

Virtualbox内のLinuxで以下コマンド。

$ head -c 1048576 /dev/mem | tail -c 65536 > vboxbios.bin

上記で作成した vboxbios.bin を何とかしてホストのLinuxにもってくる。

バイナリエディタで vboxbios.bin を書き換える。この程度ならvimでもいけるだろう。

$ vim -b vboxbios.binのパス
:%!xxd  vim内のコマンド
# 0x908A(09080行)からの値を以下に書き換え
#                          5769 6E64 6F77
# 735F 5669 7274 7561 6C5F 5850 5F46 3931
# 3631 4438 4537 4643 4331 3144 4442 4641
# 4133 3639 3835 3644 3839 3539 33
:%!xxd -r  vim内のコマンド
:wq  vim内のコマンド

XP Mode の仮想マシンが vboxbios.bin を読み込むようにする。

$ vboxmanage setextradata 'XP Mode 仮想マシン名' VBoxInternal/Devices/pcbios/0/Config/BiosRom 'vboxbios.binの絶対パス'

上記の vboxmange コマンドが気に食わない場合は XP Mode 仮想マシンディレクトリ内にある *.vbox ファイルを編集する。

テキストエディタで開いて <ExtraData> 内に書き込めばいい。
以下の赤字のように書けばいい。

*.vbox
~ 省略 ~
<ExtraData>
  ~ 省略 ~
  <ExtraDataItem name="VBoxInternal/Devices/pcbios/0/Config/BiosRom" value="vboxbios.binの絶対パス"/>
</ExtraData>
~ 省略 ~

XP Mode の仮想マシンを起動してコマンドプロンプトで以下のコマンドで認証の確認ができるらしい。

c:\windows\system32\oobe\msoobe /a

おわりに

今更XPに需要があるようには思いませんが、セキュリティ面では不満はあるものの軽量で使いやすいOSでしたよね。
今ではXPで動くソフトも減ったのではないでしょうか?
ちなみに私が初めてパソコンを購入したときはXPでした。

 

何かの参考になれば幸いです。