手把手教你給行情收集器升級回測自定義數據源功能

上一篇文章手把手教你實現一個行情收集器我們一起實現了一個收集行情的機器人程序,收集了行情數據接下來怎麼使用呢?當然是用於回測系統了,這裏依託於發明者量化交易平臺回測系統的自定義數據源功能,我們就可以直接把收集到的數據作爲回測系統的數據源,這樣我們就可以讓回測系統應用於任何我們想回測歷史數據的市場了。

因此,我們可以給「行情收集器」來一個升級!讓行情收集器同時可以作爲自定義數據源給回測系統提供數據。

有了需求,動手!

準備

和上次文章中的準備工作有所不同,上一次是在我的本機MAC電腦上運行的託管者程序,安裝mongodb數據庫啓動數據庫服務。這次我們把運行環境換到VPS上,使用阿里雲linux服務器,來運行我們這一套程序。

  • mongodb數據庫

    和上篇文章一樣,需要在行情收集器程序運行的設備上安裝mongodb數據庫,並且開啓服務。和在MAC電腦上安裝mongodb基本一樣,網上有不少教程,可以搜索看下,很簡單。

  • 安裝python3
    程序使用python3語言,注意用到了一些庫,沒有的話需要安裝。

    • pymongo
    • http
    • urllib
  • 託管者
    運行一個發明者量化交易平臺的託管者即可。

改造「行情收集器」

行情收集器即RecordsCollecter (教學)這個策略。
我們來對它做一些改造:
在程序進入收集數據的while循環之前,使用多線程庫,併發執行啓動一個服務,用來監聽發明者量化交易平臺回測系統的數據請求。
(其它的一些細節修改可以忽略)

RecordsCollecter (升級提供自定義數據源功能)

import _thread
import pymongo
import json
import math
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse

def url2Dict(url):
    query = urlparse(url).query  
    params = parse_qs(query)  
    result = {key: params[key][0] for key in params}  
    return result

class Provider(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            self.send_response(200)
            self.send_header("Content-type", "application/json")
            self.end_headers()

            dictParam = url2Dict(self.path)
            Log("自定義數據源服務接收到請求,self.path:", self.path, "query 參數:", dictParam)
            
            # 目前回測系統只能從列表中選擇交易所名稱,在添加自定義數據源時,設置爲幣安,即:Binance
            exName = exchange.GetName()                                     
            # 注意,period爲底層K線週期
            tabName = "%s_%s" % ("records", int(int(dictParam["period"]) / 1000))  
            priceRatio = math.pow(10, int(dictParam["round"]))
            amountRatio = math.pow(10, int(dictParam["vround"]))
            fromTS = int(dictParam["from"]) * int(1000)
            toTS = int(dictParam["to"]) * int(1000)
            
            
            # 連接數據庫
            Log("連接數據庫服務,獲取數據,數據庫:", exName, "表:", tabName)
            myDBClient = pymongo.MongoClient("mongodb://localhost:27017")
            ex_DB = myDBClient[exName]
            exRecords = ex_DB[tabName]
            
            
            # 要求應答的數據
            data = {
                "schema" : ["time", "open", "high", "low", "close", "vol"],
                "data" : []
            }
            
            # 構造查詢條件:大於某個值{'age': {'$gt': 20}} 小於某個值{'age': {'$lt': 20}}
            dbQuery = {"$and":[{'Time': {'$gt': fromTS}}, {'Time': {'$lt': toTS}}]}
            Log("查詢條件:", dbQuery, "查詢條數:", exRecords.find(dbQuery).count(), "數據庫總條數:", exRecords.find().count())
            
            for x in exRecords.find(dbQuery).sort("Time"):
                # 需要根據請求參數round和vround,處理數據精度
                bar = [x["Time"], int(x["Open"] * priceRatio), int(x["High"] * priceRatio), int(x["Low"] * priceRatio), int(x["Close"] * priceRatio), int(x["Volume"] * amountRatio)]
                data["data"].append(bar)
            
            Log("數據:", data, "響應回測系統請求。")
            # 寫入數據應答
            self.wfile.write(json.dumps(data).encode())
        except BaseException as e:
            Log("Provider do_GET error, e:", e)


def createServer(host):
    try:
        server = HTTPServer(host, Provider)
        Log("Starting server, listen at: %s:%s" % host)
        server.serve_forever()
    except BaseException as e:
        Log("createServer error, e:", e)
        raise Exception("stop")

def main():
    LogReset(1)
    exName = exchange.GetName()
    period = exchange.GetPeriod()
    Log("收集", exName, "交易所的K線數據,", "K線週期:", period, "秒")
    
    # 連接數據庫服務,服務地址 mongodb://127.0.0.1:27017 具體看服務器上安裝的mongodb設置
    Log("連接託管者所在設備mongodb服務,mongodb://localhost:27017")
    myDBClient = pymongo.MongoClient("mongodb://localhost:27017")   
    # 創建數據庫
    ex_DB = myDBClient[exName]
    
    # 打印目前數據庫表
    collist = ex_DB.list_collection_names()
    Log("mongodb ", exName, " collist:", collist)
    
    # 檢測是否刪除表
    arrDropNames = json.loads(dropNames)
    if isinstance(arrDropNames, list):
        for i in range(len(arrDropNames)):
            dropName = arrDropNames[i]
            if isinstance(dropName, str):
                if not dropName in collist:
                    continue
                tab = ex_DB[dropName]
                Log("dropName:", dropName, "刪除:", dropName)
                ret = tab.drop()
                collist = ex_DB.list_collection_names()
                if dropName in collist:
                    Log(dropName, "刪除失敗")
                else :
                    Log(dropName, "刪除成功")
    
    # 開啓一個線程,提供自定義數據源服務
    try:
        # _thread.start_new_thread(createServer, (("localhost", 9090), ))     # 本機測試
        _thread.start_new_thread(createServer, (("0.0.0.0", 9090), ))         # VPS服務器上測試
        Log("開啓自定義數據源服務線程", "#FF0000")
    except BaseException as e:
        Log("啓動自定義數據源服務失敗!")
        Log("錯誤信息:", e)
        raise Exception("stop")
    
    # 創建records表
    ex_DB_Records = ex_DB["%s_%d" % ("records", period)]
    Log("開始收集", exName, "K線數據", "週期:", period, "打開(創建)數據庫表:", "%s_%d" % ("records", period), "#FF0000")
    preBarTime = 0
    index = 1
    while True:
        r = _C(exchange.GetRecords)
        if len(r) < 2:
            Sleep(1000)
            continue
        if preBarTime == 0:
            # 首次寫入所有BAR數據
            for i in range(len(r) - 1):
                bar = r[i]
                # 逐根寫入,需要判斷當前數據庫表中是否已經有該條數據,基於時間戳檢測,如果有該條數據,則跳過,沒有則寫入
                retQuery = ex_DB_Records.find({"Time": bar["Time"]})
                if retQuery.count() > 0:
                    continue
                
                # 寫入bar到數據庫表
                ex_DB_Records.insert_one({"High": bar["High"], "Low": bar["Low"], "Open": bar["Open"], "Close": bar["Close"], "Time": bar["Time"], "Volume": bar["Volume"]})                
                index += 1
            preBarTime = r[-1]["Time"]
        elif preBarTime != r[-1]["Time"]:
            bar = r[-2]
            # 寫入數據前檢測,數據是否已經存在,基於時間戳檢測
            retQuery = ex_DB_Records.find({"Time": bar["Time"]})
            if retQuery.count() > 0:
                continue
            
            ex_DB_Records.insert_one({"High": bar["High"], "Low": bar["Low"], "Open": bar["Open"], "Close": bar["Close"], "Time": bar["Time"], "Volume": bar["Volume"]})
            index += 1
            preBarTime = r[-1]["Time"]
        LogStatus(_D(), "preBarTime:", preBarTime, "_D(preBarTime):", _D(preBarTime/1000), "index:", index)
        # 增加畫圖展示
        ext.PlotRecords(r, "%s_%d" % ("records", period))
        Sleep(10000)
        

測試

配置機器人

運行機器人,運行行情收集器。

打開一個測試策略,進行回測,例如這樣的回測策略,測試一下。

function main() {
    Log(exchange.GetRecords())
    Log(exchange.GetRecords())
    Log(exchange.GetRecords())
    Log(exchange.GetRecords())
    Log(exchange.GetRecords())
    Log(exchange.GetRecords())
    Log(exchange.GetRecords().length)
}

配置回測選項,設置交易所爲幣安是因爲暫時自定義數據源還不能自己制定一個交易所名稱,只能借用列表中的某個交易所配置上,回測時顯示的是幣安,實際是wexApp模擬盤的數據。

對比回測系統根據行情收集器作爲自定義數據源回測生成的圖表和wexApp交易所頁面上的1小時K線圖表是否相同。

這樣就可以讓VPS上的機器人自己收集K線數據,而我們可以隨時獲取這些收集的數據直接在回測系統回測了。
拋磚引玉,各位大神還可以繼續擴展,例如支持實盤級別回測自定義數據源,支持多品種、多市場數據收集等等功能。

歡迎留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章