在no_ui中使用多程式實現多賬戶並行執行,並分配各自獨立的工作環境和策略

張國平發表於2021-12-10


看到群裡有人問vnpy下多賬戶同時執行,並且給每個賬戶分配不同工作內容,就是執行不同策略。


考慮了一下,實現方式很多,在vnTrader 例項程式碼中 no_ui 啟動vnTrader就是用使用多程式模組multiprocessing,在子程式中執行。因為python GIL全域性鎖的存在,每個子程式都有獨立的例項化環境,其實就是一個獨立執行vnTrader。那麼多開幾個子程式,分配給對應的賬戶引數,就可以實現 vnpy下多賬戶同時執行。


這裡我用json檔案儲存賬戶配置資訊,每個賬戶配置資訊,

其中包括

- 賬戶名,後面用賬戶名命名子程式,這樣可以在log檔案中區加入子程式資訊,區分是那個賬戶的日誌資訊,方便跟蹤。

- 介面名,預設是CTP,也可以支援其他介面。

- 工作路徑,就是放.vnTrader資料夾和策略的路徑;給每個賬戶分配不同配置資訊和策略引數等。這裡要注意,必須在路徑下建立 .vnTrader資料夾,否則 vnpy的 utility 中_get_trader_dir方法就會使用home路徑作為工作路徑。

- 登入資訊,就是登入密碼一類東西。


具體程式碼這裡有幾個涉及技術點,先說說,具體的在程式碼看註釋就可以了。

第一個,同時執行account 不應該超過cpu核心數,因為vntrader 執行可以看成是一個持續死迴圈,在main_engine.close之前不會退出cpu佔用; 如果超過核心數,等待的執行事務將一直等待,所以不可以超過。


第二個,其實一開始我是用multiprocessing.Pool,再用pool.map_async(run_child, account_detail_list) 來實現批次建立程式,但是這樣沒法給每個程式命名,而且 map_async不帶阻塞,沒法跟蹤每個子程式返回。最後還是使用

multiprocessing.Process(target=run_child, name=account_detail["account_name"], args=(account_detail,))。

最後這裡不用pool.map,或者jion();這樣阻塞主程式的操作,剛剛說的因為vnTrader子執行緒是一個持續死迴圈,一旦阻塞,下次主執行緒只用等到完全停止完畢才能開始。


第三個,工作環境切換,這裡使用os.chdir更改工作路徑到配置檔案中的指定工作路徑;但是因為在引用vnpy 模組時候,就已經有全域性資料使用靜態方法去獲取工作路徑了,所以程式碼中把vnpy模組的import放在 程式方法中,更改工作路徑之後。


第四個,在啟動程式方法中,我又定義了兩個巢狀方法,用於給threading.Timer子執行緒來定時啟動監控策略初始化是否完成,和交易時間是否結束。原來程式碼都是用time.sleep 結合 while 來停止當前執行緒的,但是之前介面化執行vnTrader時候,sleep會停止pyQt前端介面,阻塞主程式碼執行,當然策略初始化和事件傳輸引擎都是獨立執行緒執行,不會影響。 所以為了為了保證交易安全,改成執行緒執行。

這裡簡單介紹下python執行緒,雖然由於GIL鎖,一個python環境只能用到一個核,但是在IO密集情況,比如網路爬蟲或者請求等待時候,使用多執行緒可以提高效率;直接sleep主執行緒,可以把資源給其他子執行緒。

執行效果如下:

兩個賬戶TEST_1_ACCOUNT ; TEST_2_ACCOUNT;各自獨立工作路徑;程式名也輸出介面,而log檔案也按照賬戶分開輸出。


引數檔案如下,名稱Mutiple_Accounts_Config.json 放在no_ui資料夾下就可以

[
    {
    "account_name": "TEST_2_ACCOUNT",
    "gateway": "CTP",
    "workspace": "C:/TEST_ACCOUNT_2_WORKSPACE/",
    "logon_detail": {
      "使用者名稱": "",
      "密碼": "",
      "經紀商程式碼": "",
      "交易伺服器": "",
      "行情伺服器": "",
      "產品名稱": "",
      "授權編碼": ""
    }
  },
  {
    "account_name": "TEST_1_ACCOUNT",
    "gateway": "CTP",
    "workspace": "C:/TEST_ACCOUNT_1_WORKSPACE/",
    "logon_detail": {
      "使用者名稱": "",
      "密碼": "",
      "經紀商程式碼": "",
      "交易伺服器": "",
      "行情伺服器": "",
      "產品名稱": "",
      "授權編碼": ""
    }
  }
]


run程式碼如下,因為改動較多,建議直接覆蓋原來的run.py:

import multiprocessing
import json
from threading import Timer
import sys
import os
from time import sleep
from datetime import datetime, time
from logging import INFO
# Chinese futures market trading period (day/night)
DAY_START = time(8, 45)
DAY_END = time(15, 0)
NIGHT_START = time(20, 45)
NIGHT_END = time(1, 30)
def check_trading_period():
    """"""
    current_time = datetime.now().time()
    trading = False
    if (
        (current_time >= DAY_START and current_time <= DAY_END)
        or (current_time >= NIGHT_START)
        or (current_time <= NIGHT_END)
    ):
        trading = True
    return trading
def run_child(account_detail):
    """
    Running in the child process.
    """
    account_name = account_detail["account_name"]
    ctp_setting = account_detail["logon_detail"]
    # 更改工作路徑
    os.chdir(account_detail["workspace"])
    # 把對vntrader 包的引用放在工作路徑更改後,不然工作路徑更改無法生效,
    from vnpy.event import EventEngine
    from vnpy.trader.setting import SETTINGS
    from vnpy.trader.engine import MainEngine
    from vnpy.gateway.ctp import CtpGateway
    from vnpy.app.cta_strategy import CtaStrategyApp
    from vnpy.app.cta_strategy.base import EVENT_CTA_LOG
    SETTINGS["log.active"] = True
    SETTINGS["log.level"] = INFO
    SETTINGS["log.console"] = True
    from vnpy.trader.utility import TRADER_DIR, TEMP_DIR
    # 結束引用
    SETTINGS["log.file"] = True
    event_engine = EventEngine()
    main_engine = MainEngine(event_engine)
    main_engine.add_gateway(CtpGateway)
    cta_engine = main_engine.add_app(CtaStrategyApp)
    main_engine.write_log(f" 主引擎建立成功")
    main_engine.write_log(f"工作路徑: {TRADER_DIR, TEMP_DIR}")
    log_engine = main_engine.get_engine("log")
    # 更新log 格式。
    event_engine.register(EVENT_CTA_LOG, log_engine.process_log_event)
    main_engine.write_log(f"註冊日誌事件監聽")
    main_engine.connect(ctp_setting, account_detail["gateway"])
    main_engine.write_log(f"連線CTP介面")
    sleep(10)
    cta_engine.init_engine()
    main_engine.write_log(f"CTA策略初始化完成")
    def recheck_thread():
        #每個10秒檢查所有策略是否初始化完成,使用Timer執行緒每10秒檢查,
        all_strategise_inited = False
        for strategy in cta_engine.strategies.values():
            if strategy.inited == False:
                all_strategise_inited = False
                break
            all_strategise_inited = True
        if all_strategise_inited:
            main_engine.write_log(f"CTA策略全部初始化=====")
            cta_engine.start_all_strategies()
            main_engine.write_log(f"CTA策略全部啟動=====")
        else:
            newTask = Timer(10, recheck_thread)
            newTask.start()
    cta_engine.init_all_strategies()
    newTask = Timer(5, recheck_thread)
    newTask.start()
    def recheck_trading_period():
        # 每隔10秒檢查是否交易時段,否則退出,使用Timer執行緒每10秒檢查,
        trading = check_trading_period()
        if not trading:
            main_engine.write_log(f"關閉子程式")
            main_engine.close()
            sys.exit(0)
        else:
            closeTask = Timer(10, recheck_trading_period)
            closeTask.start()
    closeTask = Timer(5, recheck_trading_period)
    closeTask.start()
def run_parent():
    """
    Running in the parent process.
    """
    print("啟動CTA策略守護父程式")
    with open('Mutiple_Accounts_Config.json', mode="r", encoding="UTF-8") as f:
        account_detail_list = json.load(f)
    child_process_list = []
    while True:
        trading = check_trading_period()
        # Start child process in trading period
        if trading and child_process_list == []:
            print("啟動子程式")
            # 同時執行account 不應該超過cpu核心數,因為vntrader 可以看成是一個持續迴圈,在main_engine.close之前不會退出cpu佔用;
            # 等待的執行事務將一直等待
            for account_detail in account_detail_list:
                new_process = multiprocessing.Process(target=run_child, name=account_detail["account_name"], args=(account_detail,))
                new_process.start()
                child_process_list.append(new_process)
            # # 使用程式池更加方便,但是無法給程式命名去檢視log 是那個account的,所以不建議
            # pool = multiprocessing.Pool(multiprocessing.cpu_count())
            # child_process_list = pool.map_async(run_child, account_detail_list)
            print("子程式啟動成功")
        # 非記錄時間則退出子程式
        if not trading and child_process_list:
            for process in child_process_list:
                if not process.is_alive():
                    child_process_list.remove(process)
            if child_process_list == []:
                print("子程式關閉成功")
        sleep(10)
if __name__ == "__main__":
    run_parent()


還有一個小修改,為了輸出各自子程式名,在LogEngine中,trader/engine.py 中,進入這個processName:

self.formatter = logging.Formatter(
    "%(asctime)s  %(levelname)s: %(processName)s %(message)s"
)



不過如果賬戶太多,還是有點亂,那時候可以用其他工具來執行;另外策略程式碼也可以按照工作路徑區分,這裡沒有展示。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/22259926/viewspace-2847093/,如需轉載,請註明出處,否則將追究法律責任。

相關文章