diff --git a/README.md b/README.md index 75f214b..ad0923d 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ pip3 install https://github.com/fmzquant/backtest_python/archive/master.zip ## simple example ```python '''backtest -start: 2018-02-19 00:00:00 -end: 2018-03-22 12:00:00 +start: 2022-02-19 00:00:00 +end: 2022-03-22 12:00:00 period: 15m -exchanges: [{"eid":"OKEX","currency":"LTC_BTC","balance":3,"stocks":0}] +exchanges: [{"eid":"Binance","currency":"BTC_USDT","balance":10000,"stocks":0}] ''' from fmz import * task = VCtx(__doc__) # initialize backtest engine from __doc__ diff --git a/fmz.py b/fmz.py index d1cf529..082a444 100644 --- a/fmz.py +++ b/fmz.py @@ -22,6 +22,7 @@ import io import struct import threading +import uuid try: import md5 import urllib2 @@ -29,11 +30,19 @@ import hashlib as md5 import urllib.request as urllib2 +try: + import ssl + ssl._create_default_https_context = ssl._create_unverified_context +except: + pass + try: from urllib import urlencode except: from urllib.parse import urlencode +DATASERVER = os.getenv("DATASERVER", "http://q.fmz.com") + isPython3 = sys.version_info[0] >= 3 gg = globals() gg['NaN'] = None @@ -60,13 +69,16 @@ def json_loads(s): return json.loads(s.decode('utf-8')) return json.loads(s) +def b2s(s): + if isPython3: + return s.decode('utf-8') + return s + def safe_str(s): if isPython3: return s.encode('utf-8') return str(s) -CLUSTER_IP = os.getenv("CLUSTER_IP", "q.fmz.com") -CLUSTER_DOMAIN = os.getenv("CLUSTER_DOMAIN", "q.fmz.com") BT_Status = 1 << 0 BT_Symbols = 1 << 1 @@ -87,16 +99,25 @@ def getCacheDir(): pass return tmpCache -def httpGet(url, customHost=None): - req = urllib2.Request(url) - req.add_header('Accept-Encoding', 'gzip, deflate') - if customHost is not None: - req.add_header('Host', customHost) - resp = urllib2.urlopen(req) - data = resp.read() - if resp.info().get('Content-Encoding') == 'gzip': - data = zlib.decompress(data, 16+zlib.MAX_WBITS) - return data +def httpGet(url): + try: + req = urllib2.Request(url) + req.add_header('Accept-Encoding', 'gzip, deflate') + resp = urllib2.urlopen(req) + data = resp.read() + if resp.info().get('Content-Encoding') == 'gzip': + data = zlib.decompress(data, 16+zlib.MAX_WBITS) + return data + except urllib2.HTTPError as e: + headers = dict(e.info()) + error_body = e.read() + if e.info().get('Content-Encoding') == 'gzip': + try: + error_body = zlib.decompress(error_body, 16+zlib.MAX_WBITS) + except: + pass + print("urllib2.HTTPError - Code: %s, URL: %s, Headers: %s, Body: %s" % (e.code, e.url, headers, error_body)) + raise class Std: @staticmethod @@ -465,7 +486,9 @@ def __init__(self, name): self.__name = name sys.modules['talib'] = self def __getattr__(self, attr): - raise Exception('Please install %s module for python' % self.__name) + if attr == '__file__': + return 'talib.py' + raise Exception('Please install %s module for python (%s)' % (self.__name, attr)) class MyList(list): def __init__(self, data): @@ -478,7 +501,7 @@ def __getattr__(self, attr): for item in self.__data: ret.append(item[attr]) if HasTALib: - ret = numpy.array(ret) + ret = numpy.array(ret, dtype='float64') setattr(self, attr, ret) return ret @@ -529,13 +552,31 @@ def toObj(self): for k, t in self._fields_: if k[0].isupper(): v = getattr(self, k) + if k == 'Info' and hasattr(v, 's_js'): + if v.s_js_size > 0: + v = json.loads(v.s_js[:v.s_js_size]) + else: + v = None + elif k == 'Condition' and hasattr(v, 'ConditionType'): + if v.ConditionType == -1: + continue + else: + v = v.toObj() + elif k == 'ContractType' and len(v) == 0: + continue if isinstance(v, bytes): v = v.decode() obj[k] = v return dic2obj(obj) +class _INFO(_CSTRUCT): + _fields_ = [("s_js", ctypes.c_char_p), + ("s_js_size", ctypes.c_uint)] + class _TICKER(_CSTRUCT): _fields_ = [("Time", ctypes.c_ulonglong), + ("Symbol", ctypes.c_char * 31), + ("Open", ctypes.c_double), ("High", ctypes.c_double), ("Low", ctypes.c_double), ("Sell", ctypes.c_double), @@ -543,9 +584,13 @@ class _TICKER(_CSTRUCT): ("Last", ctypes.c_double), ("Volume", ctypes.c_double), ("OpenInterest", ctypes.c_double), - ("data", ctypes.c_char_p), - ("data_size", ctypes.c_uint), - ] + ("Info", _INFO)] + +class _FUNDING(_CSTRUCT): + _fields_ = [("Time", ctypes.c_ulonglong), + ("Rate", ctypes.c_double), + ("Interval", ctypes.c_uint), + ("Symbol", ctypes.c_char * 31)] class _RECORD(_CSTRUCT): _fields_ = [("Time", ctypes.c_ulonglong), @@ -563,10 +608,27 @@ class _ACCOUNT(_CSTRUCT): _fields_ = [("Balance", ctypes.c_double), ("FrozenBalance", ctypes.c_double), ("Stocks", ctypes.c_double), - ("FrozenStocks", ctypes.c_double)] + ("FrozenStocks", ctypes.c_double), + ("Equity", ctypes.c_double), + ("UPnL", ctypes.c_double)] + +class _ASSET(_CSTRUCT): + _fields_ = [("Currency", ctypes.c_char * 31), + ("Amount", ctypes.c_double), + ("FrozenAmount", ctypes.c_double)] + +class _ORDER_CONDITION(_CSTRUCT): + _fields_ = [ + ("ConditionType", ctypes.c_int), + ("TpTriggerPrice", ctypes.c_double), + ("TpOrderPrice", ctypes.c_double), + ("SlTriggerPrice", ctypes.c_double), + ("SlOrderPrice", ctypes.c_double), + ] class _ORDER(_CSTRUCT): _fields_ = [("Id", ctypes.c_ulonglong), + ("Time", ctypes.c_ulonglong), ("Price", ctypes.c_double), ("Amount", ctypes.c_double), ("DealAmount", ctypes.c_double), @@ -574,7 +636,9 @@ class _ORDER(_CSTRUCT): ("Type", ctypes.c_uint), ("Offset", ctypes.c_uint), ("Status", ctypes.c_uint), - ("ContractType", ctypes.c_char * 31)] + ("Symbol", ctypes.c_char * 31), + ("ContractType", ctypes.c_char * 31), + ("Condition", _ORDER_CONDITION)] class _TRADE(_CSTRUCT): _fields_ = [("Id", ctypes.c_ulonglong), @@ -591,6 +655,7 @@ class _POSITION(_CSTRUCT): ("Profit", ctypes.c_double), ("Margin", ctypes.c_double), ("Type", ctypes.c_uint), + ("Symbol", ctypes.c_char * 31), ("ContractType", ctypes.c_char * 31)] def EOF(): raise EOFError() @@ -602,7 +667,7 @@ def __init__(self, v): self.v = v AsyncRet.routineId += 1 - def wait(self, timeout=-1): + def wait(self, timeout=0): if self.isWait: return (None, False) self.isWait = True @@ -621,6 +686,7 @@ def __init__(self, lib, ctx, idx, opt, cfg): self.name = cfg["Id"] self.label = cfg["Label"] self.currency = '%s_%s' % (cfg["BaseCurrency"], cfg["QuoteCurrency"]) + self.baseCurrency = cfg["BaseCurrency"] self.quoteCurrency = cfg["QuoteCurrency"] self.maxBarLen = cfg.get('MaxBarLen', 1000) self.period = opt['Period'] @@ -639,6 +705,9 @@ def GetLabel(self): def GetCurrency(self): return self.currency + def GetBaseCurrency(self): + return self.baseCurrency + def GetQuoteCurrency(self): return self.quoteCurrency @@ -650,7 +719,7 @@ def SetMaxBarLen(self, n): self.maxBarLen = n def SetPrecision(self, a, b): - pass + self.lib.api_Exchange_SetPrecision(self.ctx, self.idx, ctypes.c_double(a), ctypes.c_double(b)) def GetRate(self): self.lib.api_Exchange_GetRate.restype = ctypes.c_double @@ -663,7 +732,18 @@ def SetTimeout(self, ms): pass def SetBase(self, s): - pass + r = ctypes.c_char_p() + self.lib.api_Exchange_SetBase(self.ctx, self.idx, safe_str(s), ctypes.byref(r)) + detail = b2s(r.value) + self.lib.api_free(r) + return detail + + def GetBase(self): + r = ctypes.c_char_p() + self.lib.api_Exchange_GetBase(self.ctx, self.idx, ctypes.byref(r)) + detail = b2s(r.value) + self.lib.api_free(r) + return detail def SetCurrency(self, s): arr = s.split('_') @@ -676,10 +756,10 @@ def SetRate(self, rate=1.0): self.lib.api_Exchange_SetRate.restype = ctypes.c_double return self.lib.api_Exchange_SetRate(self.ctx, self.idx, ctypes.c_double(rate)) - def GetTrades(self): + def GetTrades(self, symbol=''): r_len = ctypes.c_uint(0) buf_ptr = ctypes.c_void_p() - ret = self.lib.api_Exchange_GetTrades(self.ctx, self.idx, ctypes.byref(r_len), ctypes.byref(buf_ptr)) + ret = self.lib.api_Exchange_GetTrades(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) if ret == API_ERR_SUCCESS: n = r_len.value @@ -703,30 +783,74 @@ def GetData(self, name, timeout=60000, offset=0): r = _TICKER() ret = self.lib.api_Exchange_GetData(self.ctx, self.idx, ctypes.byref(r), safe_str(name), int(timeout), int(offset)) if ret == API_ERR_SUCCESS: - return dic2obj({'Time': r.Time, 'Data': json.loads(r.data[:r.data_size]) if r.data_size > 0 else None}) + return dic2obj({'Time': r.Time, 'Data': json.loads(r.Info.s_js[:r.Info.s_js_size]) if r.Info.s_js_size > 0 else None}) elif ret == API_ERR_FAILED: return None EOF() - def GetTicker(self): + def GetTickers(self): + r_len = ctypes.c_uint(0) + buf_ptr = ctypes.c_void_p() + ret = self.lib.api_Exchange_GetTickers(self.ctx, self.idx, ctypes.byref(r_len), ctypes.byref(buf_ptr)) + + if ret == API_ERR_SUCCESS: + n = r_len.value + eles = [] + if n > 0: + group_array = (_TICKER * n).from_address(buf_ptr.value) + for i in range(0, n): + eles.append(group_array[i].toObj()) + self.lib.api_free(buf_ptr) + return eles + elif ret == API_ERR_FAILED: + return None + EOF() + + def GetMarkets(self): + r = ctypes.c_char_p() + self.lib.api_Exchange_GetMarkets(self.ctx, self.idx, ctypes.byref(r)) + ret = json.loads(b2s(r.value)) + self.lib.api_free(r) + return ret + + + def GetTicker(self, symbol=''): r = _TICKER() - ret = self.lib.api_Exchange_GetTicker(self.ctx, self.idx, ctypes.byref(r)) + ret = self.lib.api_Exchange_GetTicker(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r)) if ret == API_ERR_SUCCESS: return r.toObj() elif ret == API_ERR_FAILED: return None EOF() + def GetFundings(self, symbol=''): + r_len = ctypes.c_uint(0) + buf_ptr = ctypes.c_void_p() + ret = self.lib.api_Exchange_GetFundings(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) + + if ret == API_ERR_SUCCESS: + n = r_len.value + eles = [] + if n > 0: + group_array = (_FUNDING * n).from_address(buf_ptr.value) + for i in range(0, n): + eles.append(group_array[i].toObj()) + self.lib.api_free(buf_ptr) + return eles + elif ret == API_ERR_FAILED: + return None + EOF() + def IO(self, k, v = 0): if k == 'currency': return self.SetCurrency(v) return self.lib.api_Exchange_IO(self.ctx, self.idx, safe_str(k), int(v)) - def GetDepth(self): + def GetDepth(self, symbol=''): ask_len = ctypes.c_uint(0) bid_len = ctypes.c_uint(0) buf_ptr = ctypes.c_void_p() - ret = self.lib.api_Exchange_GetDepth(self.ctx, self.idx, ctypes.byref(ask_len), ctypes.byref(bid_len), ctypes.byref(buf_ptr)) + ret = self.lib.api_Exchange_GetDepth(self.ctx, self.idx, safe_str(symbol), ctypes.byref(ask_len), ctypes.byref(bid_len), ctypes.byref(buf_ptr)) if ret == API_ERR_SUCCESS: n = ask_len.value + bid_len.value @@ -745,12 +869,16 @@ def GetDepth(self): return None EOF() - def GetRecords(self, period=-1): + def GetRecords(self, symbol='', period=-1, limit=0): + if isinstance(symbol, int): + limit = period + period = symbol + symbol = '' if period == -1: period = int(self.period/1000) r_len = ctypes.c_uint(0) buf_ptr = ctypes.c_void_p() - ret = self.lib.api_Exchange_GetRecords(self.ctx, self.idx, ctypes.c_long(int(period)), ctypes.byref(r_len), ctypes.byref(buf_ptr)) + ret = self.lib.api_Exchange_GetRecords(self.ctx, self.idx, safe_str(symbol), ctypes.c_long(int(period)), ctypes.c_long(int(limit)), ctypes.byref(r_len), ctypes.byref(buf_ptr)) if ret == API_ERR_SUCCESS: n = r_len.value @@ -760,10 +888,12 @@ def GetRecords(self, period=-1): for i in range(0, n): eles.append(group_array[i].toObj()) self.lib.api_free(buf_ptr) - k = '%s/%s/%d' % (self.currency, self.ct, period) + k = '%s/%s/%s/%d' % (self.currency, symbol, self.ct, period) c = self.records_cache.get(k, None) if c is None or len(c) == 0: - self.records_cache[k] = eles[len(eles)-self.maxBarLen:] + if len(eles) > self.maxBarLen: + eles = eles[len(eles)-self.maxBarLen:] + self.records_cache[k] = eles else: preTime = 0 if len(c) == 0 else c[-1]['Time'] for ele in eles: @@ -775,7 +905,28 @@ def GetRecords(self, period=-1): if len(c) > self.maxBarLen: c.pop(0) preTime = t - return MyList(self.records_cache[k]) + r = MyList(self.records_cache[k]) + if limit > 0: + r = r[-limit:] + return r + elif ret == API_ERR_FAILED: + return None + EOF() + + def GetAssets(self): + r_len = ctypes.c_uint(0) + buf_ptr = ctypes.c_void_p() + ret = self.lib.api_Exchange_GetAssets(self.ctx, self.idx, ctypes.byref(r_len), ctypes.byref(buf_ptr)) + + if ret == API_ERR_SUCCESS: + n = r_len.value + eles = [] + if n > 0: + group_array = (_ASSET * n).from_address(buf_ptr.value) + for i in range(0, n): + eles.append(group_array[i].toObj()) + self.lib.api_free(buf_ptr) + return eles elif ret == API_ERR_FAILED: return None EOF() @@ -805,10 +956,48 @@ def Sell(self, price, amount=None, *extra): return None EOF() - def GetOrders(self): + def CreateOrder(self, symbol, side, price, amount=None, *extra): + ret = self.lib.api_Exchange_CreateOrder(self.ctx, self.idx, safe_str(symbol), safe_str(side), ctypes.c_double(price), ctypes.c_double(amount), JoinArgs(extra)) + if ret > 0: + return int(ret) + elif ret == API_ERR_FAILED: + return None + EOF() + + def ModifyOrder(self, orderId, side, price, amount=None, *extra): + ret = self.lib.api_Exchange_ModifyOrder(self.ctx, self.idx, ctypes.c_int(orderId), safe_str(side), ctypes.c_double(price), ctypes.c_double(amount), JoinArgs(extra)) + if ret > 0: + return int(ret) + elif ret == API_ERR_FAILED: + return None + EOF() + + def GetOrders(self, symbol=''): r_len = ctypes.c_uint(0) buf_ptr = ctypes.c_void_p() - ret = self.lib.api_Exchange_GetOrders(self.ctx, self.idx, ctypes.byref(r_len), ctypes.byref(buf_ptr)) + ret = self.lib.api_Exchange_GetOrders(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) + + if ret == API_ERR_SUCCESS: + n = r_len.value + eles = [] + if n > 0: + group_array = (_ORDER * n).from_address(buf_ptr.value) + for i in range(0, n): + eles.append(group_array[i].toObj()) + self.lib.api_free(buf_ptr) + return eles + elif ret == API_ERR_FAILED: + return None + EOF() + + def GetHistoryOrders(self, symbol='', since=0, limit = 0): + if isinstance(symbol, int): + limit = since + since = symbol + symbol = '' + r_len = ctypes.c_uint(0) + buf_ptr = ctypes.c_void_p() + ret = self.lib.api_Exchange_GetHistoryOrders(self.ctx, self.idx, safe_str(symbol), ctypes.c_longlong(since), ctypes.c_longlong(limit), ctypes.byref(r_len), ctypes.byref(buf_ptr)) if ret == API_ERR_SUCCESS: n = r_len.value @@ -823,12 +1012,6 @@ def GetOrders(self): return None EOF() - def Log(self, orderType, price, amount=0, *extra): - ret = self.lib.api_Exchange_Log(self.ctx, self.idx, ctypes.c_int(orderType), ctypes.c_double(price), ctypes.c_double(amount), JoinArgs(extra)) - if orderType == 2: - return bool(ret) - if ret > 0: - return int(ret) def GetOrder(self, orderId): r = _ORDER() @@ -840,12 +1023,91 @@ def GetOrder(self, orderId): EOF() def CancelOrder(self, orderId, *extra): - self.lib.api_Exchange_CancelOrder.restype = ctypes.c_bool ret = self.lib.api_Exchange_CancelOrder(self.ctx, self.idx, ctypes.c_int(orderId), JoinArgs(extra)) if ret == API_ERR_EOF: EOF() return ret == API_ERR_SUCCESS + # condition orders + def CreateConditionOrder(self, symbol, side, amount, condition, *extra): + ret = self.lib.api_Exchange_CreateConditionOrder(self.ctx, self.idx, safe_str(symbol), safe_str(side), ctypes.c_double(amount), safe_str(json.dumps(condition)), JoinArgs(extra)) + if ret > 0: + return int(ret) + elif ret == API_ERR_FAILED: + return None + EOF() + + def ModifyConditionOrder(self, orderId, side, amount, condition, *extra): + ret = self.lib.api_Exchange_ModifyConditionOrder(self.ctx, self.idx, ctypes.c_int(orderId), safe_str(side), ctypes.c_double(amount), safe_str(json.dumps(condition)), JoinArgs(extra)) + if ret > 0: + return int(ret) + elif ret == API_ERR_FAILED: + return None + EOF() + + def GetConditionOrders(self, symbol=''): + r_len = ctypes.c_uint(0) + buf_ptr = ctypes.c_void_p() + ret = self.lib.api_Exchange_GetConditionOrders(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) + + if ret == API_ERR_SUCCESS: + n = r_len.value + eles = [] + if n > 0: + group_array = (_ORDER * n).from_address(buf_ptr.value) + for i in range(0, n): + eles.append(group_array[i].toObj()) + self.lib.api_free(buf_ptr) + return eles + elif ret == API_ERR_FAILED: + return None + EOF() + + def GetHistoryConditionOrders(self, symbol='', since=0, limit = 0): + if isinstance(symbol, int): + limit = since + since = symbol + symbol = '' + r_len = ctypes.c_uint(0) + buf_ptr = ctypes.c_void_p() + ret = self.lib.api_Exchange_GetHistoryConditionOrders(self.ctx, self.idx, safe_str(symbol), ctypes.c_longlong(since), ctypes.c_longlong(limit), ctypes.byref(r_len), ctypes.byref(buf_ptr)) + + if ret == API_ERR_SUCCESS: + n = r_len.value + eles = [] + if n > 0: + group_array = (_ORDER * n).from_address(buf_ptr.value) + for i in range(0, n): + eles.append(group_array[i].toObj()) + self.lib.api_free(buf_ptr) + return eles + elif ret == API_ERR_FAILED: + return None + EOF() + + + def GetConditionOrder(self, orderId): + r = _ORDER() + ret = self.lib.api_Exchange_GetConditionOrder(self.ctx, self.idx, ctypes.c_int(orderId), ctypes.byref(r)) + if ret == API_ERR_SUCCESS: + return r.toObj() + elif ret == API_ERR_FAILED: + return None + EOF() + + def CancelConditionOrder(self, orderId, *extra): + ret = self.lib.api_Exchange_CancelConditionOrder(self.ctx, self.idx, ctypes.c_int(orderId), JoinArgs(extra)) + if ret == API_ERR_EOF: + EOF() + return ret == API_ERR_SUCCESS + + def Log(self, orderType, price, amount=0, *extra): + ret = self.lib.api_Exchange_Log(self.ctx, self.idx, ctypes.c_int(orderType), ctypes.c_double(price), ctypes.c_double(amount), JoinArgs(extra)) + if orderType == 2: + return bool(ret) + if ret > 0: + return int(ret) + def GetContractType(self): return self.ct @@ -867,18 +1129,22 @@ def SetContractType(self, symbol): return None EOF() - def SetMarginLevel(self, level): + def SetMarginLevel(self, symbol, level=None): + if isinstance(symbol, int) or isinstance(symbol, float): + level, symbol = symbol, level + if not symbol: + symbol = '' self.lib.api_Exchange_SetMarginLevel.restype = ctypes.c_bool - return self.lib.api_Exchange_SetMarginLevel(self.ctx, self.idx, ctypes.c_int(level)) + return self.lib.api_Exchange_SetMarginLevel(self.ctx, self.idx, safe_str(symbol), ctypes.c_int(level)) def SetDirection(self, direction): self.lib.api_Exchange_SetMarginLevel.restype = ctypes.c_bool return self.lib.api_Exchange_SetDirection(self.ctx, self.idx, safe_str(direction)) - def GetPosition(self): + def GetPositions(self, symbol=''): r_len = ctypes.c_uint(0) buf_ptr = ctypes.c_void_p() - ret = self.lib.api_Exchange_GetPosition(self.ctx, self.idx, ctypes.byref(r_len), ctypes.byref(buf_ptr)) + ret = self.lib.api_Exchange_GetPositions(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) if ret == API_ERR_SUCCESS: n = r_len.value @@ -893,6 +1159,9 @@ def GetPosition(self): return None EOF() + def GetPosition(self): + return self.GetPositions() + class Chart(object): def __init__(self, lib, ctx, js): self.lib = lib @@ -911,6 +1180,252 @@ def add(self, seriesIdx, d, replaceId=None): def reset(self, keep=0): self.lib.api_Chart_Reset(self.ctx, keep) +class KLineChart(): + def __init__(self, lib, ctx, options={}): + options["__isCandle"] = True + self.chart = Chart(lib, ctx, options) + self.bar = None + self.overlay = options.get("overlay", False) + self.preTime = 0 + self.runtime = { "plots": [], "signals": [], "titles": {}, "count": 0 } + + def trim(self, obj): + dst = {} + for k in obj: + if obj[k] is not None: + dst[k] = obj[k] + return dst + + def newPlot(self, obj): + shape = self.trim(obj) + if "overlay" not in shape: + shape["overlay"] = self.overlay + if shape["type"] != 'shape' and shape["type"] != 'bgcolor' and shape["type"] != 'barcolor': + if "title" not in shape or len(shape["title"]) == 0 or shape["title"] in self.runtime["titles"]: + shape["title"] = '<' + shape.get("title","plot") + '_' + str(self.runtime["count"]) + '>' + self.runtime["count"] += 1 + if "title" in shape: + self.runtime["titles"][shape["title"]] = True + return shape + + + def begin(self, bar): + self.bar = bar + + def reset(self, remain=0): + self.chart.reset(remain) + self.preTime = 0 + + def close(self, bar=None): + if bar: + self.bar = bar + + if self.bar["Time"] < self.preTime: + return + + data = { + "timestamp": self.bar["Time"], + "open": self.bar["Open"], + "high": self.bar["High"], + "low": self.bar["Low"], + "close": self.bar["Close"], + "volume": self.bar.get("Volume", 0), + } + + for k in ["plots", "signals"]: + if len(self.runtime[k]) > 0: + if "runtime" not in data: + data["runtime"] = {} + data["runtime"][k] = self.runtime[k] + + if self.preTime == self.bar["Time"]: + self.chart.add(0, data, -1) + else: + self.chart.add(0, data) + + self.preTime = self.bar["Time"] + self.runtime["plots"] = [] + self.runtime["signals"] = [] + self.runtime["titles"] = {} + self.runtime["count"] = 0 + + def plot(self, series=None, title=None, color=None, linewidth=1, style="line", trackprice=None, histbase=0, offset=0, join=False, editable=False, show_last=None, display ="all", overlay=None): + if series is None or self.bar["Time"] < self.preTime: + return + + self.runtime["plots"].append(self.newPlot({ + "series": series, + "overlay": overlay, + "title": title, + "join": join, + "color": color, + "histbase": histbase, + "type": style, + "lineWidth": linewidth, + "display": display, + "offset": offset + })) + return len(self.runtime["plots"]) - 1 + + def barcolor(self, color, offset=None, editable=False, show_last=None, title=None, display="all"): + if display != "all" or self.bar["Time"] < self.preTime: + return + self.runtime["plots"].append(self.newPlot({ + "type": 'barcolor', + "title": title, + "color": color, + "offset": offset, + "showLast": show_last, + "display": display + })) + + def plotarrow(self, series, title=None, colorup="#00ff00", + colordown = "#ff0000", + offset = 0, + minheight = 5, + maxheight = 100, + editable = False, show_last=None, display = "all", overlay = None): + if display != "all" or self.bar["Time"] < self.preTime: + return + + self.runtime["plots"].append(self.newPlot({ + "series": series, + "title": title, + "colorup": colorup, + "colordown": colordown, + "offset": offset, + "minheight": minheight, + "maxheight": maxheight, + "showLast": show_last, + "type": "shape", + "style": "arrow", + "display": display, + "overlay": overlay + })) + + def hline(self, price, title = None, color = None, linestyle = "dashed", linewidth = None, editable = False, display = "all", overlay = None): + if display != "all" or self.bar["Time"] < self.preTime: + return + + self.runtime["plots"].append(self.newPlot({ + "title": title, + "price": price, + "overlay": overlay, + "color": color, + "type": 'hline', + "lineStyle": linestyle, + "lineWidth": linewidth, + "display": display + })) + return len(self.runtime["plots"]) - 1 + + + def bgcolor(self, color, offset=None, editable=None, show_last=None, title=None, display = "all", overlay=None): + if display != "all" or self.bar["Time"] < self.preTime: + return + self.runtime["plots"].append(self.newPlot({ + "title": title, + "overlay": overlay, + "color": color, + "type": 'bgcolor', + "showLast": show_last, + "offset": offset + })) + + + def plotchar(self, series, title=None, char=None, location = "abovebar", color=None, offset=None, text=None, textcolor=None, editable=None, size = "auto", show_last=None, display="all", overlay=None): + if (location != "absolute" and series is None) or (location == "absolute" and series is None) or char is None or self.bar["Time"] < self.preTime: + return + + self.runtime["plots"].append(self.newPlot({ + "overlay": overlay, + "type": "shape", + "style": "char", + "char": char, + "series": series, + "location": location, + "color": color, + "offset": offset, + "size": size, + "text": text, + "textColor": textcolor + })) + + + + def plotshape(self, series, title=None, style=None, location="abovebar", color=None, offset=None, text=None, textcolor=None, editable=None, size = "auto", show_last=None, display="all", overlay=None): + if (location != "absolute" and series is None) or (location == "absolute" and series is None) or self.bar["Time"] < self.preTime: + return + + self.runtime["plots"].append(self.newPlot({ + "type": "shape", + "overlay": overlay, + "title": title, + "size": size, + "style": style, + "series": series, + "location": location, + "color": color, + "offset": offset, + "text": text, + "textColor": textcolor + })) + + def plotcandle(self, open, high, low, close, title=None, color=None, wickcolor=None, editable=None, show_last=None, bordercolor=None, display="all", overlay=None): + if display != "all" or self.bar["Time"] < self.preTime: + return + + self.runtime["plots"].append(self.newPlot({ + "price": high, + "open": open, + "high": high, + "low": low, + "close": close, + "title": title, + "color": color, + "wickColor": wickcolor, + "showLast": show_last, + "borderColor": bordercolor, + "type": "candle", + "display": display, + "overlay": overlay, + })) + + def fill(self, plot1, plot2, color=None, title=None, editable=None, show_last=None, fillgaps=None, display="all"): + if self.bar["Time"] < self.preTime: + return + if plot1 >= 0 and plot2 >= 0 and plot1 < len(self.runtime["plots"]) and plot2 < len(self.runtime["plots"]) and display == "all": + dst = self.runtime["plots"][plot1] + if "fill" not in dst: + dst["fill"] = [] + dst["fill"].append(self.trim({ + "value": self.runtime["plots"][plot2]["series"], + "color": color, + "showLast": show_last + })) + + def signal(self, direction, price, qty, id=None): + if self.bar["Time"] < self.preTime: + return + task = { + "id": id or direction, + "avgPrice": price, + "qty": qty + } + if direction == "buy" or direction == "long": + task["direction"] = "long" + elif direction == "sell" or direction == "short": + task["direction"] = "short" + elif direction == "closesell" or direction == "closeshort": + task["direction"] = "close" + task["closeDirection"] = "short" + elif direction == "closebuy" or direction == "closelong": + task["direction"] = "close" + task["closeDirection"] = "long" + + if task["direction"] or task["closeDirection"]: + self.runtime["signals"].append(task) + def periodToMs(s, default): period = default if len(s) < 2: @@ -938,6 +1453,7 @@ def parseTask(s): v = arr[1].strip() dic[k] = v pnl = dic.get('pnl', 'true') + dataServer = dic.get('dataServer', DATASERVER) period = periodToMs(dic.get('period', '1h'), 60000 * 60) basePeriod = periodToMs(dic.get('basePeriod', ''), 0) @@ -959,71 +1475,41 @@ def parseTask(s): elif period == 300000: basePeriod = 60000 feeDef = { - 'OKCoin_EN': [150, 200], 'Huobi': [150, 200], - 'OKEX': [150, 200], + 'OKX': [150, 200], 'Binance': [150, 200], 'Futures_BitMEX': [8, 10], - 'Futures_OKCoin': [30, 30], + 'Futures_OKX': [30, 30], 'Futures_HuobiDM': [30, 30], 'Futures_CTP': [25, 25], - 'Futures_LTS': [30, 130], + 'Futures_XTP': [30, 130], } + if e['eid'] == "Futures_CTP": + dataServer = "http://q.youquant.com" fee = e.get('fee') if fee is None: - fee = feeDef.get(e['eid'], [200, 200]) + fee = feeDef.get(e['eid'], [2000, 2000]) else: - fee = [int(fee[0]*1000), int(fee[1]*1000)] + fee = [int(fee[0]*10000), int(fee[1]*10000)] cfg = { - "Balance": e.get('balance', 10000.0), - "BaseCurrency": arr[0], - "BasePeriod": basePeriod, - "BasePrecision": 4, - "DepthDeep": 5, - "DepthAmount": 20, - "FaultTolerant": 0, - "FeeDenominator": 5, - "FeeMaker": fee[0], - "FeeTaker": fee[1], - "FeeMin": e.get('feeMin', 0), - "Id": e['eid'], - "Label": e['eid'], - "PriceTick": 1e-05, - "QuoteCurrency": arr[1], - "QuotePrecision": 8, - "SlipPoint": 0, - "Stocks": e.get('stocks', 3.0) - } - if e['eid'] == 'Futures_CTP': - cfg['BasePrecision'] = 0 - cfg['QuotePrecision'] = 1 - cfg['DepthDeep'] = 1 - elif e['eid'] == 'Futures_OKCoin' or e['eid'] == 'Futures_HuobiDM': - cfg['BasePrecision'] = 0 - cfg['QuotePrecision'] = 5 - elif e['eid'] == 'Bitfinex' or e['eid'] == 'Binance': - cfg['BasePrecision'] = 4 - cfg['QuotePrecision'] = 4 - cfg['PriceTick'] = 0.001 - elif e['eid'] == 'OKCoin_EN': - cfg['BasePrecision'] = 3 - cfg['QuotePrecision'] = 3 - cfg['PriceTick'] = 0.01 - elif e['eid'] == 'Futures_BitMEX': - cfg['PriceTick'] = 0.5 - bm = cfg['BasePeriod']/60000 - if bm == 15 or bm == 30: - cfg['BasePeriod'] = 300000 - elif 'Futures' not in e['eid']: - cfg['BasePrecision'] = 4 - cfg['QuotePrecision'] = 8 - cfg['PriceTick'] = 0.00000001 + "Balance": e.get('balance', 10000.0), + "Stocks": e.get('stocks', 3.0), + "BaseCurrency": arr[0], + "QuoteCurrency": arr[1], + "BasePeriod": basePeriod, + "FeeDenominator": 6, + "FeeMaker": fee[0], + "FeeTaker": fee[1], + "FeeMin": e.get('feeMin', 0), + "Id": e['eid'], + "Label": e['eid'] + } exchanges.append(cfg) options = { - "DataServer": CLUSTER_DOMAIN, + "DataServer": dataServer, "MaxChartLogs": 800, "MaxProfitLogs": 800, "MaxRuntimeLogs": 800, @@ -1034,15 +1520,15 @@ def parseTask(s): "TimeEnd": int(time.mktime(datetime.datetime.strptime(dic.get('end', '2019-02-10 00:00:00'), "%Y-%m-%d %H:%M:%S").timetuple())), "UpdatePeriod": 5000 } - snapshortPeriod = 86400 + snapshotPeriod = 86400 testRange = options['TimeEnd'] - options['TimeBegin'] if testRange / 3600 <= 2: - snapshortPeriod = 60 + snapshotPeriod = 60 elif testRange / 86400 <= 2: - snapshortPeriod = 300 + snapshotPeriod = 300 elif testRange / 86400 < 30: - snapshortPeriod = 3600 - options['SnapshortPeriod'] = snapshortPeriod * 1000 + snapshotPeriod = 3600 + options['SnapshotPeriod'] = snapshotPeriod * 1000 if pnl == 'true': options['RetFlags'] |= BT_Accounts_PnL return {'Exchanges': exchanges, 'Options': options} @@ -1072,18 +1558,19 @@ def __init__(self, task = None, autoRun=False, gApis = None, progressCallback=No self.progessCallbackPtr = ctypes.CFUNCTYPE(None, ctypes.c_char_p)(self.progressCallback) osName = platform.system() archName = platform.architecture()[0] - if osName == 'Linux' and hasattr(os, 'uname') and 'arm' in os.uname()[4]: - archName = 'arm' - self.os = '%s/%s' % (osName.lower(), 'amd64' if archName == '64bit' else '386') + if platform.processor() == "arm": + archName = 'arm64' if archName == '64bit' else 'arm' + self.os = '%s/%s' % (osName.lower(), archName) soName = 'backtest_py_%s_%s.so' % (osName.lower(), archName) + crcFile = 'md5.json' loader = os.path.join("./depends", soName) if not os.path.exists(loader): hdic = {} tmpCache = getCacheDir() - js = os.path.join(tmpCache, 'md5.json') + js = os.path.join(tmpCache, crcFile) if os.path.exists(js): b = open(js, 'rb').read() - if os.getenv("BOTVS_TASK_UUID") is None or "83a77b60a1dc484b533033d948b7aa5a" in str(b): + if os.getenv("BOTVS_TASK_UUID") is None or "c973408e4dbc8ce78f0fa7f6ff99275a" in str(b): hdic = json_loads(b) loader = os.path.join(tmpCache, soName) update = False @@ -1100,12 +1587,13 @@ def __init__(self, task = None, autoRun=False, gApis = None, progressCallback=No except: pass if update: - open(loader, 'wb').write(httpGet("http://" + CLUSTER_IP + "/dist/depends/" + soName, CLUSTER_DOMAIN)) - open(js, 'wb').write(httpGet("http://" + CLUSTER_IP + "/dist/depends/md5.json", CLUSTER_DOMAIN)) - #declare - lib = ctypes.CDLL(loader) + open(loader, 'wb').write(httpGet(task["Options"]["DataServer"] + "/dist/depends/" + soName)) + open(js, 'wb').write(httpGet(task["Options"]["DataServer"] + "/dist/depends/" + crcFile)) + else: + print("load from debug mode", loader) + lib = ctypes.CDLL(os.path.abspath(loader)) lib.api_backtest.restype = ctypes.c_void_p - ctx = ctypes.c_void_p(lib.api_backtest(safe_str(json.dumps(task)), self.httpGetPtr, self.progessCallbackPtr)) + ctx = ctypes.c_void_p(lib.api_backtest(safe_str(json.dumps(task)), self.httpGetPtr, self.progessCallbackPtr, None)) if not ctx: raise 'Initialize backtest engine error' self.ctx = ctx @@ -1144,6 +1632,11 @@ def __init__(self, task = None, autoRun=False, gApis = None, progressCallback=No gApis["ORDER_OFFSET_OPEN"] = 0 gApis["ORDER_OFFSET_CLOSE"] = 1 + gApis["ORDER_CONDITION_TYPE_OCO"] = 0 + gApis["ORDER_CONDITION_TYPE_TP"] = 1 + gApis["ORDER_CONDITION_TYPE_SL"] = 2 + gApis["ORDER_CONDITION_TYPE_GENERIC"] = 3 + gApis["PD_LONG"] = 0 gApis["PD_SHORT"] = 1 gApis["PD_LONG_YD"] = 2 @@ -1163,8 +1656,14 @@ def __init__(self, task = None, autoRun=False, gApis = None, progressCallback=No gApis["PERIOD_M15"] = 60 * 15 gApis["PERIOD_M30"] = 60 * 30 gApis["PERIOD_H1"] = 60 * 60 + gApis["PERIOD_H2"] = 60 * 60 * 2 + gApis["PERIOD_H4"] = 60 * 60 * 4 + gApis["PERIOD_H6"] = 60 * 60 * 6 + gApis["PERIOD_H12"] = 60 * 60 * 12 gApis["PERIOD_D1"] = 60 * 60 * 24 + gApis["PERIOD_D3"] = 60 * 60 * 24 * 3 gApis["PERIOD_W1"] = 60 * 60 * 24 * 7 + if autoRun: try: gApis['main']() @@ -1172,14 +1671,8 @@ def __init__(self, task = None, autoRun=False, gApis = None, progressCallback=No pass self.Join() - def update(self): - try: - os.remove(os.path.join(getCacheDir(), 'md5.json')) - except: - pass - def httpGetCallback(self, path, ptr_buf, ptr_size, ptr_need_free): - url = 'http://' + CLUSTER_IP + path.decode('utf8') + url = path.decode('utf8') tmpCache = getCacheDir() cacheFile = tmpCache+'/botvs_kline_'+md5.md5(path).hexdigest() data = None @@ -1188,7 +1681,7 @@ def httpGetCallback(self, path, ptr_buf, ptr_size, ptr_need_free): data = open(cacheFile, 'rb').read() cacheFile = None else: - data = httpGet(url, CLUSTER_DOMAIN) + data = httpGet(url) if len(data) > 0: open(cacheFile, 'wb').write(data) except: @@ -1260,6 +1753,15 @@ def g_MD5(self, s): def g_HttpQuery(self, *args): return 'dummy' + def g_HttpQuery_Go(self, *args): + return AsyncRet('dummy') + + def g_JSONParse(self, s): + return json.loads(s) + + def g_UUID(self): + return str(uuid.uuid4()) + def g_StrDecode(self, s, c='gbk'): self.g_LogError("sandbox not support StrDecode") @@ -1269,9 +1771,21 @@ def g_EnableLogLocal(self, b): def g_Dial(self, *args): self.g_LogError("sandbox not support Dial") + def g_DBExec(self, *args): + self.g_LogError("sandbox not support DBExec") + + def g_Encode(self, *args): + self.g_LogError("sandbox not support Encode") + + def g_EventLoop(self, *args): + self.g_LogError("sandbox not support EventLoop") + def g_Mail(self, *args): return True + def g_Mail_Go(self, *args): + return AsyncRet(True) + def g_GetCommand(self): return '' @@ -1281,11 +1795,18 @@ def g_GetMeta(self): def g_SetErrorFilter(self, s): pass + def g_SetChannelData(self, s): + pass + + def g_GetChannelData(self, s): + return '' + def g_GetOS(self): return self.os - def g_Version(self): - return '3.3' + def g_Version(self, detail=False): + self.lib.api_Version.restype = ctypes.c_char_p + return self.lib.api_Version(self.ctx, detail).decode('utf-8') def g_IsVirtual(self): return True @@ -1293,6 +1814,9 @@ def g_IsVirtual(self): def g_Chart(self, js): return Chart(self.lib, self.ctx, js) + def g_KLineChart(self, js={}): + return KLineChart(self.lib, self.ctx, js) + def g_GetPid(self): return os.getpid() @@ -1355,8 +1879,12 @@ def g__T(self, a, b=None): return '[trans]'+r+'[/trans]' def g__N(self, n, precision=4): - d = pow(10, precision) - return int(n*d) / float(d) + if precision < 0: + precision_factor = 10 ** -precision + return n - (n % precision_factor) + else: + small_factor = 1 / (10 ** (max(10, precision + 5))) + return int((n + small_factor) * (10 ** precision)) / (10 ** precision) def Show(self): import matplotlib.pyplot as plt @@ -1372,7 +1900,7 @@ def Show(self): def data_clean(self): try: - data = json.loads(self.Join().decode('utf-8'))['Snapshorts'] + data = json.loads(self.Join().decode('utf-8')) except: return dic = {} @@ -1383,41 +1911,50 @@ def data_clean(self): dic['moneyUse'] = [] dic['unit'] = '' lastAssets = 0 - for i in data: - dic['timeStamp'].append(datetime.datetime.fromtimestamp(i[0]/1000).date()) + for i in data['Snapshots']: + if not i[1]: + continue assets = 0 moneyUse = 0 - if i[1]: - for ex in i[1]: - position = ex['Symbols'] - if position: - margin = 0 - profit = 0 - holdSpot = 0 - for code in position: - if 'Long' in position[code]: - long = position[code]['Long'] - margin += long['Margin'] - profit += long['Profit'] - if 'Short' in position[code]: - short = position[code]['Short'] - margin += short['Margin'] - profit += short['Profit'] - if 'Stocks' in position[code]: - holdSpot += (position[code]['Stocks'] + position[code]['FrozenStocks']) * position[code]['Last'] - - if ex['QuoteCurrency'] == 'CNY': - assets += ex['Balance'] + ex['FrozenBalance'] + profit + margin - moneyUse += margin / assets + for pos in range(0, len(i[1])): + item = i[1][pos] + acc = data['Task']['Exchanges'][pos] + position = item['Symbols'] + if position: + margin = 0 + profit = 0 + holdSpot = 0 + diffSpot = 0 + for code in position: + if 'Long' in position[code]: + long = position[code]['Long'] + margin += long['Margin'] + profit += long['Profit'] + if 'Short' in position[code]: + short = position[code]['Short'] + margin += short['Margin'] + profit += short['Profit'] + if 'Stocks' in position[code]: + holdSpot += (position[code]['Stocks'] + position[code]['FrozenStocks']) * position[code]['Last'] + diffSpot += (position[code]['Stocks'] + position[code]['FrozenStocks'] - acc['Stocks']) * position[code]['Last'] + + for asset in item['Assets']: + if item['QuoteCurrency'] == 'CNY': + assets += asset['Amount'] + asset['FrozenAmount'] + profit + margin dic['unit'] = '(CNY)' - elif 'Futures_' in ex['Id']: - assets += ex['Stocks'] + ex['FrozenStocks'] + profit + margin - moneyUse += margin / assets - dic['unit'] = '(BTC)' + elif 'Futures_' in item['Id']: + if item['QuoteCurrency'] == 'USDT': + assets += asset['Amount'] + asset['FrozenAmount'] + profit + margin + dic['unit'] = '(USDT)' + else: + assets += asset['Amount'] + asset['FrozenAmount'] + profit + margin + dic['unit'] = '(%s)' % (item["BaseCurrency"], ) else: - assets += ex['Balance'] + holdSpot - moneyUse += holdSpot / (holdSpot + ex['Balance']) + assets += asset['Amount'] + asset['FrozenAmount'] + holdSpot + margin = abs(diffSpot) dic['unit'] = '(USD)' + moneyUse += margin / assets if assets != 0 else 0 + dic['timeStamp'].append(datetime.datetime.fromtimestamp(i[0]/1000).date()) dic['assets'].append(assets) dic['moneyUse'].append(moneyUse) if lastAssets != 0: @@ -1496,46 +2033,14 @@ def Join(self, report=False): ret = json.loads(self._joinResult) pnl = [] index = [] - symbol = None - eid = None - for ele in ret['Snapshorts']: + margin_suffix = '' + for ele in ret['Snapshots']: acc = ele[1][0] - close = float('nan') - eid = acc['Id'] - balance = acc['Balance'] + acc['FrozenBalance'] - stocks = acc['Stocks'] + acc['FrozenStocks'] - commission = acc.get('Commission', 0) - symbols = acc['Symbols'] - if eid == 'Futures_CTP' or eid == 'Futures_LTS': - if symbols: - for s in symbols: - pos = acc['Symbols'][s] - for t in ['Long', 'Short']: - if t in pos: - balance += pos[t]['Margin'] + pos[t]['Profit'] - pnl.append([acc['Balance'] + acc['FrozenBalance'], commission, balance]) - elif 'Futures_' in eid: - if symbols: - for s in symbols: - pos = acc['Symbols'][s] - for t in ['Long', 'Short']: - if t in pos: - stocks += pos[t]['Margin'] + pos[t]['Profit'] - pnl.append([acc['Stocks'] + acc['FrozenStocks'], commission, stocks]) - else: - if symbol is None and symbols: - for s in acc['Symbols']: - symbol = s - break - if symbol is not None: - close = acc['Symbols'][symbol]['Last'] - pnl.append([close, balance, stocks, commission, balance+(stocks*close)]) + if not margin_suffix: + margin_suffix = '(%s)' % (acc['MarginCurrency'], ) + pnl.append([acc['PnL'], acc['Utilization']*100]) index.append(pd.Timestamp(ele[0], unit='ms', tz='Asia/Shanghai')) - columns=["close", "balance", "stocks", "fee", "net"] - if eid == 'Futures_CTP' or eid == 'Futures_LTS': - columns=["balance", "fee", "net"] - elif 'Futures_' in eid: - columns=["stocks", "fee", "net"] + columns=["PnL"+margin_suffix, "Utilization(%)"] return pd.DataFrame(pnl, index=index, columns=columns) class Backtest(): @@ -1629,52 +2134,14 @@ def close(self, *args): def shutdown(self, *args): pass -def get_bars(symbol, unit='1d', start=None, end=None, count=1000): - if hasattr(unit, 'endswith'): - if unit.endswith('d'): - unit = int(unit[:-1]) * 1440 - elif unit.endswith('h'): - unit = int(unit[:-1]) * 60 - elif unit.endswith('m'): - unit = int(unit[:-1]) - ts_to = int(time.time()) - if end is not None: - end = end.replace('/', '-') - ts_to = int(time.mktime(datetime.datetime.strptime(end, "%Y-%m-%d %H:%M:%S" if ' ' in end else "%Y-%m-%d").timetuple())) - if start is not None: - start = start.replace('/', '-') - ts_from = int(time.mktime(datetime.datetime.strptime(start, "%Y-%m-%d %H:%M:%S" if ' ' in start else "%Y-%m-%d").timetuple())) - if end is None: - ts_to = ts_from+(unit*100*(count+10)) - else: - if end is None: - ts_from = 0 - ts_to = 0 - else: - ts_from = ts_to-(unit*100*(count+10)) - params = {"symbol": symbol, "resolution": unit, "from": ts_from, "to": ts_to, "size": count} - data = json.loads(httpGet("http://"+ CLUSTER_IP + "/chart/history?"+urlencode(params), CLUSTER_DOMAIN)) - try: - import pandas as pd - from pandas.plotting import register_matplotlib_converters - register_matplotlib_converters() - except: - return data - index = [] - for ele in data: - index.append(pd.Timestamp(ele[0], unit='s', tz='Asia/Shanghai')) - ele.pop(0) - columns=["open", "high", "low", "close", "volume"] - if len(data) > 0 and len(data[0]) == 6: - columns.append("openInterest") - return pd.DataFrame(data, index=index, columns=columns) - if __name__ == '__main__': - uuid = os.getenv("BOTVS_TASK_UUID") + btUUID = os.getenv("BOTVS_TASK_UUID") session = None - if uuid == 'dummy': + if btUUID == 'dummy': session = gg['s'] else: session = DummySession() if session is not None: Backtest(__cfg__, session).Run() + + diff --git a/setup.py b/setup.py index 1cad35e..456af38 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from distutils.core import setup setup(name='fmz', - version='1.2', + version='1.5', description='FMZ local backtest', author='Zero', author_email='hi@fmz.com',