行情收集器再升級--支持CSV格式文件導入提供自定義數據源

最近一個用戶需要讓自己的CSV格式文件作爲數據源,讓發明者量化交易平臺的回測系統使用。發明者量化交易平臺的回測系統功能衆多,使用簡潔高效,這樣只要自己有數據,就可以進行回測了,不再侷限於平臺數據中心支持的交易所、品種。

設計思路

設計思路其實很簡單,我們只要在之前的行情收集器基礎上稍微改動即可,我們給行情收集器增加一個參數isOnlySupportCSV用來控制是否只使用CSV文件作爲數據源提供給回測系統,再增加一個參數filePathForCSV,用於設置行情收集器機器人運行的服務器上放置CSV數據文件的路徑。最後就是根據isOnlySupportCSV參數是否設置爲True來決定使用那種數據源(1、自己收集的,2、CSV文件中的數據),這個改動主要在Provider類的do_GET函數中。

什麼是CSV文件?

逗號分隔值(Comma-Separated Values,CSV,有時也稱爲字符分隔值,因爲分隔字符也可以不是逗號),其文件以純文本形式存儲表格數據(數字和文本)。純文本意味着該文件是一個字符序列,不含必須像二進制數字那樣被解讀的數據。CSV文件由任意數目的記錄組成,記錄間以某種換行符分隔;每條記錄由字段組成,字段間的分隔符是其它字符或字符串,最常見的是逗號或製表符。通常,所有記錄都有完全相同的字段序列。通常都是純文本文件。建議使用WORDPAD或是記事本來開啓,再則先另存新檔後用EXCEL開啓,也是方法之一。

CSV文件格式的通用標準並不存在,但是有一定規律,一般爲一條記錄一行,第一行爲表頭。每行中的數據用逗號間隔。

例如,我們用於測試的CSV文件用記事本打開是這樣的:

觀察下,CSV文件第一行是表格頭。

,open,high,low,close,vol

我們就是要把這樣的數據解析整理,然後構造成回測系統自定義數據源要求的格式,這個我們之前的文章中的代碼裏已經處理了,只需稍加修改。

修改後的代碼

import _thread
import pymongo
import json
import math
import csv
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):
        global isOnlySupportCSV, filePathForCSV
        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)

            # 要求應答的數據
            data = {
                "schema" : ["time", "open", "high", "low", "close", "vol"],
                "data" : []
            }
            
            if isOnlySupportCSV:
                # 處理CSV讀取,filePathForCSV路徑
                listDataSequence = []
                with open(filePathForCSV, "r") as f:
                    reader = csv.reader(f)
                    # 獲取表頭
                    header = next(reader)
                    headerIsNoneCount = 0
                    if len(header) != len(data["schema"]):
                        Log("CSV文件格式有誤,列數不同,請檢查!", "#FF0000")
                        return 
                    for ele in header:
                        for i in range(len(data["schema"])):
                            if data["schema"][i] == ele or ele == "":
                                if ele == "":
                                    headerIsNoneCount += 1
                                if headerIsNoneCount > 1:
                                    Log("CSV文件格式有誤,請檢查!", "#FF0000")
                                    return 
                                listDataSequence.append(i)
                                break
                    
                    # 讀取內容
                    while True:
                        record = next(reader, -1)
                        if record == -1:
                            break
                        index = 0
                        arr = [0, 0, 0, 0, 0, 0]
                        for ele in record:
                            arr[listDataSequence[index]] = int(ele) if listDataSequence[index] == 0 else (int(float(ele) * amountRatio) if listDataSequence[index] == 5 else int(float(ele) * priceRatio))
                            index += 1
                        data["data"].append(arr)
                
                Log("數據:", data, "響應回測系統請求。")
                self.wfile.write(json.dumps(data).encode())
                return 
            
            # 連接數據庫
            Log("連接數據庫服務,獲取數據,數據庫:", exName, "表:", tabName)
            myDBClient = pymongo.MongoClient("mongodb://localhost:27017")
            ex_DB = myDBClient[exName]
            exRecords = ex_DB[tabName]
            
            # 構造查詢條件:大於某個值{'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)
    if (isOnlySupportCSV):
        try:
        # _thread.start_new_thread(createServer, (("localhost", 9090), ))         # 本機測試
            _thread.start_new_thread(createServer, (("0.0.0.0", 9090), ))         # VPS服務器上測試
            Log("開啓自定義數據源服務線程,數據由CSV文件提供。", "#FF0000")
        except BaseException as e:
            Log("啓動自定義數據源服務失敗!")
            Log("錯誤信息:", e)
            raise Exception("stop")
        while True:
            LogStatus(_D(), "只啓動自定義數據源服務,不收集數據!")
            Sleep(2000)
    
    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())
}

策略很簡單,只獲取並打印三次K線數據。

回測頁面,設置回測系統的數據源爲自定義數據源,並且地址填寫行情收集器機器人運行的服務器地址。由於我們的CSV文件中的數據爲1分鐘K線。所以回測時,我們設置K線週期爲1分鐘。

點擊開始回測,行情收集器機器人接收到了數據請求:

回測系統執行策略完成後,根據數據源中的K線數據,生成K線圖表。

對比文件中的數據:

RecordsCollecter (升級提供自定義數據源功能、支持CSV數據文件提供數據源)

拋磚引玉,歡迎留言。

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