株価予測練習ツールを作成したのでご紹介します。
ブログメニューの倉庫のなかに保管しました。
Note
動作環境:Windows10、Google Chrome(FileSystem API対応)
本ツールは株職人、相場師朗氏の著書「一生モノの株のトリセツ」から着想を得ました。第一章「キャベツの千切り1000回、株の練習は3000回」では、チャートをたくさん見て、深く考え、工夫することがとても大切であると解説されています。私も見よう見まねで株の練習を始めてみたのですが、どうしても前日何を考えどのような予測をしていたのか忘れてしまいます。2、3個の銘柄なら覚えていられるかもしれませんが … もしかして監視対象銘柄のチャートはすべて暗記してしまうくらい読み込まないといけないのかな、と思ったりもしましたが、ちょっと無理っぽいです。そこで自分がどのような予測をしたのかを残せるようなツールを作成しました。
とはいうものの実際はただの表です。作っている最中に「あれ?ひょっとしてエクセルでいいのでは?」と元も子もないような考えが浮かんできたりしましたが「それはそれこれはこれ」と呪文を唱えながらなんとか完成させました。
このツール自体はあまりおもしろいものではないので今回はツールの紹介に加え、ツールを作成する過程で学んだことを書きたいと思います。
お題
ローカルPC上でファイルシステムを使用する
株の値動き予測(high/flat/low)とメモを保存する方法としてFileSystem APIを使うことにしました。ちなみにFileSystem APIは現在Google Chromeにだけ実装されているようです。
Filesystem & FileWriter API – Can I use…
FileSystem APIは以前Androidアプリを作ったときに使ったことがあったので、そのままいけるかと思いましたが、しょっぱなから引っ掛かりました。requestFileSystem()でエラー発生です。
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; var fsDirEntry; // ファイルシステムのルートディレクトリ(DirectoryEntryオブジェクト) /*** * ファイルシステム初期化 */ function initFileSystem(callback) { window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, // cordovaの場合 // window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) { console.log('Opened file system: ' + fs.name); fsDirEntry = fs.root; if (callback) callback(); }, function(e) {errMsg(e, "requestFileSystem");} ); } function errMsg(e, funcName) { var msg = "*** ERROR ***\n"; msg += "[FUNC] " + funcName + "\n"; msg += "[CODE] " + e.name + "(" + e.code + ")" + "\n"; msg += "[DESC] " + e.message; // alert(msg); console.log(msg); } window.onload = function() { // ファイルシステム初期化 initFileSystem(); };
console出力
エラーコード18、SecurityErrorです。
Webで検索したところMDNのFileErrorのページで解説されていました。引用します。
Don’t run your app from file://
For security reasons, browsers do not allow you to run your app from file://. In fact, many of the powerful storage APIs (such as File System, BlobBuilder, and FileReader) throw errors if you run the app locally from file://. When you’re just testing your app, and you don’t want to set up a web server, you can bypass the security restriction on Chrome. Just start Chrome with the –allow-file-access-from-files flag. Use the flag only for testing purposes.
上記の引用を翻訳アプリで翻訳しました。
具体的にどのようなケースを想定して禁止にしているのか知りたかったのですがちょっと調べられませんでした。Web上からダウンロードしたアプリをPC上で動かすとき、アプリがPC上のファイルを勝手にいじれないようにするためでしょうか?
サーバにファイルをアップしてそこでコーディングやデバッグをすれば問題なさそうですが、やっぱりローカルPC上で作業したいと思い、上記の解説どおり–allow-file-from-filesフラグを付けてChromeを起動し、ローカルPC上で作業することにしました。その手順を以下に記します。
ローカル開発専用のChromeのショートカットを作成する
「このフラグは、テスト目的でのみ使用してください」ということですので、開発中はフラグ付きで、通常時や開発終了後はフラグなしでChromeを使いたいところです。ツールをいじるときに毎回フラグを設定し、また戻す、というのはめんどくさいですしフラグ名もなんか長いということで、ローカルで開発を行うときだけ使用するChromeのショートカットを作成し、それにフラグを付けて使うことにしました。
ショートカットは、たとえば、
(2)その中からChromeアイコンを右クリックしてポップアップメニューを表示させる
(3)その中からプロパティを選択してダイアログを表示させる
(4)「ファイルの場所を開く」ボタンを押してエクスプローラを表示させる
(5)そこにあるchrome.exeファイルを右クリックもしくは右クリック+ドラッグしてポップアップメニューを表示させ、ショートカットを選択する
または、
(2)「よく使うアプリ」や「G」のインデックスからGoogle Chromeを探してドラッグ&ドロップ
などの方法で作成できます。
ショートカットにフラグを設定する
作成したショートカットに適当な名前を付けてフラグ付きであることを忘れないようにします。次にショートカットを右クリックしてポップアップメニューを表示させ、プロパティを選択します。表示されたダイアログのリンク先にフラグを追加します。
実際の作業は、もともと記述されているchrome.exeへのパスの後に半角スペースを入れた後「–allow-file-access-from-files」をコピペします。
リンク先(例)
起動時の注意点
フラグ付きとフラグなしのChromeを切り替えて使うとき、すでにChromeが立ち上がっている場合は、いったんすべてのChromeを終了させないとフラグが反映されません。
とは言っても閲覧中または参照中のWebページを閉じたくないときってありますよね。そんなときはChromeの拡張機能OneTabがおすすめです。この場合に限らず普段使いとしても超おすすめで、Chromeに標準装備すべき機能だと思います。ぜひお試しください。
これでローカルPC上でもファイルシステムを使用できる環境ができました。
ラジオボタンのチェックをループを使わずに取得する
本ツールは3択のラジオボタン(high/flat/low)を株の銘柄の数だけ持っています。
Saveボタンを押したときにすべての銘柄についてラジオボタンのチェック項目を取得してファイルに保存したいと考えていました。
ここで、ラジオボタンのチェック項目を取得する単純なサンプルを示します。とりあえずラジオボタンのグループを3つ(A, B, C)用意しました。
HTML
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>radio button</title> <script src="app.js"></script> </head> <body> <div> <span>A:</span> <input type="radio" name="r0" value="0">high <input type="radio" name="r0" value="1" checked>flat <input type="radio" name="r0" value="2">low </div> <div> <span>B:</span> <input type="radio" name="r1" value="0">high <input type="radio" name="r1" value="1" checked>flat <input type="radio" name="r1" value="2">low </div> <div> <span>C:</span> <input type="radio" name="r2" value="0">high <input type="radio" name="r2" value="1" checked>flat <input type="radio" name="r2" value="2">low </div> <button id="check">チェック</button> <span id="result"></span> </body> </html>
Script
window.onload = function() { var elem = document.getElementById("check"); elem.addEventListener("click", checkValue); };
一般的な方法で書くと以下のような感じでしょうか。forループ内でラジオボタンのchecked属性の値をif文で判定し、チェックされている要素を見つけます。
Script
function checkValue(e) { var price = ["high", "flat", "low"]; var names = document.getElementsByName("r0"); for (var i=0; i<names.length; i++) { if (names[i].checked) { var r0 = parseInt(names[i].value, 10); break; } } var names = document.getElementsByName("r1"); for (var i=0; i<names.length; i++) { if (names[i].checked) { var r1 = parseInt(names[i].value, 10); break; } } var names = document.getElementsByName("r2"); for (var i=0; i<names.length; i++) { if (names[i].checked) { var r2 = parseInt(names[i].value, 10); break; } } var elem = document.getElementById("result"); elem.textContent = "A: " + price[r0] + ", " + "B: " + price[r1] + ", " + "C: " + price[r2]; }
上記をがんばってまとめると以下のように書けます。基本的な部分は同じですが、だいぶコンパクトになりました。
Script
function checkValue(e) { var price = ["high", "flat", "low"]; var r = [], ROWS = 3; for (var j=0; j<ROWS; j++) { var names = document.getElementsByName("r" + j); for (var i=0; i<names.length; i++) { if (names[i].checked) { var value = parseInt(names[i].value, 10); break; } } r[j] = value; } var elem = document.getElementById("result"); elem.textContent = "A: " + price[r[0]] + ", " + "B: " + price[r[1]] + ", " + "C: " + price[r[2]]; }
上記の方法でまったく問題ないのですが、あるときとてもいい感じの方法を思いつきました。
Script
function checkValue(e) { var price = ["high", "flat", "low"]; var names = document.querySelectorAll("input[type='radio']:checked"); var r0 = parseInt(names[0].value, 10); var r1 = parseInt(names[1].value, 10); var r2 = parseInt(names[2].value, 10); var elem = document.getElementById("result"); elem.textContent = "A: " + price[r0] + ", " + "B: " + price[r1] + ", " + "C: " + price[r2]; }
querySelectorAll()のおかげでforループとif文がなくなりました。とても見やすくなったと思います。querySelector/querySelectorAllってけっこうなんでもありなんですね(誉め言葉)。
CSVファイルを扱う
今回CSVファイルを初めて扱いました。まず、扱ってみて思ったことを書いてみます。
- CSVファイルといえばMicroSoft Excel。私の使っているバージョンは2013(15.0.4981.1000)。「名前を付けて保存」のとき「ファイルの種類」で「CSV(カンマ区切り)(*.csv)」を選択することができるが、保存するとBOM(byte order mark)が削除される。
- HTML5 FileAPIのreadAsText()メソッドによりutf-8形式でファイルを読み込むとき、CSVファイルにBOMが付いていないと文字化けする。
- メモ帳などでCSVファイルを保存し直すとBOMを付けてくれる。
- CSVファイルの文字列中の改行コードをちゃんと扱おうとすると一気にたいへんになる。今回はパス。ツールでは文字列中の改行は取り除く。文字列をダブルクォーテーションで囲うことで行の終わりの改行と区別するみたいだけど、文字列中にダブルクォーテーションがあるケースも考えられるしCSVの仕様をよくわかっていないと危険かも。
- 書き出す場合は項目をカンマでつなげて行の最後に改行コードをくっつけるだけなので簡単(たぶん)。
CSVファイルの文字列データを二次元配列に格納するコードはこんな感じになりました。上記で述べているように文字列中の改行には対応していません。
Script
var sysTbl = []; app.createSysTbl = function(str) { // strはcsvファイルの文字列データ sysTbl.length = 0; str = str.replace(/\r\n/g, "\n"); // 改行文字を"\n"に統一 str = str.replace(/^\s+|\s+$/g, ""); // 端の空白や空行を削除 var lines = str.split(/\n/g); // 改行文字で分割 lines.forEach(function(line, idx) { // 一行づつ処理する sysTbl[idx] = new Array(); // 二次元配列を生成 [].push.apply(sysTbl[idx], line.split(/,|\t/g)); // 一行のデータを区切り文字で分割し、それを二次元配列にpush }); };
CVSファイルに書き出すデータを作成するコードはこんな感じです。
Script
var content = ""; sysTbl.forEach(function(sd) { content += sd.join(","); content += "\r\n"; });
アップロードとダウンロード
CSVファイルのアップロードは<input type=”file”>から行っています。この要素をクリックするだけでファイル選択のダイアログが表示され、ローカルPC上のファイルにアクセスできます。便利ですね。ファイルを選択してダイアログを閉じるとchangeイベントが発生するのでイベントリスナでこれを受け取りアップロードの処理を行います。
Script
document.getElementById("upload") .addEventListener('change', function(e) { if (this.files.length) { app.upload(this.files[0]); } this.value = ""; // 同一ファイル選択時も処理を行う }); app.upload = function(file) { var divs = file.name.split('.'); if (divs[divs.length -1].toLowerCase() === "csv") { var fr = new FileReader(); fr.readAsText(file, "utf-8"); // ファイルの読み込み fr.addEventListener('load', function() { // 読み込み完了 app.createSysTbl(fr.result); // 読み込んだ文字列がfr.resultに app.createUI(); // 格納されるのでこれを使ってUIに表示 }); fr.addEventListener('error', function(e) { // エラー処理 errMsg(e.target.error, "readAsText"); }); } };
ダウンロードはhtmlのa要素でDownLoadボタンを作成しdownload属性にファイル名を設定しています。スクリプトではCSVファイルのデータをBlob(Binary Large OBject)に変換し、そのオブジェクトURLをa要素のhref属性に設定します。このa要素のクリックイベントのリスナを抜けた後にダウンロードが開始されます。
HTML
<a id="download" target="_blank" download="stocktool.csv">DownLoad</a>
Script
document.getElementById("download") .addEventListener('click', function(e) { app.download(); }); app.download = function() { if (!FS_ENABLE) return; var len = sysTbl.length; if (len === 0) return; app.save(false); var content = ""; sysTbl.forEach(function(sd) { // 二次元配列をひとつの文字列にする content += sd.join(","); content += "\r\n"; }); var bom = new Uint8Array([0xEF, 0xBB, 0xBF]); var blob = new Blob([bom, content], {"type": "text/csv"}); if (window.navigator.msSaveBlob) { // IE window.navigator.msSaveBlob(blob, "stocktool.csv"); } else { if (objectURL) { window.URL.revokeObjectURL(objectURL); // 解放 } var elem = document.getElementById("download"); objectURL = window.URL.createObjectURL(blob); elem.href = objectURL; } };
ファイルAPI
Cordovaのファイル操作プラグインcordova-plugin-fileを使うときに作ったソースをそのまま使おうとしたらエラーが2か所で発生したのでメモしておきます。
・FileErrorが定義されていないというエラーが発生(Uncaught ReferenceError: FileError is not defined)。FileErrorは廃止されたようです。(Cordovaプラグインは大丈夫?)
Note: This interface is obsolete per the latest specification. Use the new DOM4 DOMError interface instead.
上記の引用を翻訳アプリで翻訳しました。
以下のサイトでも説明されています。
Remove FileError interface (deprecated) – chromestatus.com
・window.requestFileSystem()の第一引数にLocalFileSystem.PERSISTENTを指定していたところLocalFileSystemが定義されていないというエラーが発生(Uncaught ReferenceError: LocalFileSystem is not defined)。window.LocalFileSystemはAndroidには実装されてるけどGoogle Chromeには実装されていないってことでしょうか?とりあえず今回は第一引数をwindow.TEMPORARYに変更しました。
・ファイルの読み込み、書き込みについては何もいじらなくても動きました。
最後にFile System API/File APIのサンプルコードを載せます。コメントはあったほうがいい派なので残しました。なお動作の保証はできませんのでご了承ください。
Script
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; var fsDirEntry; // ファイルシステムのルートディレクトリ(DirectoryEntryオブジェクト) /*** * ファイルシステム初期化 */ function initFileSystem(callback) { window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, // cordovaの場合 // window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) { console.log('Opened file system: ' + fs.name); fsDirEntry = fs.root; if (callback) callback(); }, function(e) {errMsg(e, "requestFileSystem");} ); } /*** * ファイル読み出し * 指定したファイルのコンテンツをテキスト形式で読み出し、コールバックを呼び出す */ function readTextFile(path, callback) { fsDirEntry.getFile(path, {}, function(entry) { entry.file(function(file) { var reader = new FileReader(); reader.readAsText(file); reader.onload = function() { callback(reader.result); }; reader.onerror = function(e) {errMsg(e, "readAsText");}; }, function(e) {errMsg(e, "file");}); }, function(e) {errMsg(e, "getFile");}); } /*** * ファイル書き込み * 指定したコンテンツをファイルに書き込み、コールバックを呼び出す * ファイルが存在しない場合は、新しいファイルを作成する。 */ function writeTextFile(path, contents, callback) { fsDirEntry.getFile(path, {create: true, exclusive: false}, function(entry) { entry.createWriter( function(writer) { var blob = new Blob([contents]); writer.write(blob); writer.onerror= function(e) {errMsg(e, "write");}; if (blob.size < writer.length) { // 書き込むデータのサイズが既存ファイルより小さい場合、 writer.onwrite = function() { // 書き込み終了後にファイルサイズを書き込みサイズに合わせる writer.truncate(blob.size); if (callback) { writer.onwrite = callback; } }; } else { if (callback) { writer.onwrite = callback; } } }, function(e) {errMsg(e, "createWriter");} ); }, function(e) {errMsg(e, "getFile");}); } /*** * ファイル追記書き込み * 指定したコンテンツをファイルに追記し、コールバックを呼び出す * ファイルが存在しない場合は、新しいファイルを作成する。 */ function appendToFile(path, contents, callback) { fsDirEntry.getFile(path, {create: true, exclusive: false}, function(entry) { entry.createWriter( function(writer) { writer.seek(writer.length); // ファイルの末尾に移動 var blob = new Blob([contents]); writer.write(blob); writer.onerror= function(e) {errMsg(e, "write");}; if (callback) { writer.onwrite = callback; } }, function(e) {errMsg(e, "createWriter");} ); }, function(e) {errMsg(e, "getFile");}); } /*** * ファイル削除 * 名前で指定したファイルを削除し、コールバックを呼び出す */ function deleteFile(name, callback) { fsDirEntry.getFile(name, {}, function(entry){ entry.remove(callback, function(e) {errMsg(e, "remove");}); }, function(e) {errMsg(e, "getFile");}); } /*** * ディレクトリ作成 */ function makeDirectory(name, callback) { fsDirEntry.getDirectory(name, {create: true, exclusive: true}, callback, function(e) {errMsg(e, "getDirectory");}); } /*** * ディレクトリのコンテンツ読み出し */ function listFiles(path, callback){ if (!path) getFiles(fsDirEntry); else fsDirEntry.getDirectory(path, {}, getFiles, function(e) {errMsg(e, "getDirectory");}); function getFiles(dir) { var reader = dir.createReader(); var list = []; reader.readEntries(handleEntries, function(e) {errMsg(e, "readEntries");}); function handleEntries(entries) { if (entries.length == 0) callback(list); // 完了 else { for(var i=0; i<entries.length; i++) { var name = entries[i].name; if (entries[i].isDirectory) name += "/"; list.push(name); } reader.readEntries(handleEntries, function(e) {errMsg(e, "readEntries");}); } } } } /*** * ディレクトリ削除 */ function deleteDirectory(name, callback) { fsDirEntry.getDirectory(name, {}, function(entry) { entry.removeRecursively(callback, function(e) {errMsg(e, "removeDirectory");}); // ディレクトリと、その中身をすべて削除する }, function(e) {errMsg(e, "getDirectory");}); // ファイルは消せない。エラー用コールバックが呼ばれる } /*** * ファイル一括削除 */ function deleteFiles(name, callback) { listFiles(name, function(list) { for(var i=0; i<list.length; i++) { deleteFile(list[i], callback); // ディレクトリは消せない。エラー用コールバックが呼ばれる } }); } /*** * ファイル情報取得 */ function getFileInfo(path, onSuccess, onFail) { fsDirEntry.getFile(path, {}, function(entry) { entry.file(onSuccess, function(e) { errMsg(e, "file"); if (onFail) onFail(); }); }, function(e) { errMsg(e, "getFile"); if (onFail) onFail(); }); } /*** * エラー処理 */ /* function errMsg(e, funcName) { var info = errInfo(e); var msg = "エラーが発生しました!\n"; msg += "[FUNC] " + funcName + "\n"; msg += "[CODE] " + info.type + "\n"; msg += "[DESC] " + info.desc; // alert(msg); console.log(msg); } */ function errMsg(e, funcName) { var msg = "*** ERROR ***\n"; msg += "[FUNC] " + funcName + "\n"; msg += "[CODE] " + e.name + "(" + e.code + ")" + "\n"; msg += "[DESC] " + e.message; // alert(msg); console.log(msg); } /*** * FileErrorオブジェクトのエラーコードを文字列化する */ function errInfo(e){ var inf = {}; switch(e.code) { case FileError.ENCODING_ERR: inf.type = "ENCODING_ERR"; inf.desc = "The URL is malformed. Make sure that the URL is complete and valid."; break; case FileError.INVALID_MODIFICATION_ERR: inf.type = "INVALID_MODIFICATION_ERR"; inf.desc = "The modification requested is not allowed. For example, the app might be trying to move a directory" + " into its own child or moving a file into its parent directory without changing its name."; break; case FileError.INVALID_STATE_ERR: inf.type = "INVALID_STATE_ERR"; inf.desc = "The operation cannot be performed on the current state of the interface object. For example," + " the state that was cached in an interface object has changed since it was last read from disk."; break; case FileError.NO_MODIFICATION_ALLOWED_ERR: inf.type = "NO_MODIFICATION_ALLOWED_ERR"; inf.desc = "The state of the underlying file system prevents any writing to a file or a directory."; break; case FileError.NOT_FOUND_ERR: inf.type = "NOT_FOUND_ERR"; inf.desc = "A required file or directory could not be found at the time an operation was processed." + " For example, a file did not exist but was being opened."; break; case FileError.NOT_READABLE_ERR: inf.type = "NOT_READABLE_ERR"; inf.desc = "The file or directory cannot be read, typically due to permission problems that occur after a reference to" + " a file has been acquired (for example, the file or directory is concurrently locked by another application)."; break; case FileError.PATH_EXISTS_ERR: inf.type = "PATH_EXISTS_ERR"; inf.desc = "The file or directory with the same path already exists."; break; case FileError.QUOTA_EXCEEDED_ERR: inf.type = "QUOTA_EXCEEDED_ERR"; inf.desc = "Either there's not enough remaining storage space or the storage quota was reached" + " and the user declined to give more space to the database. To ask for more storage," + " see Managing HTML5 Offline Storage."; break; case FileError.SECURITY_ERR: inf.type = "SECURITY_ERR"; inf.desc = "Access to the files were denied for one of the following reasons:\n" + " ・The files might be unsafe for access within a Web application.\n" + " ・Too many calls are being made on file resources.\n" + " ・Other unspecified security error code or situations."; break; case FileError.TYPE_MISMATCH_ERR: inf.type = "TYPE_MISMATCH_ERR"; inf.desc = "The app looked up an entry, but the entry found is of the wrong type." + " For example, the app is asking for a directory, when the entry is really a file."; break; case FileError.ABORT_ERR: inf.type = "ABORT_ERR"; inf.desc = "FileAPI about error."; break; case FileError.SYNTAX_ERR: inf.type = "SYNTAX_ERR"; inf.desc = "FileAPI syntax error."; break; default: inf.type = "UNKNOWN_ERROR"; inf.desc = "FileAPI unknown error."; break; } return inf; }
ここからダウンロードできます。
file.js