ReactとCordovaで、Web/モバイルのハイブリットアプリを作った話
ReactとCordovaを使って、ブラウザ向けのWebUI + Androidで動くスマホアプリ を提供するサービスを、一人で作ってみた話です。
サマリー
- 作ったもの
- 最大の課題:作業量
- 一人で作りきるために意識したこと
- 取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する
- 取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する
- 取り組み3: ユニットテストを書く
- まとめ
作ったもの
自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんにFX自動取引を開始できる、システムトレードフレームワークです。
- アルゴリズムの作成、バックテスト、リアル口座での自動取引まで、これ一つで可能。
- 取引アルゴリズムはRubyで記述。メール送信や取引タイミングのPush通知も、APIを呼び出すだけで実現できます。
- 取引の状況は、スマホアプリ/Web UIでいつでもどこでも確認が可能。アルゴリズムの管理もできるので、相場が急に動いても安心です。
- クラウドプラットフォームのHerokuに対応。運用も低コスト。
- オープンソース。ソースコードはGitHubで公開しています。
スクリーンショットをいくつか。
Web UI:
スマホアプリ:
コード規模
UI側のコード規模は以下の通り。テストケースを含めた合計で、31000行くらいです。
WebUI | アプリ | 合計 | |
---|---|---|---|
ソースコード | 15222 | 4454 | 19676 |
テストケース | 9676 | 1574 | 11250 |
また、これとは別に、サーバーのコード(ruby)が、ソースコード/テストケースあわせて、27000行くらいあります。
構成
- UIは、いわゆるシングルページアプリケーションで、ECMAScript2015 で書いています。
- Babelでコンパイルして、Webpackで統合する形。
- マテリアルデザインを採用していて、Reactで動くUIライブラリの Material UI を利用しています。
前置きは以上。
最大の課題:作業量
開発にあたっての最大の課題は、なんといっても作業量でした。
初期の検討段階で、「Webアプリ or モバイルアプリのどちらかに注力できないか?」と考えましたが、
Push通知やモバイルでのシステム管理に対応した、スマホ時代のFXシステムトレードフレームワークにしたかった。
- 同様の機能を提供するソフトはすでにあるのですが、モバイルでの取引状況の確認やアルゴリズムの管理に難があったのが、開発のきっかけにもなっています。
ということで、両方必要という結論に至りました。そうと決まれば、後は、如何にして作りきるかです。
一人で作りきるために意識したこと
以下の2点を、特に意識しました。
- コードの共通化/再利用
- Web UI/スマホアプリのコードを可能な限り共通化して、開発コストを削減する。
- ユニットテスト
- ユニットテストで個々のモジュールの動作を保障。
- 機能追加や変更を、低コストで素早く行えるようにする。
そして、↑のための具体的な取り組みとして、以下を行いました。
取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する
スマホアプリをCordovaを使ったハイブリットアプリにして、Web UI/スマホアプリのコードを共通化しました。
- Cordovaを使うことで、JavaScript + HTML + CSSでスマホアプリを作ることができるので、同様の構成で作成するWeb UIのコードを再利用できるようになります。
- React Native という選択肢もありましたが、UI側の開発に着手した時点ではAndroidには対応していなかったため、見送りました。
- Cordovaは、Webでの情報が豊富で、実績も多くあった点がプラスでした。
- また、Push通知や課金決済関連のプラグインがあったことも大きいです。
- 採用に当たって懸念だった、UIのパフォーマンスについては、かんたんなプロトタイプを作って検証しました。
- やはり、ネイティブアプリよりはもっさりしていますが、ゲームのようなパフォーマンスがアプリの価値に直結する性質のソフトウェアではないので、問題ないレベルと判断。
- それよりも、リリースまでの開発工数を抑えること、また、リリース後の機能強化や改善をスピード感を持って行えること、を重視しました。 unageanu.hatenablog.com
「UIの反応速度とか使い勝手とか、実際どんなもんなの?」と気になる方は、ダウンロードしてお試し頂ければと思います。(まいど、ありがとうございます! / 30日の無料トライアル期間内に、定期利用を解除すれば請求は発生しないので大丈夫です)
なお、Crosswalkの効果は絶大でした。体感速度が劇的に向上したうえ、怪しい動きをしていた箇所も治ったり。 apkのサイズ/起動時間は増えますが、採用する価値はあります。
取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する
MVVMのレイヤードアーキテクチャを採用して、Modelレイヤのコードを共通化。
ViewやViewModelも、変更の度合いに応じて最適なレイヤーでカスタマイズすることで、共有できるコードを最大化しました。
- Model/View/ViewModel + 通信などのInfrastructureで構成。
- Model(UIに依存しない、アプリ共通のコアドメイン)は、Web UI/スマホアプリでそのまま共有。
- スタイルの変更だけで済む場合は、CSSのレベルでカスタマイズ。
- DOM構造を変える必要がある場合は、View(Reactコンポーネント)のコードを差し替えて対応。
- Viewで管理するデータや機能がそもそも異なる場合は、ViewModel(Modelをラップして、Viewに依存するデータや操作を提供するクラス群)のレベルで、カスタマイズして再利用。
このほか、Infrastructureの通信部分やGoogle Analyticsでの利用状況解析も、WebUI/スマホアプリでカスタマイズして利用しています。
- 通信部分は接続先REST APIを差し替える必要があるため(Web UIは自ホストに、スマホアプリはUIで設定したサーバーに接続)、URLResolverとして機能を切り出しカスタマイズしています。
- 利用状況解析は、スマホアプリではJava APIを使うようカスタマイズしたクラスを用意して差し替え。
以下は、利用状況解析のコードの抜粋です。 インターフェイスを統一しつつ、実装を差し替える形で、別のクラスを用意しました。
WebUI版:
export default class GoogleAnalytics { // 略 // Google Analytics のJavaScript版APIを呼び出す sendEvent( action, label="", value={} ) { this.run((ga) => ga('send', 'event', this.category, action, label, value)); } sendTiming( category, timingVar, time, label ) { this.run((ga) => { ga('timing', category, timingVar, time, label); }); } // 略 }
スマホアプリ版:
export default class GoogleAnalytics { // 略 // Cordova google-analytics-plugin のAPIを呼び出す // https://github.com/danwilson/google-analytics-plugin sendEvent( action, label="", value={} ) { this.run((ga) => { ga.trackEvent(this.category, action, label, value, () => {}, (e) => console.log(e)); }); } sendTiming( category, timingVar, time, label ) { this.run((ga) => { ga.trackTiming(category, time, timingVar, label, () => {}, (e) => console.log(e.message)); }); } // 略 }
これら、コンポーネントの差し替えは、DI(Dependency Injection)コンテナを利用して、行うようにしました。 DIコンテナを使うことで、コンポーネントの外側で、環境ごとにどのクラスを使うのか宣言的に指定できるようになります。
WebUI版のコンポーネント定義:
binder.bind("googleAnalytics").to("utils.GoogleAnalytics") .withProperties({ category: "web-ui", version: "1.0" }).onInitialize("initialize");
// スマホアプリ用のクラスを使うように変更 binder.bind("googleAnalytics").to("app.utils.GoogleAnalytics") .withProperties({ category: "app", // category, versionもカスタマイズ。 version: "1.0.6" }).onInitialize("initialize");
利用側のコードは以下のような感じ。Inject
としてマークしておくと、コンポーネント定義で宣言されたコンポーネントが注入されるので、利用側はAPIを呼び出すだけです。
export default class RmtService extends AbstractService { constructor() { this.urlResolver = ContainerJS.Inject; this.xhrManager = ContainerJS.Inject; this.googleAnalytics = ContainerJS.Inject; // コンポーネント定義で宣言されたコンポーネントが注入される } // 略 putAgentSetting(settings) { this.googleAnalytics.sendEvent( "put rmt agent setting" ); // GAにイベントを送信 return this.xhrManager.xhr(this.serviceUrl("agents"), "PUT", settings); } }
取り組み3: ユニットテストを書く
MVVMアーキテクチャを採用したことで、View以外の部分はDOMに依存しなくなるため、容易にユニットテストができるようになります。 ユニットテストを意識的に用意したことで、一人でも何とか作りきることができたかな、と感じます。
- 一人で開発したとこもあり、サーバー側の開発で1か月くらいrubyのコードばかり書いていると、自分が書いたコードでも忘れてしまって、変なバグを埋め込んだりします。テストコードがあることでデグレードが検知され、バグに気付くことが多くありました。
- また、コードがどう動くか、についても、テストケースをみると思い出せるということもありました。
ユニットテストで確認された"動く"コードを少しずつ積み上げていく、という作り方は、心理的にも良かったと思います。 一人なので、開発期間はどうしても長くなりますし、やってもやってもタスクがなくならない、終わりが見えない感じになりますが、"動く"コードが積み上がっていくことで、毎日少しずつでもコードを書いていれば、いつか完成すると思えるようになります。
なお、初版リリースの段階ではViewのテストは省略しました。
- 初版リリースでは、UIの精緻化などでDOMの構造を変更する可能性が高く、テストを作成しても、効果が変更コストに見合わないと考えました。
- また、Viewの機能は「ViewModelの状態に応じて仮想DOMを生成する」だけで、テストすべきロジックが少ない && ViewModelのテストで、アプリケーションがこういう状態の時はこうなる、〇〇操作を実行するとこういう状態になる、といった機能は確認できている、というのも理由としてあります。
- 同じ理由で、E2Eテストも行っていません。このあたりは、今後、サービスが利用されるようになって、メンテナンスの比重が大きくなってからの課題と考えています。
まとめ
- Cordovaを使ってちゃんと作れば、一人でもサービスを作れる。
- プロトタイプや最初のMVPは、Cordovaで素早くコストをかけずに作る、そして、手ごたえがあったらネイティブで作り直す、という選択肢は割とアリなのではと思います。
- もちろん、パフォーマンスが重要なアプリの場合は、不相応な場合もありますが。
- ユニットテスト重要。テスタビリティは最初から考慮して作ろう。
- Crosswalk速い。使おう。
FXシステムトレードフレームワーク「Jiji2」の開発状況 その2
追記(2015-12-01):
FXシステムトレードフレームワーク「Jiji」、リリースしました!
使ってみて、ご意見など頂けるとうれしいです。
前回の更新からだいぶ時間が空いてしまいました・・・。少しずつ実装は進んでいます。
9月10月でスマホアプリの実装とUIデザインがだいたいできたので、スクリーンショットを公開してみます。 例によって、コメントなど頂ければ嬉しいです。
ホーム
- 最初に表示される画面です。
- 以下のような情報を、1画面でさっと確認できます。
- 口座残高/直近の勝率などのサマリ
- 現在のレートと値動きをチャートで
- トレードシステムからの新着の通知
- 最新の建玉
取引状況
- 合計損益や勝率などシステムトレードの取引状況を確認する画面です。
- 左上のメニューから集計期間を変えて分析することができます。
チャート
- 詳細チャートです。最大10年前までさかのぼってレートを閲覧できます。
- ローソク足のほか、エージェントで描画した移動平均線などのグラフを表示できます。
- 真ん中あたりにある緑と赤のバーが建玉の保有期間を示していて、チャートの値動きにあわせて期待通りトレードができているかチェックできるようになっています。
取引ロジック(エージェント)エディタ
通知一覧
- 取引ロジック(エージェントと呼んでいます)から送られてきた通知の一覧画面。
- クリックで詳細が閲覧可能です。
ポジション一覧
- 取引ロジックが行った取引の一覧です。
- こちらもクリックで詳細が表示されます。
バックテストの作成
- 任意の期間とエージェントを指定してバックテストを実行できます。
バックテスト詳細
- バックテストの結果を確認できます。
ログ
- エージェントが出力したログを確認できます。
設定
- 使用する証券会社やアクセスパスワードを設定できます。
残タスク
残っているタスクは以下です。
- ロゴの作成とデザインの最終調整
- デバッグと使い込みテスト
- 使い方などのドキュメント準備
- 導入手順の整備
1か月でこれらを消化して、11月末にはなんとかリリースしたいところ。もうしばらくお待ちください。
Jiji2 - ホーム画面のスクリーンショット
追記(2015-12-01):
FXシステムトレードフレームワーク「Jiji」、リリースしました!
使ってみて、ご意見など頂けるとうれしいです。
ホーム画面のデザインができてきたので、現状のスクリーンショットを公開します。
ご意見などいただければ嬉しいです。
PC版:
スマホ版:
ホームはアプリ利用時のエントリーポイントになる画面です。
以下のような、システムトレードの主要な情報を1画面でさっと確認できるようにしました。
- 口座残高/直近の勝率などのサマリ
- 現在のレートと値動きをチャートで
- トレードシステムからの新着の通知
- 最新の建玉
まずはこの画面で状況を把握した後、エージェント設定の変更などのアクションを実行していくイメージです。
ソースコード
コードはGitHubで公開しているので、気になる方はこちらをご覧ください。
Jiji2の開発状況
追記(2015-12-01):
FXシステムトレードフレームワーク「Jiji」、リリースしました!
使ってみて、ご意見など頂けるとうれしいです。
しばらく更新できていなかったので、現在の開発状況を簡単にまとめておきます。
徐々にですが、動くようになってきています。
できていること
- エージェントの作成/バックテスト/リアル口座でのトレードなど、コアとなる機能は一通り動く。
- UIも主要な機能は一通り実装完了。
- UIから、エージェントを作成→バックテスト→リアル口座で動かす、というところまでできます。
- ただし、「とりあえず機能を配置しました」レベルです。デザイン等はまだまだです。
- また、バックテストの削除など細かなところの作りこみもできていません。
残
- スマホアプリ
- Cordoaのハイブリットアプリにして、UIコンポーネントはなるべくWebUI側と共有する方針ですが、それでもデザインの調整作業等は必要とみています。
- あとはPush通知など、アプリだけの機能の部分ですね。
- UIデザイン
- 細かな機能の作りこみ
- 使ってみて最低限必要と思われる機能はリリースまでに入れておきたい。
- デバッグ
- ドキュメント整備
- 導入手順の整備
- Herokuへの1クリックデプロイ
- Dockerコンテナの用意
初版では見送った機能
以下の機能は、とりあえず初版では見送ることにしました。
- UIからの成行注文機能
- OANDA のクライアントアプリを使えばできるので。
- また、Jijiではレートの更新が15秒ごとになってしまうので、作っても使いにくい機能になるかなと。
思ったよりもだいぶ時間がかかっていますが、あと1~2か月くらいあればリリースできるかなー、というところです。
コードはGitHubにあるので、気になる方はこちらをご覧ください。
導入手順とかまだ何も整備できていないので、動かすのは難しいですが。
マニュアルに書かれていないOANDA fx Trade APIの仕様3つ
OANDA fx Trade APIで、マニュアル に詳細が書かれていない仕様がいくつかあったので、動かしてみて確認した内容をまとめておきます。
サマリ
- REST APIでは、stop と marketIfTouched で注文が可能
- 注文のレスポンスに含まれる、tradeOpened、tradesClosed、tradeReduced, orderOpened の違い
- tradeOpened、tradesClosed、tradeReduced のidは建玉(trade)のID
REST APIでは、stop と marketIfTouched で注文が可能
OANDA 取引ツールでは成行(market)、指値(limit) での注文ができますが、REST APIではこれらに加えて 逆指値(stop)、marketIfTouchedでの注文が可能です。
- 成行/指値/逆指値は、他の証券会社でも提供している一般的な注文方法なので、説明はこのあたりを参照 していただければわかるかと。
marketIfTouchedは、「価格が指定価格になったら成行きで発注する」注文方法です。
トレールストップなど、建玉の決済条件は注文方法とは別に指定できるようになっています。
- 指定できる条件は以下の3つです。
- 利益確定価格(takeProfit)
- 損切価格(stopLoss)、
- トレールストップのpip数(trailingStop))
- これらの決済条件は、注文方法(成行、指値..etc..)を問わず指定可能です。
- 指定できる条件は以下の3つです。
なお、OCOのような両建ての注文方法はありません。
注文のレスポンスに含まれる、tradeOpened、tradesClosed、tradeReduced, orderOpened の違い
注文(order)の結果に応じて、これらのレスポンスの内容が変わります。
- 注文が約定しなかった場合、新規注文が作成され、 orderOpened で注文の情報が返されます。
- 注文が約定し、新しい建玉ができた場合、 tradeOpened で作成された建玉の情報が返されます。
- 注文が約定し、既存の建玉が決済された場合、 全決済された建玉が tradesClosed、部分決済された建玉が tradeReduced で返されます。
tradeOpened、tradesClosed、tradeReduced のidは建玉(trade)のID
細かい話ですが、注文のレスポンス tradeOpened、tradesClosed、tradeReduced のidは建玉(trade)のIDになっています。 ちょっと紛らわしいので、念のため。
追記(2015-12-01)
OANDA FX trade APIを利用した、無料のFXシステムトレードフレームワーク「Jiji」をリリースしました!
使ってみて、ご意見など頂けるとうれしいです。
Webアプリ向け高機能コードエディタ「Ace」をReactに組み込んで使ってみる
Ace とは?
Ace は、Webアプリケーション向けのリッチなコードエディタです。 シンタックスハイライトや正規表現検索に対応した高機能なコードエディタを、Webアプリにさくっと組み込めます。
機能一覧: (Aceのサイトより)
- Java,Ruby等を含む、110言語のシンタックスハイライトに対応
- 自動インデント
- マルチカーソル
- 正規表現での検索/置換
- 関数やクラスの折り畳み機能(Code folding)
- 「Ctrl + /」での一括コメント
- ..etc..
おお、なんかすごい高機能だ。
Reactに組み込んで使う
React-Ace を使います。
$ npm install react-ace
jsのソースは以下。
import React from 'react' import AceEditor from 'react-ace' // webpackで統合するので、必要なテーマや拡張モジュールは // 明示的にimportしておく必要があります。 import 'brace/mode/ruby' import 'brace/theme/github' import 'brace/ext/searchbox' class RubyEditor extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return ( <AceEditor mode="ruby" theme="github" value={this.props.source} onChange={this.onChange.bind(this)} name="editor" /> ); } onChange(newValue) { console.log(newValue); // とりあえず } } RubyEditor.propTypes = { source: React.PropTypes.string.isRequired }; RubyEditor.defaultProps = { source: "# test. \nclass Foo\nend" }; React.render( <RubyEditor />, document.body );
unageanu/sandbox/ace - GitHub にPushしているので、package.jsonなど他のソースはこちらを参照ください。