【シストレ開発#13】リファクタリングで保守性、機能拡張性を向上!

 ※ 本記事は広告・プロモーションを含みます。
シストレ開発

今後の開発効率を上げるためリファクタリングを行うことにしました! たっきん(Twitter)です!

自作システムトレードをここ2,3ヵ月運用してみた結果、軽微な不具合や改善点がいくつか見つかったため、このへんでシステム全体的にリファクタリングを行いたいと思います。

「リファクタリング」って何?って思ってる人も多いかもしれませんが、システム開発に携わっていると割とよく聞く用語で、ざっくり説明するならシステムの外見上の動作を変えずにプログラムの内部構造を整理整頓することを言います。

プログラムする前にちゃんと基本設計していたとしても、その後の仕様変更等で案外設計構造は崩れてきてしまうものです。

設計構造が崩れている状態で長期に渡り設計やソースコードを保守、メンテしていくとなると不具合を埋め込んでしまう可能性も上がりますし、必要以上に時間を浪費し続けてしまうことにもなりかねません。

なので定期的にリファクタリングをすることで設計やソースコードの品質を向上できますし、保守、メンテの時間の削減にも繋がるメリットがあります。

僕の個人的な考え方になりますが、長期にわたってソースコードを保守、メンテしていくならこの「リファクタリング」は積極的に行っていくべきだと思っているのですが、実際のところ開発現場ではリファクタリングに反対派の人も結構いる気がします。

(おそらく不必要にソースコードに変更を入れられるのを嫌っているのだと思います。)

まぁリファクタリングをすることによって逆に不具合を埋め込んでしまう可能性もあるため、人によって考え方が別れやすいというのもあるかもしれませんね。

話が大きく逸れてしまいましたが、今回リファクタリングを行うにあたり自分の中で方針を決めています。

それは、「Pythonの状態遷移パッケージ:transitions」で実装し直すことです。

少し前にこのパッケージの存在を知り、軽く使ってみたのですが、かなり便利なパッケージで状態遷移設計を簡単にコードに落とし込むことができるだけでなく、簡易でシンプルなコードになるため可読性も上がります。

また、状態ごとに設計、実装を行うため不具合が発生したとしても不具合発生ヵ所を特定しやすく、また不具合の影響範囲も不具合が発生した状態内に限定されることが多いため、保守やメンテナンスの負荷も下げられます。

さらに、機能を拡張するときは「状態」「遷移」を追加するだけでよいため機能拡張性にも優れています。

シストレの一部のノードで状態遷移設計はしていたのですが、コードに落とし込むときに状態遷移を全て自作コードで書いていたため、かえって複雑で可読性の悪いコードになっていました。

状態遷移設計の実装がネックになっていたのですが、それを解決してくれるのがtransitionsパッケージになります。

時間があるならtransitionsパッケージを紹介する記事を書きたいくらいですが、今はシストレ開発を優先して進めたいため、時間ができたら実装例も含めてtransitionsパッケージ紹介ブログを書きたいと思っています。

trade_managerパッケージをリファクタリング

今回大きくリファクタリングを行ったのはtrade_managerパッケージ内の以下2つのノードです。

  • order_managerノード
  • candlestick_managerノード

まずこの2つノードについてはノード名の変更を行いました。

ROSノードの命名規則ではなく一般的なプログラミングの命名規則の話になるのですが、クラス名やメソッド名を付けるときは役割が明確になるような名前を付けるようにするのが好ましいとされています。

その際に「manager」というネーミングは汎用的すぎるため、あらゆる機能が詰め込まれてしまい、そのクラスやメソッドが肥大化してしまう可能性があることから「manager」という名前を付けるのはアンチパターンと考えている人も多い感じでした。

なので今回、各ノードの役割を明確にするために、以下のようにノード名をリネームしました。

  • order_managerノード ⇒ order_schedulerノード
  • candlestick_managerノード ⇒ historical_candlesノード

order_managerノードはアプリケーションから発行された注文リクエストを受け取り、指定された時刻を過ぎると指値注文をキャンセルしたり、注文を決済したりとタイムマネジメントを主に行わせていることから「order_scheduler」ノードに改名しました。

candlestick_managerノードはローソク足のヒストリカルデータをマネジメントすることが役割であることから「historical_candles」ノードに改名しました。

今回のリネームでより一層各ノードの役割が明確になりました!

また、今回のリファクタリングに伴いシストレ・アーキテクチャ図もアップデートしました。

<シストレ・アーキテクチャ ver.1.1.1>

order_schedulerノードの状態遷移設計と実装

order_schedulerノードの前身であるorder_managerノードは元々状態遷移で設計していました。

しかし、この設計では注文リクエストのタイミングのズレなど構造的な不具合がいくつか見つかっていたため、改めて状態遷移設計を1から見直すことにしました。

そして、改修後の状態遷移図は以下のようになりました。

各状態の説明は下記のとおりです。

状態名説明
新規注文中APIを通じてFXサーバーへ新規注文中の状態
新規約定待ち新規で発注した指値、逆指値注文が約定するのを待っている状態
新規約上状態確認中APIを通じてFXサーバーへ新規に発注した指値、逆指値注文が約定したか否かを確認している状態
新規注文キャンセル中APIを通じてFXサーバーへ新規に発注した指値、逆指値注文をキャンセルしている状態
決済約定待ち決済注文が約定するのを待っている状態
決済約上状態確認中APIを通じてFXサーバーへ決済注文が約定したか否かを確認している状態
決済注文中APIを通じてFXサーバーへ決済注文中の状態
トレード完了トレードを完了した状態

※:注文方法は全てIFO注文であることを前提とした設計となっています。

IFO注文とは、IFD注文とOCO注文を組み合わせた注文方法です。
わかりやすく説明するなら「指値、逆指値の新規注文」と、新規注文が約定した後の「利益確定の決済注文」「損失確定の決済注文」の3つの注文を同時に出せる注文方法になります。
エントリーからエグジットまで全て自動でやってくれる注文方法のためシステムトレードとの相性が良いです。

そして上の状態遷移図をpythonのコードに落とし込むのですが、pythonのtransitionsパッケージを使用することで状態遷移を簡単に実装することができます。

以下のコードはOrderTicketクラスにtransitionsパッケージを使用して状態遷移を実装した例になります。

class OrderTicket():

    class States(Enum):
        EntryOrdering = auto()
        EntryWaiting = auto()
        EntryChecking = auto()
        EntryCanceling = auto()
        ExitWaiting = auto()
        ExitChecking = auto()
        ExitOrdering = auto()
        Complete = auto()

    def __init__(self) -> None:

        # ---------- Create State Machine ----------
        states = [
            {
                Tr.NAME.value: self.States.EntryOrdering,
                Tr.ON_ENTER.value: None,
                Tr.ON_EXIT.value: None
            },
            {
                Tr.NAME.value: self.States.EntryWaiting,
                Tr.ON_ENTER.value: "_on_entry_EntryWaiting",
                Tr.ON_EXIT.value: None
            },
            {
                Tr.NAME.value: self.States.EntryChecking,
                Tr.ON_ENTER.value: "_on_enter_EntryChecking",
                Tr.ON_EXIT.value: "_on_exit_EntryChecking"
            },
            {
                Tr.NAME.value: self.States.EntryCanceling,
                Tr.ON_ENTER.value: "_on_enter_EntryCanceling",
                Tr.ON_EXIT.value: None
            },
            {
                Tr.NAME.value: self.States.ExitWaiting,
                Tr.ON_ENTER.value: "_on_entry_ExitWaiting",
                Tr.ON_EXIT.value: None
            },
            {
                Tr.NAME.value: self.States.ExitChecking,
                Tr.ON_ENTER.value: "_on_enter_ExitChecking",
                Tr.ON_EXIT.value: "_on_exit_ExitChecking"
            },
            {
                Tr.NAME.value: self.States.ExitOrdering,
                Tr.ON_ENTER.value: "_on_enter_ExitOrdering",
                Tr.ON_EXIT.value: None
            },
            {
                Tr.NAME.value: self.States.Complete,
                Tr.ON_ENTER.value: None,
                Tr.ON_EXIT.value: None
            },
        ]

        transitions = [
            {
                Tr.TRIGGER.value: "_trans_from_EntryOrdering_to_ExitWaiting",
                Tr.SOURCE.value: self.States.EntryOrdering,
                Tr.DEST.value: self.States.ExitWaiting,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_EntryOrdering_to_EntryWaiting",
                Tr.SOURCE.value: self.States.EntryOrdering,
                Tr.DEST.value: self.States.EntryWaiting,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_EntryWaiting_to_EntryChecking",
                Tr.SOURCE.value: self.States.EntryWaiting,
                Tr.DEST.value: self.States.EntryChecking,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: "_conditions_trans_lock"
            },
            {
                Tr.TRIGGER.value: "_trans_from_EntryChecking_to_EntryWaiting",
                Tr.SOURCE.value: self.States.EntryChecking,
                Tr.DEST.value: self.States.EntryWaiting,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },

            {
                Tr.TRIGGER.value: "_trans_from_EntryWaiting_to_EntryCanceling",
                Tr.SOURCE.value: self.States.EntryWaiting,
                Tr.DEST.value: self.States.EntryCanceling,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_EntryCanceling_to_Complete",
                Tr.SOURCE.value: self.States.EntryCanceling,
                Tr.DEST.value: self.States.Complete,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_EntryCanceling_to_EntryChecking",
                Tr.SOURCE.value: self.States.EntryCanceling,
                Tr.DEST.value: self.States.EntryChecking,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: "_conditions_trans_lock"
            },

            {
                Tr.TRIGGER.value: "_trans_from_EntryChecking_to_ExitWaiting",
                Tr.SOURCE.value: self.States.EntryChecking,
                Tr.DEST.value: self.States.ExitWaiting,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_ExitWaiting_to_ExitChecking",
                Tr.SOURCE.value: self.States.ExitWaiting,
                Tr.DEST.value: self.States.ExitChecking,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: "_conditions_trans_lock"
            },
            {
                Tr.TRIGGER.value: "_trans_from_ExitChecking_to_ExitWaiting",
                Tr.SOURCE.value: self.States.ExitChecking,
                Tr.DEST.value: self.States.ExitWaiting,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_ExitChecking_to_Complete",
                Tr.SOURCE.value: self.States.ExitChecking,
                Tr.DEST.value: self.States.Complete,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_ExitWaiting_to_ExitOrdering",
                Tr.SOURCE.value: self.States.ExitWaiting,
                Tr.DEST.value: self.States.ExitOrdering,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_ExitOrdering_to_Complete",
                Tr.SOURCE.value: self.States.ExitOrdering,
                Tr.DEST.value: self.States.Complete,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_to_Complete",
                Tr.SOURCE.value: "*",
                Tr.DEST.value: self.States.Complete,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
        ]

        self._sm = Machine(model=self,
                           states=states,
                           initial=self.States.EntryOrdering,
                           transitions=transitions)

historical_candlesノードの状態遷移設計と実装

historical_candlesノードの前身であるcandlestick_managerノードですが、今回の改修に伴い状態遷移で設計と実装をしました。

historical_candlesは比較的シンプルな状態遷移図となります。

各状態の説明は下記のとおりです。

状態名説明
DF(DataFrame)更新待ち各時間足のローソク足データが確定するまで待っている状態
DF(DataFrame)更新中確定したローソク足データをAPIを通じてFXサーバーから取得、更新している状態
DF(DataFrame)更新リトライ待ちローソク足データの更新に失敗し、リトライ待ちの状態

上の状態遷移図をtransitionsパッケージを使用してCandlesDataクラスとして実装した例は以下のようになります。

class CandlesData():

    class States(Enum):
        waiting = auto()
        updating = auto()
        retrying = auto()

    def __init__(self) -> None:

        # ---------- Create State Machine ----------
        states = [
            {
                Tr.NAME.value: self.States.waiting,
                Tr.ON_ENTER.value: "_on_entry_waiting",
                Tr.ON_EXIT.value: "_on_exit_waiting"
            },
            {
                Tr.NAME.value: self.States.updating,
                Tr.ON_ENTER.value: "_on_entry_updating",
                Tr.ON_EXIT.value: None
            },
            {
                Tr.NAME.value: self.States.retrying,
                Tr.ON_ENTER.value: "_on_enrty_retrying",
                Tr.ON_EXIT.value: "_on_exit_retrying"
            },
        ]

        transitions = [
            {
                Tr.TRIGGER.value: "_trans_from_wating_to_updating",
                Tr.SOURCE.value: self.States.waiting,
                Tr.DEST.value: self.States.updating,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_updating_to_retrying",
                Tr.SOURCE.value: self.States.updating,
                Tr.DEST.value: self.States.retrying,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_retrying_to_updating",
                Tr.SOURCE.value: self.States.retrying,
                Tr.DEST.value: self.States.updating,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_from_updating_to_waiting",
                Tr.SOURCE.value: self.States.updating,
                Tr.DEST.value: self.States.waiting,
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
            {
                Tr.TRIGGER.value: "_trans_self_updating",
                Tr.SOURCE.value: self.States.updating,
                Tr.DEST.value: "=",
                Tr.PREPARE.value: None,
                Tr.BEFORE.value: None,
                Tr.AFTER.value: None,
                Tr.CONDITIONS.value: None
            },
        ]

        self._sm = Machine(model=self,
                           states=states,
                           initial=self.States.waiting,
                           transitions=transitions)

さいごに

シストレ開発を始めてから約1年が経過し、軽微な不具合や改善点がいくつか出てくるようになったため今回このタイミングでリファクタリングを行ってみました。

大きく変更したのはtrade_managerパッケージ内の「order_managerノード」と「candlestick_managerノード」になり、これらのノードの役割も明確にするためにノード名の変更も行いました。

リファクタリングの方針としては「Pythonの状態遷移パッケージ:transitions」を使用して、簡易でシンプルなコードにし、可読性を向上させることです。

まずは状態遷移表を作成してからコードに落とし込みました。

今回作成したorder_schedulerノードとhistorical_candlesノードのソースコードはGitHubで公開しています。

今回のリファクタリングによって、trade_managerパッケージはより盤石となりそうです。

今回変更した個所のテストが一通り終わったらOANDAの本番口座で運用しているソースコードにマージする予定です。

スポンサーリンク

コメント

タイトルとURLをコピーしました