最近一個用戶需要讓自己的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數據文件提供數據源)
拋磚引玉,歡迎留言。