├── __init__.py ├── setup.py ├── README.md └── fmz.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name='fmz', 4 | version='1.5', 5 | description='FMZ local backtest', 6 | author='Zero', 7 | author_email='hi@fmz.com', 8 | url='https://www.fmz.com/', 9 | packages=[""] 10 | ) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # backtest_python 2 | 3 | FMZ backtest engine python package 4 | support python2 and python3, support Windows, Linux, Mac 5 | 6 | ## install 7 | 8 | ``` 9 | pip install https://github.com/fmzquant/backtest_python/archive/master.zip 10 | ``` 11 | 12 | OR 13 | 14 | ``` 15 | pip3 install https://github.com/fmzquant/backtest_python/archive/master.zip 16 | ``` 17 | 18 | ## simple example 19 | ```python 20 | '''backtest 21 | start: 2022-02-19 00:00:00 22 | end: 2022-03-22 12:00:00 23 | period: 15m 24 | exchanges: [{"eid":"Binance","currency":"BTC_USDT","balance":10000,"stocks":0}] 25 | ''' 26 | from fmz import * 27 | task = VCtx(__doc__) # initialize backtest engine from __doc__ 28 | print(exchange.GetAccount()) 29 | print(exchange.GetTicker()) 30 | print(task.Join(True)) # print backtest result 31 | task.Show() # or show backtest chart 32 | ``` 33 | 34 | The config string can be generated automatically by saving the backtest configuration in the strategy edit page. 35 | 36 | 配置字符串可以通过策略编辑界面里的保存回测配置来自动生成 37 | 38 | ![meta](https://www.fmz.com/upload/asset/aa67494fc6306759753385bf7634ee4cd437f3f2.png) 39 | 40 | ## api 41 | https://www.fmz.com/api 42 | 43 | -------------------------------------------------------------------------------- /fmz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import warnings 4 | warnings.filterwarnings("ignore", category=DeprecationWarning) 5 | 6 | import sys 7 | import os 8 | import platform 9 | import base64 10 | import time 11 | import datetime 12 | import json 13 | import tempfile 14 | import zlib 15 | import math 16 | import ctypes 17 | import traceback 18 | import socket 19 | import signal 20 | import copy 21 | import inspect 22 | import io 23 | import struct 24 | import threading 25 | import uuid 26 | try: 27 | import md5 28 | import urllib2 29 | except: 30 | import hashlib as md5 31 | import urllib.request as urllib2 32 | 33 | try: 34 | import ssl 35 | ssl._create_default_https_context = ssl._create_unverified_context 36 | except: 37 | pass 38 | 39 | try: 40 | from urllib import urlencode 41 | except: 42 | from urllib.parse import urlencode 43 | 44 | DATASERVER = os.getenv("DATASERVER", "http://q.fmz.com") 45 | 46 | isPython3 = sys.version_info[0] >= 3 47 | gg = globals() 48 | gg['NaN'] = None 49 | gg['null'] = None 50 | gg['true'] = True 51 | gg['false'] = False 52 | 53 | # https://hynek.me/articles/hasattr/ 54 | saved_hasattr = hasattr 55 | def hasattr(obj, method): 56 | try: 57 | return saved_hasattr(obj, method) 58 | except: 59 | return False 60 | 61 | if isPython3: 62 | gg['xrange'] = range 63 | string_types = str 64 | else: 65 | string_types = basestring 66 | 67 | def json_loads(s): 68 | if isPython3: 69 | return json.loads(s.decode('utf-8')) 70 | return json.loads(s) 71 | 72 | def b2s(s): 73 | if isPython3: 74 | return s.decode('utf-8') 75 | return s 76 | 77 | def safe_str(s): 78 | if isPython3: 79 | return s.encode('utf-8') 80 | return str(s) 81 | 82 | 83 | BT_Status = 1 << 0 84 | BT_Symbols = 1 << 1 85 | BT_Indicators = 1 << 2 86 | BT_Chart = 1 << 3 87 | BT_ProfitLogs = 1 << 4 88 | BT_RuntimeLogs = 1 << 5 89 | BT_CloseProfitLogs = 1 << 6 90 | BT_Accounts = 1 << 7 91 | BT_Accounts_PnL = 1 << 8 92 | 93 | def getCacheDir(): 94 | tmpCache = tempfile.gettempdir()+ '/cache' 95 | if not os.path.exists(tmpCache): 96 | try: 97 | os.mkdir(tmpCache) 98 | except: 99 | pass 100 | return tmpCache 101 | 102 | def httpGet(url): 103 | try: 104 | req = urllib2.Request(url) 105 | req.add_header('Accept-Encoding', 'gzip, deflate') 106 | resp = urllib2.urlopen(req) 107 | data = resp.read() 108 | if resp.info().get('Content-Encoding') == 'gzip': 109 | data = zlib.decompress(data, 16+zlib.MAX_WBITS) 110 | return data 111 | except urllib2.HTTPError as e: 112 | headers = dict(e.info()) 113 | error_body = e.read() 114 | if e.info().get('Content-Encoding') == 'gzip': 115 | try: 116 | error_body = zlib.decompress(error_body, 16+zlib.MAX_WBITS) 117 | except: 118 | pass 119 | print("urllib2.HTTPError - Code: %s, URL: %s, Headers: %s, Body: %s" % (e.code, e.url, headers, error_body)) 120 | raise 121 | 122 | class Std: 123 | @staticmethod 124 | def _skip(arr, period): 125 | k = 0 126 | j = 0 127 | for j in xrange(0, len(arr)): 128 | if arr[j] is not None: 129 | k+=1 130 | if k == period: 131 | break 132 | return j 133 | 134 | @staticmethod 135 | def _sum(arr, num): 136 | s = 0.0 137 | for i in xrange(0, num): 138 | if arr[i] is not None: 139 | s += arr[i] 140 | return s 141 | 142 | @staticmethod 143 | def _avg(arr, num): 144 | if len(arr) == 0: 145 | return 0 146 | s = 0.0 147 | n = 0 148 | for i in xrange(0, min(len(arr), num)): 149 | if arr[i] is not None: 150 | s += arr[i] 151 | n += 1 152 | if n == 0: 153 | return 0 154 | return s / n 155 | 156 | @staticmethod 157 | def _zeros(n): 158 | return [0.0] * n 159 | 160 | @staticmethod 161 | def _set(arr, start, end, value): 162 | for i in xrange(start, min(len(arr), end)): 163 | arr[i] = value 164 | 165 | @staticmethod 166 | def _diff(a, b): 167 | d = [None] * len(b) 168 | for i in xrange(0, len(b)): 169 | if a[i] is not None and b[i] is not None: 170 | d[i] = a[i] - b[i] 171 | return d 172 | 173 | @staticmethod 174 | def _move_diff(a): 175 | d = [None] * (len(a)-1) 176 | for i in xrange(1, len(a)): 177 | d[i-1] = a[i] - a[i-1] 178 | return d 179 | 180 | @staticmethod 181 | def _cmp(arr, start, end, cmpFunc): 182 | v = arr[start] 183 | for i in xrange(start, end): 184 | v = cmpFunc(arr[i], v) 185 | return v 186 | 187 | @staticmethod 188 | def _filt(records, n, attr, iv, cmpFunc): 189 | if len(records) < 2: 190 | return None 191 | v = iv 192 | pos = 0 193 | if n != 0: 194 | pos = len(records) - min(len(records)-1, n) - 1 195 | for i in xrange(len(records)-2, pos-1, -1): 196 | if records[i] is not None: 197 | if attr is not None: 198 | v = cmpFunc(v, records[i][attr]) 199 | else: 200 | v = cmpFunc(v, records[i]) 201 | return v 202 | 203 | @staticmethod 204 | def _ticks(records): 205 | if len(records) == 0: 206 | return [] 207 | if isinstance(records[0], int) or isinstance(records[0], float): 208 | return records 209 | 210 | ticks = [None] * len(records) 211 | for i in xrange(0, len(records)): 212 | ticks[i] = records[i]['Close'] 213 | return ticks 214 | 215 | @staticmethod 216 | def _sma(S, period): 217 | R = Std._zeros(len(S)) 218 | j = Std._skip(S, period) 219 | Std._set(R, 0, j, None) 220 | if j < len(S): 221 | s = 0 222 | for i in xrange(j, len(S)): 223 | if i == j: 224 | s = Std._sum(S, i+1) 225 | else: 226 | s += S[i] - S[i-period] 227 | R[i] = s / period 228 | return R 229 | 230 | @staticmethod 231 | def _smma(S, period): 232 | R = Std._zeros(len(S)) 233 | j = Std._skip(S, period) 234 | Std._set(R, 0, j, None) 235 | if j < len(S): 236 | R[j] = Std._avg(S, j+1) 237 | for i in xrange(j+1, len(S)): 238 | R[i] = (R[i-1] * (period-1) + S[i]) / period 239 | return R 240 | 241 | @staticmethod 242 | def _ema(S, period): 243 | R = Std._zeros(len(S)) 244 | multiplier = 2.0 / (period + 1) 245 | j = Std._skip(S, period) 246 | Std._set(R, 0, j, None) 247 | if j < len(S): 248 | R[j] = Std._avg(S, j+1) 249 | for i in xrange(j+1, len(S)): 250 | R[i] = ((S[i] - R[i-1] ) * multiplier) + R[i-1] 251 | return R 252 | 253 | class TAInstance: 254 | def __init__(self, logPtr = None): 255 | self._logPtr = logPtr 256 | 257 | def _log(self, name, *args): 258 | if self._logPtr is not None: 259 | self._logPtr(name, safe_str(','.join(map(str, args)))) 260 | 261 | @staticmethod 262 | def Highest(records, n, attr=None): 263 | return Std._filt(records, n, attr, 5e-324, max) 264 | 265 | @staticmethod 266 | def Lowest(records, n, attr=None): 267 | return Std._filt(records, n, attr, 1.7976931348623157e+308, min) 268 | 269 | def MA(self, records, period=9): 270 | self._log('MA', period) 271 | 272 | return Std._sma(Std._ticks(records), period) 273 | 274 | def SMA(self, records, period=9): 275 | self._log('SMA', period) 276 | 277 | return Std._sma(Std._ticks(records), period) 278 | 279 | def EMA(self, records, period=9): 280 | self._log('EMA', period) 281 | 282 | return Std._ema(Std._ticks(records), period) 283 | 284 | def MACD(self, records, fastEMA=12, slowEMA=26, signalEMA=9): 285 | self._log('MACD', fastEMA, slowEMA, signalEMA) 286 | 287 | ticks = Std._ticks(records) 288 | slow = Std._ema(ticks, slowEMA) 289 | fast = Std._ema(ticks, fastEMA) 290 | # DIF 291 | dif = Std._diff(fast, slow) 292 | # DEA 293 | sig = Std._ema(dif, signalEMA) 294 | histogram = Std._diff(dif, sig) 295 | return [ dif, sig, histogram] 296 | 297 | def BOLL(self, records, period=20, multiplier=2): 298 | self._log('BOLL', period, multiplier) 299 | 300 | S = Std._ticks(records) 301 | j = period - 1 302 | while j < len(S) and (S[j] is None): 303 | j+=1 304 | UP = Std._zeros(len(S)) 305 | MB = Std._zeros(len(S)) 306 | DN = Std._zeros(len(S)) 307 | Std._set(UP, 0, j, None) 308 | Std._set(MB, 0, j, None) 309 | Std._set(DN, 0, j, None) 310 | n = 0.0 311 | for i in xrange(j, len(S)): 312 | if i == j: 313 | for k in xrange(0, period): 314 | n += S[k] 315 | else: 316 | n = n + S[i] - S[i - period] 317 | ma = n / period 318 | d = 0 319 | for k in xrange(i+1-period, i+1): 320 | d += (S[k] - ma) * (S[k] - ma) 321 | stdev = math.sqrt(d / period) 322 | up = ma + (multiplier * stdev) 323 | dn = ma - (multiplier * stdev) 324 | UP[i] = up 325 | MB[i] = ma 326 | DN[i] = dn 327 | return [UP, MB, DN] 328 | 329 | def KDJ(self, records, n=9, k=3, d=3): 330 | self._log('KDJ', n, k, d) 331 | 332 | RSV = Std._zeros(len(records)) 333 | Std._set(RSV, 0, n - 1, None) 334 | K = Std._zeros(len(records)) 335 | D = Std._zeros(len(records)) 336 | J = Std._zeros(len(records)) 337 | 338 | hs = Std._zeros(len(records)) 339 | ls = Std._zeros(len(records)) 340 | for i in xrange(0, len(records)): 341 | hs[i] = records[i]['High'] 342 | ls[i] = records[i]['Low'] 343 | 344 | for i in xrange(0, len(records)): 345 | if i >= (n - 1): 346 | c = records[i]['Close'] 347 | h = Std._cmp(hs, i - (n - 1), i + 1, max) 348 | l = Std._cmp(ls, i - (n - 1), i + 1, min) 349 | RSV[i] = (100 * ((c - l) / (h - l))) if h != l else 100 350 | K[i] = float(1 * RSV[i] + (k - 1) * K[i - 1]) / k 351 | D[i] = float(1 * K[i] + (d - 1) * D[i - 1]) / d 352 | else: 353 | K[i] = D[i] = 50.0 354 | RSV[i] = 0.0 355 | J[i] = 3 * K[i] - 2 * D[i] 356 | # remove prefix 357 | for i in xrange(0, n-1): 358 | K[i] = D[i] = J[i] = None 359 | return [K, D, J] 360 | 361 | def RSI(self, records, period=14): 362 | self._log('RSI', period) 363 | 364 | n = period 365 | rsi = Std._zeros(len(records)) 366 | Std._set(rsi, 0, len(rsi), None) 367 | if len(records) < n: 368 | return rsi 369 | 370 | ticks = Std._ticks(records) 371 | deltas = Std._move_diff(ticks) 372 | seed = deltas[:n] 373 | up = 0.0 374 | down = 0.0 375 | for i in xrange(0, len(seed)): 376 | if seed[i] >= 0: 377 | up += seed[i] 378 | else: 379 | down += seed[i] 380 | up /= n 381 | down /= n 382 | down = -down 383 | if down != 0: 384 | rs = up / down 385 | else: 386 | rs = 0 387 | rsi[n] = 100 - 100 / (1 + rs) 388 | delta = 0.0 389 | upval = 0.0 390 | downval = 0.0 391 | for i in xrange(n+1, len(ticks)): 392 | delta = deltas[i - 1] 393 | if delta > 0: 394 | upval = delta 395 | downval = 0.0 396 | else: 397 | upval = 0.0 398 | downval = -delta 399 | up = (up * (n - 1) + upval) / n 400 | down = (down * (n - 1) + downval) / n 401 | rs = 0 if down == 0 else (up / down) 402 | rsi[i] = 100 - 100 / (1 + rs) 403 | return rsi 404 | 405 | def OBV(self, records): 406 | self._log('OBV') 407 | 408 | if len(records) == 0: 409 | return [] 410 | 411 | if 'Close' not in records[0]: 412 | raise "self.OBV argument must KLine" 413 | 414 | R = Std._zeros(len(records)) 415 | for i in xrange(0, len(records)): 416 | if i == 0: 417 | R[i] = records[i]['Volume'] 418 | elif records[i]['Close'] >= records[i - 1]['Close']: 419 | R[i] = R[i - 1] + records[i]['Volume'] 420 | else: 421 | R[i] = R[i - 1] - records[i]['Volume'] 422 | return R 423 | 424 | def ATR(self, records, period=14): 425 | self._log('ATR', period) 426 | 427 | if len(records) == 0: 428 | return [] 429 | if 'Close' not in records[0]: 430 | raise "self.ATR argument must KLine" 431 | 432 | R = Std._zeros(len(records)) 433 | m = 0.0 434 | n = 0.0 435 | for i in xrange(0, len(records)): 436 | TR = 0 437 | if i == 0: 438 | TR = records[i]['High'] - records[i]['Low'] 439 | else: 440 | TR = max(records[i]['High'] - records[i]['Low'], abs(records[i]['High'] - records[i - 1]['Close']), abs(records[i - 1]['Close'] - records[i]['Low'])) 441 | m += TR 442 | if i < period: 443 | n = m / (i + 1) 444 | else: 445 | n = (((period - 1) * n) + TR) / period 446 | R[i] = n 447 | return R 448 | 449 | def Alligator(self, records, jawLength=13, teethLength=8, lipsLength=5): 450 | self._log('Alligator', jawLength, teethLength, lipsLength) 451 | 452 | ticks = [] 453 | for i in xrange(0, len(records)): 454 | ticks.append((records[i]['High'] + records[i]['Low']) / 2) 455 | return [ 456 | [None]*8+Std._smma(ticks, jawLength), # // jaw (blue) 457 | [None]*5+Std._smma(ticks, teethLength), # teeth (red) 458 | [None]*3+Std._smma(ticks, lipsLength) # lips (green) 459 | ] 460 | 461 | def CMF(self, records, period=20): 462 | self._log('CMF', period) 463 | 464 | ret = [] 465 | sumD = 0.0 466 | sumV = 0.0 467 | arrD = [] 468 | arrV = [] 469 | for i in xrange(0, len(records)): 470 | d = 0.0 471 | if records[i]['High'] != records[i]['Low']: 472 | d = (2 * records[i]['Close'] - records[i]['Low'] - records[i]['High']) / (records[i]['High'] - records[i]['Low']) * records[i]['Volume'] 473 | arrD.append(d) 474 | arrV.append(records[i]['Volume']) 475 | sumD += d 476 | sumV += records[i]['Volume'] 477 | if i >= period: 478 | sumD -= arrD.pop(0) 479 | sumV -= arrV.pop(0) 480 | ret.append(sumD / sumV) 481 | return ret 482 | # end of TA 483 | 484 | class DummyModule: 485 | def __init__(self, name): 486 | self.__name = name 487 | sys.modules['talib'] = self 488 | def __getattr__(self, attr): 489 | if attr == '__file__': 490 | return 'talib.py' 491 | raise Exception('Please install %s module for python (%s)' % (self.__name, attr)) 492 | 493 | class MyList(list): 494 | def __init__(self, data): 495 | super(MyList, self).__init__(data) 496 | self.__data = data 497 | def __getattr__(self, attr): 498 | if attr.startswith('_'): 499 | raise AttributeError 500 | ret = [] 501 | for item in self.__data: 502 | ret.append(item[attr]) 503 | if HasTALib: 504 | ret = numpy.array(ret) 505 | setattr(self, attr, ret) 506 | return ret 507 | 508 | HasTALib = False 509 | try: 510 | import talib 511 | import numpy 512 | HasTALib = True 513 | except ImportError: 514 | talib = DummyModule('talib') 515 | 516 | API_ERR_SUCCESS = 0 517 | API_ERR_FAILED = -1 518 | API_ERR_EOF = -2 519 | 520 | class dic2obj(dict): 521 | def __getattr__(self, name): 522 | if name in self: 523 | return self[name] 524 | else: 525 | raise AttributeError("no attribute '%s'" % name) 526 | 527 | def __setattr__(self, name, value): 528 | self[name] = value 529 | 530 | def __delattr__(self, name): 531 | if name in self: 532 | del self[name] 533 | else: 534 | raise AttributeError("no attribute '%s'" % name) 535 | 536 | def JoinArgs(args): 537 | arr = [] 538 | for item in args: 539 | if hasattr(item, 'savefig') and callable(item.savefig): 540 | d = io.BytesIO() 541 | item.savefig(d, format="png") 542 | arr.append('`data:image/png;base64,%s`'%(base64.b64encode(d.getvalue()).decode('utf-8'))) 543 | elif isinstance(item, dict) and item.get('type') == 'table' and item.get('cols'): 544 | arr.append('`%s`' % json.dumps(item)) 545 | else: 546 | arr.append(str(item)) 547 | return safe_str(' '.join(arr)) 548 | 549 | class _CSTRUCT(ctypes.Structure): 550 | def toObj(self): 551 | obj = {} 552 | for k, t in self._fields_: 553 | if k[0].isupper(): 554 | v = getattr(self, k) 555 | if k == 'Info' and hasattr(v, 's_js'): 556 | if v.s_js_size > 0: 557 | v = json.loads(v.s_js[:v.s_js_size]) 558 | else: 559 | v = None 560 | elif k == 'Condition' and hasattr(v, 'ConditionType'): 561 | if v.ConditionType == -1: 562 | continue 563 | else: 564 | v = v.toObj() 565 | elif k == 'ContractType' and len(v) == 0: 566 | continue 567 | if isinstance(v, bytes): 568 | v = v.decode() 569 | obj[k] = v 570 | return dic2obj(obj) 571 | 572 | class _INFO(_CSTRUCT): 573 | _fields_ = [("s_js", ctypes.c_char_p), 574 | ("s_js_size", ctypes.c_uint)] 575 | 576 | class _TICKER(_CSTRUCT): 577 | _fields_ = [("Time", ctypes.c_ulonglong), 578 | ("Symbol", ctypes.c_char * 31), 579 | ("Open", ctypes.c_double), 580 | ("High", ctypes.c_double), 581 | ("Low", ctypes.c_double), 582 | ("Sell", ctypes.c_double), 583 | ("Buy", ctypes.c_double), 584 | ("Last", ctypes.c_double), 585 | ("Volume", ctypes.c_double), 586 | ("OpenInterest", ctypes.c_double), 587 | ("Info", _INFO)] 588 | 589 | class _FUNDING(_CSTRUCT): 590 | _fields_ = [("Time", ctypes.c_ulonglong), 591 | ("Rate", ctypes.c_double), 592 | ("Interval", ctypes.c_uint), 593 | ("Symbol", ctypes.c_char * 31)] 594 | 595 | class _RECORD(_CSTRUCT): 596 | _fields_ = [("Time", ctypes.c_ulonglong), 597 | ("Open", ctypes.c_double), 598 | ("High", ctypes.c_double), 599 | ("Low", ctypes.c_double), 600 | ("Close", ctypes.c_double), 601 | ("Volume", ctypes.c_double), 602 | ("OpenInterest", ctypes.c_double)] 603 | 604 | class _MARKET_ORDER(_CSTRUCT): 605 | _fields_ = [("Price", ctypes.c_double), ("Amount", ctypes.c_double)] 606 | 607 | class _ACCOUNT(_CSTRUCT): 608 | _fields_ = [("Balance", ctypes.c_double), 609 | ("FrozenBalance", ctypes.c_double), 610 | ("Stocks", ctypes.c_double), 611 | ("FrozenStocks", ctypes.c_double), 612 | ("Equity", ctypes.c_double), 613 | ("UPnL", ctypes.c_double)] 614 | 615 | class _ASSET(_CSTRUCT): 616 | _fields_ = [("Currency", ctypes.c_char * 31), 617 | ("Amount", ctypes.c_double), 618 | ("FrozenAmount", ctypes.c_double)] 619 | 620 | class _ORDER_CONDITION(_CSTRUCT): 621 | _fields_ = [ 622 | ("ConditionType", ctypes.c_int), 623 | ("TpTriggerPrice", ctypes.c_double), 624 | ("TpOrderPrice", ctypes.c_double), 625 | ("SlTriggerPrice", ctypes.c_double), 626 | ("SlOrderPrice", ctypes.c_double), 627 | ] 628 | 629 | class _ORDER(_CSTRUCT): 630 | _fields_ = [("Id", ctypes.c_ulonglong), 631 | ("Time", ctypes.c_ulonglong), 632 | ("Price", ctypes.c_double), 633 | ("Amount", ctypes.c_double), 634 | ("DealAmount", ctypes.c_double), 635 | ("AvgPrice", ctypes.c_double), 636 | ("Type", ctypes.c_uint), 637 | ("Offset", ctypes.c_uint), 638 | ("Status", ctypes.c_uint), 639 | ("Symbol", ctypes.c_char * 31), 640 | ("ContractType", ctypes.c_char * 31), 641 | ("Condition", _ORDER_CONDITION)] 642 | 643 | class _TRADE(_CSTRUCT): 644 | _fields_ = [("Id", ctypes.c_ulonglong), 645 | ("Time", ctypes.c_ulonglong), 646 | ("Price", ctypes.c_double), 647 | ("Amount", ctypes.c_double), 648 | ("Type", ctypes.c_uint)] 649 | 650 | class _POSITION(_CSTRUCT): 651 | _fields_ = [("MarginLevel", ctypes.c_double), 652 | ("Amount", ctypes.c_double), 653 | ("FrozenAmount", ctypes.c_double), 654 | ("Price", ctypes.c_double), 655 | ("Profit", ctypes.c_double), 656 | ("Margin", ctypes.c_double), 657 | ("Type", ctypes.c_uint), 658 | ("Symbol", ctypes.c_char * 31), 659 | ("ContractType", ctypes.c_char * 31)] 660 | def EOF(): 661 | raise EOFError() 662 | 663 | class AsyncRet: 664 | routineId = 0 665 | def __init__(self, v): 666 | self.isWait = False 667 | self.v = v 668 | AsyncRet.routineId += 1 669 | 670 | def wait(self, timeout=0): 671 | if self.isWait: 672 | return (None, False) 673 | self.isWait = True 674 | return (self.v, True) 675 | 676 | def __repr__(self): 677 | return '' % AsyncRet.routineId 678 | 679 | class Exchange: 680 | def __init__(self, lib, ctx, idx, opt, cfg): 681 | self.lib = lib 682 | self.ctx = ctx 683 | self.idx = ctypes.c_int(idx) 684 | self.opt = opt 685 | self.cfg = cfg 686 | self.name = cfg["Id"] 687 | self.label = cfg["Label"] 688 | self.currency = '%s_%s' % (cfg["BaseCurrency"], cfg["QuoteCurrency"]) 689 | self.baseCurrency = cfg["BaseCurrency"] 690 | self.quoteCurrency = cfg["QuoteCurrency"] 691 | self.maxBarLen = cfg.get('MaxBarLen', 1000) 692 | self.period = opt['Period'] 693 | self.ct = '' 694 | self.records_cache = {} 695 | 696 | def Go(self, method, *args): 697 | return AsyncRet(getattr(self, method)(*args)) 698 | 699 | def GetName(self): 700 | return self.name 701 | 702 | def GetLabel(self): 703 | return self.label 704 | 705 | def GetCurrency(self): 706 | return self.currency 707 | 708 | def GetBaseCurrency(self): 709 | return self.baseCurrency 710 | 711 | def GetQuoteCurrency(self): 712 | return self.quoteCurrency 713 | 714 | def GetUSDCNY(self): 715 | self.lib.api_Exchange_GetUSDCNY.restype = ctypes.c_double 716 | return self.lib.api_Exchange_GetUSDCNY(self.ctx, self.idx) 717 | 718 | def SetMaxBarLen(self, n): 719 | self.maxBarLen = n 720 | 721 | def SetPrecision(self, a, b): 722 | self.lib.api_Exchange_SetPrecision(self.ctx, self.idx, ctypes.c_double(a), ctypes.c_double(b)) 723 | 724 | def GetRate(self): 725 | self.lib.api_Exchange_GetRate.restype = ctypes.c_double 726 | return self.lib.api_Exchange_GetRate(self.ctx, self.idx) 727 | 728 | def SetProxy(self, s): 729 | pass 730 | 731 | def SetTimeout(self, ms): 732 | pass 733 | 734 | def SetBase(self, s): 735 | r = ctypes.c_char_p() 736 | self.lib.api_Exchange_SetBase(self.ctx, self.idx, safe_str(s), ctypes.byref(r)) 737 | detail = b2s(r.value) 738 | self.lib.api_free(r) 739 | return detail 740 | 741 | def GetBase(self): 742 | r = ctypes.c_char_p() 743 | self.lib.api_Exchange_GetBase(self.ctx, self.idx, ctypes.byref(r)) 744 | detail = b2s(r.value) 745 | self.lib.api_free(r) 746 | return detail 747 | 748 | def SetCurrency(self, s): 749 | arr = s.split('_') 750 | if len(arr) == 2: 751 | self.currency = s 752 | self.quoteCurrency = arr[1] 753 | return self.lib.api_Exchange_SetCurrency(self.ctx, self.idx, safe_str(s)) 754 | 755 | def SetRate(self, rate=1.0): 756 | self.lib.api_Exchange_SetRate.restype = ctypes.c_double 757 | return self.lib.api_Exchange_SetRate(self.ctx, self.idx, ctypes.c_double(rate)) 758 | 759 | def GetTrades(self, symbol=''): 760 | r_len = ctypes.c_uint(0) 761 | buf_ptr = ctypes.c_void_p() 762 | ret = self.lib.api_Exchange_GetTrades(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) 763 | 764 | if ret == API_ERR_SUCCESS: 765 | n = r_len.value 766 | eles = [] 767 | if n > 0: 768 | group_array = (_TRADE * n).from_address(buf_ptr.value) 769 | for i in range(0, n): 770 | eles.append(group_array[i].toObj()) 771 | self.lib.api_free(buf_ptr) 772 | return eles 773 | elif ret == API_ERR_FAILED: 774 | return None 775 | EOF() 776 | 777 | def SetData(self, name, data): 778 | if not isinstance(data, string_types): 779 | data = json.dumps(data) 780 | return self.lib.api_Exchange_SetData(self.ctx, self.idx, safe_str(name), safe_str(data)) 781 | 782 | def GetData(self, name, timeout=60000, offset=0): 783 | r = _TICKER() 784 | ret = self.lib.api_Exchange_GetData(self.ctx, self.idx, ctypes.byref(r), safe_str(name), int(timeout), int(offset)) 785 | if ret == API_ERR_SUCCESS: 786 | 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}) 787 | elif ret == API_ERR_FAILED: 788 | return None 789 | EOF() 790 | 791 | def GetTickers(self): 792 | r_len = ctypes.c_uint(0) 793 | buf_ptr = ctypes.c_void_p() 794 | ret = self.lib.api_Exchange_GetTickers(self.ctx, self.idx, ctypes.byref(r_len), ctypes.byref(buf_ptr)) 795 | 796 | if ret == API_ERR_SUCCESS: 797 | n = r_len.value 798 | eles = [] 799 | if n > 0: 800 | group_array = (_TICKER * n).from_address(buf_ptr.value) 801 | for i in range(0, n): 802 | eles.append(group_array[i].toObj()) 803 | self.lib.api_free(buf_ptr) 804 | return eles 805 | elif ret == API_ERR_FAILED: 806 | return None 807 | EOF() 808 | 809 | def GetMarkets(self): 810 | r = ctypes.c_char_p() 811 | self.lib.api_Exchange_GetMarkets(self.ctx, self.idx, ctypes.byref(r)) 812 | ret = json.loads(b2s(r.value)) 813 | self.lib.api_free(r) 814 | return ret 815 | 816 | 817 | def GetTicker(self, symbol=''): 818 | r = _TICKER() 819 | ret = self.lib.api_Exchange_GetTicker(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r)) 820 | if ret == API_ERR_SUCCESS: 821 | return r.toObj() 822 | elif ret == API_ERR_FAILED: 823 | return None 824 | EOF() 825 | 826 | def GetFundings(self, symbol=''): 827 | r_len = ctypes.c_uint(0) 828 | buf_ptr = ctypes.c_void_p() 829 | ret = self.lib.api_Exchange_GetFundings(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) 830 | 831 | if ret == API_ERR_SUCCESS: 832 | n = r_len.value 833 | eles = [] 834 | if n > 0: 835 | group_array = (_FUNDING * n).from_address(buf_ptr.value) 836 | for i in range(0, n): 837 | eles.append(group_array[i].toObj()) 838 | self.lib.api_free(buf_ptr) 839 | return eles 840 | elif ret == API_ERR_FAILED: 841 | return None 842 | EOF() 843 | 844 | def IO(self, k, v = 0): 845 | if k == 'currency': 846 | return self.SetCurrency(v) 847 | return self.lib.api_Exchange_IO(self.ctx, self.idx, safe_str(k), int(v)) 848 | 849 | def GetDepth(self, symbol=''): 850 | ask_len = ctypes.c_uint(0) 851 | bid_len = ctypes.c_uint(0) 852 | buf_ptr = ctypes.c_void_p() 853 | 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)) 854 | 855 | if ret == API_ERR_SUCCESS: 856 | n = ask_len.value + bid_len.value 857 | asks = [] 858 | bids = [] 859 | if n > 0: 860 | group_array = (_MARKET_ORDER * n).from_address(buf_ptr.value) 861 | for i in range(0, n): 862 | if i < ask_len.value: 863 | asks.append(group_array[i].toObj()) 864 | else: 865 | bids.append(group_array[i].toObj()) 866 | self.lib.api_free(buf_ptr) 867 | return dic2obj({'Asks': asks, 'Bids': bids}) 868 | elif ret == API_ERR_FAILED: 869 | return None 870 | EOF() 871 | 872 | def GetRecords(self, symbol='', period=-1, limit=0): 873 | if isinstance(symbol, int): 874 | limit = period 875 | period = symbol 876 | symbol = '' 877 | if period == -1: 878 | period = int(self.period/1000) 879 | r_len = ctypes.c_uint(0) 880 | buf_ptr = ctypes.c_void_p() 881 | 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)) 882 | 883 | if ret == API_ERR_SUCCESS: 884 | n = r_len.value 885 | eles = [] 886 | if n > 0: 887 | group_array = (_RECORD * n).from_address(buf_ptr.value) 888 | for i in range(0, n): 889 | eles.append(group_array[i].toObj()) 890 | self.lib.api_free(buf_ptr) 891 | k = '%s/%s/%s/%d' % (self.currency, symbol, self.ct, period) 892 | c = self.records_cache.get(k, None) 893 | if c is None or len(c) == 0: 894 | if len(eles) > self.maxBarLen: 895 | eles = eles[len(eles)-self.maxBarLen:] 896 | self.records_cache[k] = eles 897 | else: 898 | preTime = 0 if len(c) == 0 else c[-1]['Time'] 899 | for ele in eles: 900 | t = ele['Time'] 901 | if t == preTime: 902 | c[-1] = ele 903 | elif t > preTime: 904 | c.append(ele) 905 | if len(c) > self.maxBarLen: 906 | c.pop(0) 907 | preTime = t 908 | r = MyList(self.records_cache[k]) 909 | if limit > 0: 910 | r = r[-limit:] 911 | return r 912 | elif ret == API_ERR_FAILED: 913 | return None 914 | EOF() 915 | 916 | def GetAssets(self): 917 | r_len = ctypes.c_uint(0) 918 | buf_ptr = ctypes.c_void_p() 919 | ret = self.lib.api_Exchange_GetAssets(self.ctx, self.idx, ctypes.byref(r_len), ctypes.byref(buf_ptr)) 920 | 921 | if ret == API_ERR_SUCCESS: 922 | n = r_len.value 923 | eles = [] 924 | if n > 0: 925 | group_array = (_ASSET * n).from_address(buf_ptr.value) 926 | for i in range(0, n): 927 | eles.append(group_array[i].toObj()) 928 | self.lib.api_free(buf_ptr) 929 | return eles 930 | elif ret == API_ERR_FAILED: 931 | return None 932 | EOF() 933 | 934 | def GetAccount(self): 935 | r = _ACCOUNT() 936 | ret = self.lib.api_Exchange_GetAccount(self.ctx, self.idx, ctypes.byref(r)) 937 | if ret == API_ERR_SUCCESS: 938 | return r.toObj() 939 | elif ret == API_ERR_FAILED: 940 | return None 941 | EOF() 942 | 943 | def Buy(self, price, amount=None, *extra): 944 | ret = self.lib.api_Exchange_Trade(self.ctx, self.idx, 0, ctypes.c_double(price), ctypes.c_double(amount), JoinArgs(extra)) 945 | if ret > 0: 946 | return int(ret) 947 | elif ret == API_ERR_FAILED: 948 | return None 949 | EOF() 950 | 951 | def Sell(self, price, amount=None, *extra): 952 | ret = self.lib.api_Exchange_Trade(self.ctx, self.idx, 1, ctypes.c_double(price), ctypes.c_double(amount), JoinArgs(extra)) 953 | if ret > 0: 954 | return int(ret) 955 | elif ret == API_ERR_FAILED: 956 | return None 957 | EOF() 958 | 959 | def CreateOrder(self, symbol, side, price, amount=None, *extra): 960 | 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)) 961 | if ret > 0: 962 | return int(ret) 963 | elif ret == API_ERR_FAILED: 964 | return None 965 | EOF() 966 | 967 | def GetOrders(self, symbol=''): 968 | r_len = ctypes.c_uint(0) 969 | buf_ptr = ctypes.c_void_p() 970 | ret = self.lib.api_Exchange_GetOrders(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) 971 | 972 | if ret == API_ERR_SUCCESS: 973 | n = r_len.value 974 | eles = [] 975 | if n > 0: 976 | group_array = (_ORDER * n).from_address(buf_ptr.value) 977 | for i in range(0, n): 978 | eles.append(group_array[i].toObj()) 979 | self.lib.api_free(buf_ptr) 980 | return eles 981 | elif ret == API_ERR_FAILED: 982 | return None 983 | EOF() 984 | 985 | def GetHistoryOrders(self, symbol='', since=0, limit = 0): 986 | if isinstance(symbol, int): 987 | limit = since 988 | since = symbol 989 | symbol = '' 990 | r_len = ctypes.c_uint(0) 991 | buf_ptr = ctypes.c_void_p() 992 | 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)) 993 | 994 | if ret == API_ERR_SUCCESS: 995 | n = r_len.value 996 | eles = [] 997 | if n > 0: 998 | group_array = (_ORDER * n).from_address(buf_ptr.value) 999 | for i in range(0, n): 1000 | eles.append(group_array[i].toObj()) 1001 | self.lib.api_free(buf_ptr) 1002 | return eles 1003 | elif ret == API_ERR_FAILED: 1004 | return None 1005 | EOF() 1006 | 1007 | 1008 | def GetOrder(self, orderId): 1009 | r = _ORDER() 1010 | ret = self.lib.api_Exchange_GetOrder(self.ctx, self.idx, ctypes.c_int(orderId), ctypes.byref(r)) 1011 | if ret == API_ERR_SUCCESS: 1012 | return r.toObj() 1013 | elif ret == API_ERR_FAILED: 1014 | return None 1015 | EOF() 1016 | 1017 | def CancelOrder(self, orderId, *extra): 1018 | ret = self.lib.api_Exchange_CancelOrder(self.ctx, self.idx, ctypes.c_int(orderId), JoinArgs(extra)) 1019 | if ret == API_ERR_EOF: 1020 | EOF() 1021 | return ret == API_ERR_SUCCESS 1022 | 1023 | # condition orders 1024 | def CreateConditionOrder(self, symbol, side, amount, condition, *extra): 1025 | 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)) 1026 | if ret > 0: 1027 | return int(ret) 1028 | elif ret == API_ERR_FAILED: 1029 | return None 1030 | EOF() 1031 | 1032 | def GetConditionOrders(self, symbol=''): 1033 | r_len = ctypes.c_uint(0) 1034 | buf_ptr = ctypes.c_void_p() 1035 | ret = self.lib.api_Exchange_GetConditionOrders(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) 1036 | 1037 | if ret == API_ERR_SUCCESS: 1038 | n = r_len.value 1039 | eles = [] 1040 | if n > 0: 1041 | group_array = (_ORDER * n).from_address(buf_ptr.value) 1042 | for i in range(0, n): 1043 | eles.append(group_array[i].toObj()) 1044 | self.lib.api_free(buf_ptr) 1045 | return eles 1046 | elif ret == API_ERR_FAILED: 1047 | return None 1048 | EOF() 1049 | 1050 | def GetHistoryConditionOrders(self, symbol='', since=0, limit = 0): 1051 | if isinstance(symbol, int): 1052 | limit = since 1053 | since = symbol 1054 | symbol = '' 1055 | r_len = ctypes.c_uint(0) 1056 | buf_ptr = ctypes.c_void_p() 1057 | 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)) 1058 | 1059 | if ret == API_ERR_SUCCESS: 1060 | n = r_len.value 1061 | eles = [] 1062 | if n > 0: 1063 | group_array = (_ORDER * n).from_address(buf_ptr.value) 1064 | for i in range(0, n): 1065 | eles.append(group_array[i].toObj()) 1066 | self.lib.api_free(buf_ptr) 1067 | return eles 1068 | elif ret == API_ERR_FAILED: 1069 | return None 1070 | EOF() 1071 | 1072 | 1073 | def GetConditionOrder(self, orderId): 1074 | r = _ORDER() 1075 | ret = self.lib.api_Exchange_GetConditionOrder(self.ctx, self.idx, ctypes.c_int(orderId), ctypes.byref(r)) 1076 | if ret == API_ERR_SUCCESS: 1077 | return r.toObj() 1078 | elif ret == API_ERR_FAILED: 1079 | return None 1080 | EOF() 1081 | 1082 | def CancelConditionOrder(self, orderId, *extra): 1083 | ret = self.lib.api_Exchange_CancelConditionOrder(self.ctx, self.idx, ctypes.c_int(orderId), JoinArgs(extra)) 1084 | if ret == API_ERR_EOF: 1085 | EOF() 1086 | return ret == API_ERR_SUCCESS 1087 | 1088 | def Log(self, orderType, price, amount=0, *extra): 1089 | ret = self.lib.api_Exchange_Log(self.ctx, self.idx, ctypes.c_int(orderType), ctypes.c_double(price), ctypes.c_double(amount), JoinArgs(extra)) 1090 | if orderType == 2: 1091 | return bool(ret) 1092 | if ret > 0: 1093 | return int(ret) 1094 | 1095 | def GetContractType(self): 1096 | return self.ct 1097 | 1098 | def GetPeriod(self): 1099 | return int(self.period/1000) 1100 | 1101 | def SetContractType(self, symbol): 1102 | r = ctypes.c_char_p() 1103 | ret = self.lib.api_Exchange_SetContractType(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r)) 1104 | if ret == API_ERR_SUCCESS: 1105 | self.ct = symbol 1106 | if r: 1107 | detail = json_loads(r.value) 1108 | self.lib.api_free(r) 1109 | return detail 1110 | else: 1111 | return True 1112 | elif ret == API_ERR_FAILED: 1113 | return None 1114 | EOF() 1115 | 1116 | def SetMarginLevel(self, symbol, level=None): 1117 | if isinstance(symbol, int) or isinstance(symbol, float): 1118 | level, symbol = symbol, level 1119 | if not symbol: 1120 | symbol = '' 1121 | self.lib.api_Exchange_SetMarginLevel.restype = ctypes.c_bool 1122 | return self.lib.api_Exchange_SetMarginLevel(self.ctx, self.idx, safe_str(symbol), ctypes.c_int(level)) 1123 | 1124 | def SetDirection(self, direction): 1125 | self.lib.api_Exchange_SetMarginLevel.restype = ctypes.c_bool 1126 | return self.lib.api_Exchange_SetDirection(self.ctx, self.idx, safe_str(direction)) 1127 | 1128 | def GetPositions(self, symbol=''): 1129 | r_len = ctypes.c_uint(0) 1130 | buf_ptr = ctypes.c_void_p() 1131 | ret = self.lib.api_Exchange_GetPositions(self.ctx, self.idx, safe_str(symbol), ctypes.byref(r_len), ctypes.byref(buf_ptr)) 1132 | 1133 | if ret == API_ERR_SUCCESS: 1134 | n = r_len.value 1135 | eles = [] 1136 | if n > 0: 1137 | group_array = (_POSITION * n).from_address(buf_ptr.value) 1138 | for i in range(0, n): 1139 | eles.append(group_array[i].toObj()) 1140 | self.lib.api_free(buf_ptr) 1141 | return eles 1142 | elif ret == API_ERR_FAILED: 1143 | return None 1144 | EOF() 1145 | 1146 | def GetPosition(self): 1147 | return self.GetPositions() 1148 | 1149 | class Chart(object): 1150 | def __init__(self, lib, ctx, js): 1151 | self.lib = lib 1152 | self.ctx = ctx 1153 | self.lib.api_Chart_New(self.ctx, safe_str(json.dumps(js))) 1154 | 1155 | def update(self, js): 1156 | self.lib.api_Chart_New(self.ctx, safe_str(json.dumps(js))) 1157 | 1158 | def add(self, seriesIdx, d, replaceId=None): 1159 | obj = [seriesIdx, d] 1160 | if replaceId is not None: 1161 | obj.append(replaceId) 1162 | self.lib.api_Chart_Add(self.ctx, safe_str(json.dumps(obj))) 1163 | 1164 | def reset(self, keep=0): 1165 | self.lib.api_Chart_Reset(self.ctx, keep) 1166 | 1167 | class KLineChart(): 1168 | def __init__(self, lib, ctx, options={}): 1169 | options["__isCandle"] = True 1170 | self.chart = Chart(lib, ctx, options) 1171 | self.bar = None 1172 | self.overlay = options.get("overlay", False) 1173 | self.preTime = 0 1174 | self.runtime = { "plots": [], "signals": [], "titles": {}, "count": 0 } 1175 | 1176 | def trim(self, obj): 1177 | dst = {} 1178 | for k in obj: 1179 | if obj[k] is not None: 1180 | dst[k] = obj[k] 1181 | return dst 1182 | 1183 | def newPlot(self, obj): 1184 | shape = self.trim(obj) 1185 | if "overlay" not in shape: 1186 | shape["overlay"] = self.overlay 1187 | if shape["type"] != 'shape' and shape["type"] != 'bgcolor' and shape["type"] != 'barcolor': 1188 | if "title" not in shape or len(shape["title"]) == 0 or shape["title"] in self.runtime["titles"]: 1189 | shape["title"] = '<' + shape.get("title","plot") + '_' + str(self.runtime["count"]) + '>' 1190 | self.runtime["count"] += 1 1191 | if "title" in shape: 1192 | self.runtime["titles"][shape["title"]] = True 1193 | return shape 1194 | 1195 | 1196 | def begin(self, bar): 1197 | self.bar = bar 1198 | 1199 | def reset(self, remain=0): 1200 | self.chart.reset(remain) 1201 | self.preTime = 0 1202 | 1203 | def close(self): 1204 | if self.bar["Time"] < self.preTime: 1205 | return 1206 | 1207 | data = { 1208 | "timestamp": self.bar["Time"], 1209 | "open": self.bar["Open"], 1210 | "high": self.bar["High"], 1211 | "low": self.bar["Low"], 1212 | "close": self.bar["Close"], 1213 | "volume": self.bar.get("Volume", 0), 1214 | } 1215 | 1216 | for k in ["plots", "signals"]: 1217 | if len(self.runtime[k]) > 0: 1218 | if "runtime" not in data: 1219 | data["runtime"] = {} 1220 | data["runtime"][k] = self.runtime[k] 1221 | 1222 | if self.preTime == self.bar["Time"]: 1223 | self.chart.add(0, data, -1) 1224 | else: 1225 | self.chart.add(0, data) 1226 | 1227 | self.preTime = self.bar["Time"] 1228 | self.runtime["plots"] = [] 1229 | self.runtime["signals"] = [] 1230 | self.runtime["titles"] = {} 1231 | self.runtime["count"] = 0 1232 | 1233 | 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): 1234 | if series is None or self.bar["Time"] < self.preTime: 1235 | return 1236 | 1237 | self.runtime["plots"].append(self.newPlot({ 1238 | "series": series, 1239 | "overlay": overlay, 1240 | "title": title, 1241 | "join": join, 1242 | "color": color, 1243 | "histbase": histbase, 1244 | "type": style, 1245 | "lineWidth": linewidth, 1246 | "display": display, 1247 | "offset": offset 1248 | })) 1249 | return len(self.runtime["plots"]) - 1 1250 | 1251 | def barcolor(self, color, offset=None, editable=False, show_last=None, title=None, display="all"): 1252 | if display != "all" or self.bar["Time"] < self.preTime: 1253 | return 1254 | self.runtime["plots"].append(self.newPlot({ 1255 | "type": 'barcolor', 1256 | "title": title, 1257 | "color": color, 1258 | "offset": offset, 1259 | "showLast": show_last, 1260 | "display": display 1261 | })) 1262 | 1263 | def plotarrow(self, series, title=None, colorup="#00ff00", 1264 | colordown = "#ff0000", 1265 | offset = 0, 1266 | minheight = 5, 1267 | maxheight = 100, 1268 | editable = False, show_last=None, display = "all", overlay = None): 1269 | if display != "all" or self.bar["Time"] < self.preTime: 1270 | return 1271 | 1272 | self.runtime["plots"].append(self.newPlot({ 1273 | "series": series, 1274 | "title": title, 1275 | "colorup": colorup, 1276 | "colordown": colordown, 1277 | "offset": offset, 1278 | "minheight": minheight, 1279 | "maxheight": maxheight, 1280 | "showLast": show_last, 1281 | "type": "shape", 1282 | "style": "arrow", 1283 | "display": display, 1284 | "overlay": overlay 1285 | })) 1286 | 1287 | def hline(self, price, title = None, color = None, linestyle = "dashed", linewidth = None, editable = False, display = "all", overlay = None): 1288 | if display != "all" or self.bar["Time"] < self.preTime: 1289 | return 1290 | 1291 | self.runtime["plots"].append(self.newPlot({ 1292 | "title": title, 1293 | "price": price, 1294 | "overlay": overlay, 1295 | "color": color, 1296 | "type": 'hline', 1297 | "lineStyle": linestyle, 1298 | "lineWidth": linewidth, 1299 | "display": display 1300 | })) 1301 | return len(self.runtime["plots"]) - 1 1302 | 1303 | 1304 | def bgcolor(self, color, offset=None, editable=None, show_last=None, title=None, display = "all", overlay=None): 1305 | if display != "all" or self.bar["Time"] < self.preTime: 1306 | return 1307 | self.runtime["plots"].append(self.newPlot({ 1308 | "title": title, 1309 | "overlay": overlay, 1310 | "color": color, 1311 | "type": 'bgcolor', 1312 | "showLast": show_last, 1313 | "offset": offset 1314 | })) 1315 | 1316 | 1317 | 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): 1318 | if (location != "absolute" and series is None) or (location == "absolute" and series is None) or char is None or self.bar["Time"] < self.preTime: 1319 | return 1320 | 1321 | self.runtime["plots"].append(self.newPlot({ 1322 | "overlay": overlay, 1323 | "type": "shape", 1324 | "style": "char", 1325 | "char": char, 1326 | "series": series, 1327 | "location": location, 1328 | "color": color, 1329 | "offset": offset, 1330 | "size": size, 1331 | "text": text, 1332 | "textColor": textcolor 1333 | })) 1334 | 1335 | 1336 | 1337 | 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): 1338 | if (location != "absolute" and series is None) or (location == "absolute" and series is None) or self.bar["Time"] < self.preTime: 1339 | return 1340 | 1341 | self.runtime["plots"].append(self.newPlot({ 1342 | "type": "shape", 1343 | "overlay": overlay, 1344 | "title": title, 1345 | "size": size, 1346 | "style": style, 1347 | "series": series, 1348 | "location": location, 1349 | "color": color, 1350 | "offset": offset, 1351 | "text": text, 1352 | "textColor": textcolor 1353 | })) 1354 | 1355 | def plotcandle(self, open, high, low, close, title=None, color=None, wickcolor=None, editable=None, show_last=None, bordercolor=None, display="all", overlay=None): 1356 | if display != "all" or self.bar["Time"] < self.preTime: 1357 | return 1358 | 1359 | self.runtime["plots"].append(self.newPlot({ 1360 | "price": high, 1361 | "open": open, 1362 | "high": high, 1363 | "low": low, 1364 | "close": close, 1365 | "title": title, 1366 | "color": color, 1367 | "wickColor": wickcolor, 1368 | "showLast": show_last, 1369 | "borderColor": bordercolor, 1370 | "type": "candle", 1371 | "display": display, 1372 | "overlay": overlay, 1373 | })) 1374 | 1375 | def fill(self, plot1, plot2, color=None, title=None, editable=None, show_last=None, fillgaps=None, display="all"): 1376 | if self.bar["Time"] < self.preTime: 1377 | return 1378 | if plot1 >= 0 and plot2 >= 0 and plot1 < len(self.runtime["plots"]) and plot2 < len(self.runtime["plots"]) and display == "all": 1379 | dst = self.runtime["plots"][plot1] 1380 | if "fill" not in dst: 1381 | dst["fill"] = [] 1382 | dst["fill"].append(self.trim({ 1383 | "value": self.runtime["plots"][plot2]["series"], 1384 | "color": color, 1385 | "showLast": show_last 1386 | })) 1387 | 1388 | def signal(self, direction, price, qty, id=None): 1389 | if self.bar["Time"] < self.preTime: 1390 | return 1391 | task = { 1392 | "id": id or direction, 1393 | "avgPrice": price, 1394 | "qty": qty 1395 | } 1396 | if direction == "buy" or direction == "long": 1397 | task["direction"] = "long" 1398 | elif direction == "sell" or direction == "short": 1399 | task["direction"] = "short" 1400 | elif direction == "closesell" or direction == "closeshort": 1401 | task["direction"] = "close" 1402 | task["closeDirection"] = "short" 1403 | elif direction == "closebuy" or direction == "closelong": 1404 | task["direction"] = "close" 1405 | task["closeDirection"] = "long" 1406 | 1407 | if task["direction"] or task["closeDirection"]: 1408 | self.runtime["signals"].append(task) 1409 | 1410 | def periodToMs(s, default): 1411 | period = default 1412 | if len(s) < 2: 1413 | return period 1414 | tmp = int(s[:-1]) 1415 | c = s[-1] 1416 | if c == 'd': 1417 | period = tmp * 60000 * 60 * 24 1418 | elif c == 'm': 1419 | period = tmp * 60000 1420 | elif c == 'm': 1421 | period = tmp * 60000 1422 | return period 1423 | 1424 | def parseTask(s): 1425 | settings = s 1426 | dic = {} 1427 | for line in settings.split('\n'): 1428 | line = line.strip() 1429 | if ':' not in line: 1430 | continue 1431 | arr = line.split(':', 1) 1432 | if len(arr) == 2: 1433 | k = arr[0].strip() 1434 | v = arr[1].strip() 1435 | dic[k] = v 1436 | pnl = dic.get('pnl', 'true') 1437 | dataServer = dic.get('dataServer', DATASERVER) 1438 | period = periodToMs(dic.get('period', '1h'), 60000 * 60) 1439 | basePeriod = periodToMs(dic.get('basePeriod', ''), 0) 1440 | 1441 | exchanges = [] 1442 | for e in json.loads(dic.get('exchanges', '[]')): 1443 | arr = e['currency'].upper().split('_') 1444 | if len(arr) == 1: 1445 | arr.append('CNY' if 'CTP' in e['eid'] else 'USD') 1446 | if basePeriod == 0: 1447 | basePeriod = 60000 * 60 1448 | if period == 86400000: 1449 | basePeriod = 60000 * 60 1450 | elif period == 3600000: 1451 | basePeriod = 60000 * 30 1452 | elif period == 1800000: 1453 | basePeriod = 60000 * 15 1454 | elif period == 900000: 1455 | basePeriod = 60000 * 5 1456 | elif period == 300000: 1457 | basePeriod = 60000 1458 | feeDef = { 1459 | 'Huobi': [150, 200], 1460 | 'OKX': [150, 200], 1461 | 'Binance': [150, 200], 1462 | 'Futures_BitMEX': [8, 10], 1463 | 'Futures_OKX': [30, 30], 1464 | 'Futures_HuobiDM': [30, 30], 1465 | 'Futures_CTP': [25, 25], 1466 | 'Futures_XTP': [30, 130], 1467 | } 1468 | if e['eid'] == "Futures_CTP": 1469 | dataServer = "http://q.youquant.com" 1470 | 1471 | fee = e.get('fee') 1472 | if fee is None: 1473 | fee = feeDef.get(e['eid'], [2000, 2000]) 1474 | else: 1475 | fee = [int(fee[0]*10000), int(fee[1]*10000)] 1476 | 1477 | cfg = { 1478 | "Balance": e.get('balance', 10000.0), 1479 | "Stocks": e.get('stocks', 3.0), 1480 | "BaseCurrency": arr[0], 1481 | "QuoteCurrency": arr[1], 1482 | "BasePeriod": basePeriod, 1483 | "FeeDenominator": 6, 1484 | "FeeMaker": fee[0], 1485 | "FeeTaker": fee[1], 1486 | "FeeMin": e.get('feeMin', 0), 1487 | "Id": e['eid'], 1488 | "Label": e['eid'] 1489 | } 1490 | exchanges.append(cfg) 1491 | 1492 | options = { 1493 | "DataServer": dataServer, 1494 | "MaxChartLogs": 800, 1495 | "MaxProfitLogs": 800, 1496 | "MaxRuntimeLogs": 800, 1497 | "NetDelay": 200, 1498 | "Period": period, 1499 | "RetFlags": BT_Status | BT_Indicators | BT_Accounts | BT_Chart | BT_RuntimeLogs | BT_ProfitLogs, 1500 | "TimeBegin": int(time.mktime(datetime.datetime.strptime(dic.get('start', '2019-02-01 00:00:00'), "%Y-%m-%d %H:%M:%S").timetuple())), 1501 | "TimeEnd": int(time.mktime(datetime.datetime.strptime(dic.get('end', '2019-02-10 00:00:00'), "%Y-%m-%d %H:%M:%S").timetuple())), 1502 | "UpdatePeriod": 5000 1503 | } 1504 | snapshotPeriod = 86400 1505 | testRange = options['TimeEnd'] - options['TimeBegin'] 1506 | if testRange / 3600 <= 2: 1507 | snapshotPeriod = 60 1508 | elif testRange / 86400 <= 2: 1509 | snapshotPeriod = 300 1510 | elif testRange / 86400 < 30: 1511 | snapshotPeriod = 3600 1512 | options['SnapshotPeriod'] = snapshotPeriod * 1000 1513 | if pnl == 'true': 1514 | options['RetFlags'] |= BT_Accounts_PnL 1515 | return {'Exchanges': exchanges, 'Options': options} 1516 | 1517 | class Templates(): 1518 | pass 1519 | 1520 | class VCtx(object): 1521 | def __init__(self, task = None, autoRun=False, gApis = None, progressCallback=None): 1522 | self._joinResult = None 1523 | self.gs = threading.Lock() 1524 | if gApis is None: 1525 | if __name__ == "__main__": 1526 | gApis = globals() 1527 | else: 1528 | gApis = dict(inspect.getmembers(inspect.stack()[1][0]))["f_globals"] 1529 | 1530 | if task is None: 1531 | task = parseTask(gApis['__doc__']) 1532 | elif hasattr(task, 'upper'): 1533 | task = parseTask(task) 1534 | 1535 | if progressCallback is not None: 1536 | self.progressCallback = progressCallback 1537 | 1538 | self.httpGetPtr = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int))(self.httpGetCallback) 1539 | self.progessCallbackPtr = ctypes.CFUNCTYPE(None, ctypes.c_char_p)(self.progressCallback) 1540 | osName = platform.system() 1541 | archName = platform.architecture()[0] 1542 | if platform.processor() == "arm": 1543 | archName = 'arm64' if archName == '64bit' else 'arm' 1544 | self.os = '%s/%s' % (osName.lower(), archName) 1545 | soName = 'backtest_py_%s_%s_v2.so' % (osName.lower(), archName) 1546 | crcFile = 'md5_v2.json' 1547 | loader = os.path.join("./depends", soName) 1548 | if not os.path.exists(loader): 1549 | hdic = {} 1550 | tmpCache = getCacheDir() 1551 | js = os.path.join(tmpCache, crcFile) 1552 | if os.path.exists(js): 1553 | b = open(js, 'rb').read() 1554 | if os.getenv("BOTVS_TASK_UUID") is None or "f153d5c3992730ebf04d568845e66731" in str(b): 1555 | hdic = json_loads(b) 1556 | loader = os.path.join(tmpCache, soName) 1557 | update = False 1558 | if not os.path.exists(loader): 1559 | update = True 1560 | else: 1561 | old = md5.md5(open(loader, 'rb').read()).hexdigest() 1562 | if old != hdic.get(soName, None): 1563 | update = True 1564 | backup = os.path.join(tmpCache, old) 1565 | try: 1566 | os.rename(loader, backup) 1567 | os.remove(backup) 1568 | except: 1569 | pass 1570 | if update: 1571 | open(loader, 'wb').write(httpGet(task["Options"]["DataServer"] + "/dist/depends/" + soName)) 1572 | open(js, 'wb').write(httpGet(task["Options"]["DataServer"] + "/dist/depends/" + crcFile)) 1573 | else: 1574 | print("load from debug mode", loader) 1575 | lib = ctypes.CDLL(os.path.abspath(loader)) 1576 | lib.api_backtest.restype = ctypes.c_void_p 1577 | ctx = ctypes.c_void_p(lib.api_backtest(safe_str(json.dumps(task)), self.httpGetPtr, self.progessCallbackPtr, None)) 1578 | if not ctx: 1579 | raise 'Initialize backtest engine error' 1580 | self.ctx = ctx 1581 | self.lib = lib 1582 | self.cache = {} 1583 | self.kvdb = {} 1584 | self.cRetryDelay = 3000 1585 | # HOOK 1586 | exchanges = [] 1587 | i = 0 1588 | for ele in task["Exchanges"]: 1589 | exchanges.append(Exchange(lib, ctx, i, task["Options"], ele)) 1590 | i += 1 1591 | 1592 | for k in dir(self): 1593 | if k.startswith('g_'): 1594 | gApis[k[2:]] = getattr(self, k) 1595 | 1596 | self.realTime = time.time 1597 | time.time = self.g_PyTime 1598 | gApis['__name__'] = '__main__' 1599 | gApis["TA"] = TAInstance(self._logTA) 1600 | gApis['exchanges'] = exchanges 1601 | gApis['exchange'] = exchanges[0] 1602 | gApis['ext'] = Templates() 1603 | gApis['time'] = time 1604 | gApis['null'] = None 1605 | gApis['true'] = True 1606 | gApis['false'] = False 1607 | gApis["ORDER_STATE_PENDING"] = 0 1608 | gApis["ORDER_STATE_CLOSED"] = 1 1609 | gApis["ORDER_STATE_CANCELED"] = 2 1610 | gApis["ORDER_STATE_UNKNOWN"] = 3 1611 | gApis["ORDER_TYPE_BUY"] = 0 1612 | gApis["ORDER_TYPE_SELL"] = 1 1613 | gApis["ORDER_OFFSET_OPEN"] = 0 1614 | gApis["ORDER_OFFSET_CLOSE"] = 1 1615 | 1616 | gApis["ORDER_CONDITION_TYPE_OCO"] = 0 1617 | gApis["ORDER_CONDITION_TYPE_TP"] = 1 1618 | gApis["ORDER_CONDITION_TYPE_SL"] = 2 1619 | gApis["ORDER_CONDITION_TYPE_GENERIC"] = 3 1620 | 1621 | gApis["PD_LONG"] = 0 1622 | gApis["PD_SHORT"] = 1 1623 | gApis["PD_LONG_YD"] = 2 1624 | gApis["PD_SHORT_YD"] = 3 1625 | 1626 | gApis["LOG_TYPE_BUY"] = 0 1627 | gApis["LOG_TYPE_SELL"] = 1 1628 | gApis["LOG_TYPE_CANCEL"] = 2 1629 | gApis["LOG_TYPE_ERROR"] = 3 1630 | gApis["LOG_TYPE_PROFIT"] = 4 1631 | gApis["LOG_TYPE_LOG"] = 5 1632 | gApis["LOG_TYPE_RESTART"] = 6 1633 | 1634 | gApis["PERIOD_M1"] = 60 * 1 1635 | gApis["PERIOD_M3"] = 60 * 3 1636 | gApis["PERIOD_M5"] = 60 * 5 1637 | gApis["PERIOD_M15"] = 60 * 15 1638 | gApis["PERIOD_M30"] = 60 * 30 1639 | gApis["PERIOD_H1"] = 60 * 60 1640 | gApis["PERIOD_H2"] = 60 * 60 * 2 1641 | gApis["PERIOD_H4"] = 60 * 60 * 4 1642 | gApis["PERIOD_H6"] = 60 * 60 * 6 1643 | gApis["PERIOD_H12"] = 60 * 60 * 12 1644 | gApis["PERIOD_D1"] = 60 * 60 * 24 1645 | gApis["PERIOD_D3"] = 60 * 60 * 24 * 3 1646 | gApis["PERIOD_W1"] = 60 * 60 * 24 * 7 1647 | 1648 | if autoRun: 1649 | try: 1650 | gApis['main']() 1651 | except EOFError: 1652 | pass 1653 | self.Join() 1654 | 1655 | def httpGetCallback(self, path, ptr_buf, ptr_size, ptr_need_free): 1656 | url = path.decode('utf8') 1657 | tmpCache = getCacheDir() 1658 | cacheFile = tmpCache+'/botvs_kline_'+md5.md5(path).hexdigest() 1659 | data = None 1660 | try: 1661 | if os.path.exists(cacheFile): 1662 | data = open(cacheFile, 'rb').read() 1663 | cacheFile = None 1664 | else: 1665 | data = httpGet(url) 1666 | if len(data) > 0: 1667 | open(cacheFile, 'wb').write(data) 1668 | except: 1669 | pass 1670 | if data is None: 1671 | return 1 1672 | ptr_size.contents.value = len(data) 1673 | ptr_need_free.contents.value = 0 1674 | str_buf = ctypes.create_string_buffer(data) 1675 | ptr_buf.contents.value = ctypes.addressof(str_buf) 1676 | self.cache[cacheFile] = str_buf #prevent to release 1677 | return 0 1678 | 1679 | def progressCallback(self, st): 1680 | pass 1681 | 1682 | def _logTA(self, name, args): 1683 | self.lib.api_LogTA(self.ctx, name, args) 1684 | 1685 | def g_Unix(self): 1686 | self.lib.api_Unix.restype = ctypes.c_ulonglong 1687 | return self.lib.api_Unix(self.ctx) 1688 | 1689 | def g_UnixNano(self): 1690 | self.lib.api_UnixNano.restype = ctypes.c_ulonglong 1691 | return self.lib.api_UnixNano(self.ctx) 1692 | 1693 | def g_PyTime(self): 1694 | return float(self.g_UnixNano())/1e9 1695 | 1696 | def g_Sleep(self, n): 1697 | if self.lib.api_Sleep(self.ctx, ctypes.c_double(n)) != 0: 1698 | EOF() 1699 | 1700 | def g_EnableLog(self, flag = True): 1701 | self.lib.api_EnableLog(self.ctx, ctypes.c_bool(flag)) 1702 | 1703 | def g_Log(self, *extra): 1704 | self.lib.api_Log(self.ctx, JoinArgs(extra)) 1705 | 1706 | def g_LogReset(self, keep = 0): 1707 | self.lib.api_LogReset(self.ctx, ctypes.c_int(keep)) 1708 | 1709 | def g_LogVacuum(self): 1710 | pass 1711 | 1712 | def g_LogStatus(self, *extra): 1713 | self.lib.api_LogStatus(self.ctx, JoinArgs(extra)) 1714 | 1715 | def g_LogProfit(self, profit, *extra): 1716 | self.lib.api_LogProfit(self.ctx, ctypes.c_double(profit), JoinArgs(extra)) 1717 | 1718 | def g_LogProfitReset(self, keep = 0): 1719 | self.lib.api_LogProfitReset(self.ctx, ctypes.c_int(keep)) 1720 | 1721 | def g_LogError(self, *extra): 1722 | self.lib.api_LogError(self.ctx, JoinArgs(extra)) 1723 | 1724 | def g_Panic(self, *extra): 1725 | self.lib.api_LogError(self.ctx, JoinArgs(extra)) 1726 | EOF() 1727 | 1728 | def g_GetLastError(self): 1729 | return '' 1730 | 1731 | def g_MD5(self, s): 1732 | return md5.md5(safe_str(s)).hexdigest() 1733 | 1734 | def g_HttpQuery(self, *args): 1735 | return 'dummy' 1736 | 1737 | def g_HttpQuery_Go(self, *args): 1738 | return AsyncRet('dummy') 1739 | 1740 | def g_JSONParse(self, s): 1741 | return json.loads(s) 1742 | 1743 | def g_UUID(self): 1744 | return str(uuid.uuid4()) 1745 | 1746 | def g_StrDecode(self, s, c='gbk'): 1747 | self.g_LogError("sandbox not support StrDecode") 1748 | 1749 | def g_EnableLogLocal(self, b): 1750 | pass 1751 | 1752 | def g_Dial(self, *args): 1753 | self.g_LogError("sandbox not support Dial") 1754 | 1755 | def g_DBExec(self, *args): 1756 | self.g_LogError("sandbox not support DBExec") 1757 | 1758 | def g_Encode(self, *args): 1759 | self.g_LogError("sandbox not support Encode") 1760 | 1761 | def g_EventLoop(self, *args): 1762 | self.g_LogError("sandbox not support EventLoop") 1763 | 1764 | def g_Mail(self, *args): 1765 | return True 1766 | 1767 | def g_Mail_Go(self, *args): 1768 | return AsyncRet(True) 1769 | 1770 | def g_GetCommand(self): 1771 | return '' 1772 | 1773 | def g_GetMeta(self): 1774 | return None 1775 | 1776 | def g_SetErrorFilter(self, s): 1777 | pass 1778 | 1779 | def g_SetChannelData(self, s): 1780 | pass 1781 | 1782 | def g_GetChannelData(self, s): 1783 | return '' 1784 | 1785 | def g_GetOS(self): 1786 | return self.os 1787 | 1788 | def g_Version(self, detail=False): 1789 | self.lib.api_Version.restype = ctypes.c_char_p 1790 | return self.lib.api_Version(self.ctx, detail).decode('utf-8') 1791 | 1792 | def g_IsVirtual(self): 1793 | return True 1794 | 1795 | def g_Chart(self, js): 1796 | return Chart(self.lib, self.ctx, js) 1797 | 1798 | def g_KLineChart(self, js={}): 1799 | return KLineChart(self.lib, self.ctx, js) 1800 | 1801 | def g_GetPid(self): 1802 | return os.getpid() 1803 | 1804 | def g__Cross(self, arr1, arr2): 1805 | if len(arr1) != len(arr2): 1806 | raise Exception("cross array length not equal") 1807 | n = 0 1808 | for i in range(len(arr1)-1, -1, -1): 1809 | if arr1[i] is None or arr2[i] is None: 1810 | break 1811 | if arr1[i] < arr2[i]: 1812 | if n > 0: 1813 | break 1814 | n -= 1 1815 | elif arr1[i] > arr2[i]: 1816 | if n < 0: 1817 | break 1818 | n += 1 1819 | else: 1820 | break 1821 | return n 1822 | 1823 | def g__G(self, k='__wtf__', v='__wtf__'): 1824 | if k == '__wtf__': 1825 | return 1 1826 | elif k is None: 1827 | self.kvdb = {} 1828 | else: 1829 | k = k.lower() 1830 | if v is None: 1831 | if hasattr(self.kvdb, k): 1832 | delattr(self.kvdb, k) 1833 | elif v == '__wtf__': 1834 | return self.kvdb.get(k, None) 1835 | elif v is not None: 1836 | self.kvdb[k] = v 1837 | 1838 | def g__CDelay(self, d): 1839 | if d > 0: 1840 | self.cRetryDelay = d 1841 | 1842 | def g__C(self, pfn, *arg): 1843 | while True: 1844 | ret = pfn(*arg) 1845 | if ret == False or ret is None: 1846 | self.g_Sleep(self.cRetryDelay) 1847 | else: 1848 | return ret 1849 | def g__D(self, date=None, fmt=None): 1850 | if date is None: 1851 | date = self.g_Unix() 1852 | if fmt is None: 1853 | fmt = '%Y-%m-%d %H:%M:%S' 1854 | return time.strftime(fmt, time.localtime(date)) 1855 | 1856 | def g__T(self, a, b=None): 1857 | r = str(a) 1858 | if b is not None: 1859 | r = str(a) + '|' + str(b) 1860 | return '[trans]'+r+'[/trans]' 1861 | 1862 | def g__N(self, n, precision=4): 1863 | if precision < 0: 1864 | precision_factor = 10 ** -precision 1865 | return n - (n % precision_factor) 1866 | else: 1867 | small_factor = 1 / (10 ** (max(10, precision + 5))) 1868 | return int((n + small_factor) * (10 ** precision)) / (10 ** precision) 1869 | 1870 | def Show(self): 1871 | import matplotlib.pyplot as plt 1872 | from matplotlib import ticker 1873 | try: 1874 | from IPython import get_ipython 1875 | get_ipython().run_line_magic('matplotlib', 'inline') 1876 | from pandas.plotting import register_matplotlib_converters 1877 | register_matplotlib_converters() 1878 | except: 1879 | pass 1880 | 1881 | 1882 | def data_clean(self): 1883 | try: 1884 | data = json.loads(self.Join().decode('utf-8')) 1885 | except: 1886 | return 1887 | dic = {} 1888 | dic['timeStamp'] = [] 1889 | dic['assets'] = [] 1890 | dic['surplus'] = [] 1891 | dic['loss'] = [] 1892 | dic['moneyUse'] = [] 1893 | dic['unit'] = '' 1894 | lastAssets = 0 1895 | for i in data['Snapshots']: 1896 | if not i[1]: 1897 | continue 1898 | assets = 0 1899 | moneyUse = 0 1900 | for pos in range(0, len(i[1])): 1901 | item = i[1][pos] 1902 | acc = data['Task']['Exchanges'][pos] 1903 | position = item['Symbols'] 1904 | if position: 1905 | margin = 0 1906 | profit = 0 1907 | holdSpot = 0 1908 | diffSpot = 0 1909 | for code in position: 1910 | if 'Long' in position[code]: 1911 | long = position[code]['Long'] 1912 | margin += long['Margin'] 1913 | profit += long['Profit'] 1914 | if 'Short' in position[code]: 1915 | short = position[code]['Short'] 1916 | margin += short['Margin'] 1917 | profit += short['Profit'] 1918 | if 'Stocks' in position[code]: 1919 | holdSpot += (position[code]['Stocks'] + position[code]['FrozenStocks']) * position[code]['Last'] 1920 | diffSpot += (position[code]['Stocks'] + position[code]['FrozenStocks'] - acc['Stocks']) * position[code]['Last'] 1921 | 1922 | for asset in item['Assets']: 1923 | if item['QuoteCurrency'] == 'CNY': 1924 | assets += asset['Amount'] + asset['FrozenAmount'] + profit + margin 1925 | dic['unit'] = '(CNY)' 1926 | elif 'Futures_' in item['Id']: 1927 | if item['QuoteCurrency'] == 'USDT': 1928 | assets += asset['Amount'] + asset['FrozenAmount'] + profit + margin 1929 | dic['unit'] = '(USDT)' 1930 | else: 1931 | assets += asset['Amount'] + asset['FrozenAmount'] + profit + margin 1932 | dic['unit'] = '(%s)' % (item["BaseCurrency"], ) 1933 | else: 1934 | assets += asset['Amount'] + asset['FrozenAmount'] + holdSpot 1935 | margin = abs(diffSpot) 1936 | dic['unit'] = '(USD)' 1937 | moneyUse += margin / assets if assets != 0 else 0 1938 | dic['timeStamp'].append(datetime.datetime.fromtimestamp(i[0]/1000).date()) 1939 | dic['assets'].append(assets) 1940 | dic['moneyUse'].append(moneyUse) 1941 | if lastAssets != 0: 1942 | assetsDiff = assets - lastAssets 1943 | if assetsDiff > 0: 1944 | dic['surplus'].append(assetsDiff) 1945 | dic['loss'].append(0) 1946 | elif assetsDiff < 0: 1947 | dic['surplus'].append(0) 1948 | dic['loss'].append(assetsDiff) 1949 | else: 1950 | dic['surplus'].append(0) 1951 | dic['loss'].append(0) 1952 | else: 1953 | dic['surplus'].append(0) 1954 | dic['loss'].append(0) 1955 | lastAssets = assets 1956 | return dic 1957 | 1958 | # plt.rcParams['font.sans-serif']=['SimHei'] 1959 | plt.rcParams['axes.unicode_minus']=False 1960 | 1961 | # test 1962 | data = data_clean(self) 1963 | if data: 1964 | x = data['timeStamp'] 1965 | assets = data['assets'] 1966 | surplus = data['surplus'] 1967 | loss = data['loss'] 1968 | moneyUse = data['moneyUse'] 1969 | unit = data['unit'] 1970 | 1971 | plt.figure(figsize=(14, 8)) 1972 | plt.subplots_adjust(left=0.090, right=0.930) 1973 | plt.subplots_adjust(hspace=0, wspace=0) 1974 | ax = plt.subplot(311) 1975 | plt.title(u'Backtest', fontsize=18) 1976 | plt.grid(linestyle='--', color='#D9D9D9') 1977 | plt.plot(x, assets, color='#3A859E', label=u'Equity %s %f' % (unit, assets[-1])) 1978 | plt.fill_between(x, min(assets), assets, color='#D0DBE8', alpha=.5) 1979 | plt.legend(loc='upper left') 1980 | ax = plt.subplot(312) 1981 | plt.grid(linestyle='--', color='#D9D9D9') 1982 | plt.bar(x, surplus, color='r') 1983 | plt.bar(x, loss, color='g') 1984 | plt.legend(loc='upper left', labels=[u'Win' + unit, u'Loss' + unit]) 1985 | 1986 | ax = plt.subplot(313) 1987 | plt.grid(linestyle='--', color='#D9D9D9') 1988 | plt.plot(x, moneyUse, color='#EBB000', label='Utilization') 1989 | ax.yaxis.set_major_formatter(ticker.PercentFormatter(xmax=1, decimals=1)) 1990 | plt.fill_between(x, 0, moneyUse, color='#FFFBEB', alpha=.5) 1991 | plt.legend(loc='upper left') 1992 | # plt.get_current_fig_manager().full_screen_toggle() 1993 | plt.show() 1994 | else: 1995 | print('No data') 1996 | 1997 | def Join(self, report=False): 1998 | self.gs.acquire() 1999 | if self._joinResult is None: 2000 | self.lib.api_Join.restype = ctypes.c_char_p 2001 | r = self.lib.api_Join(self.ctx) 2002 | self.lib.api_Release(self.ctx) 2003 | self._joinResult = r 2004 | self.gs.release() 2005 | time.time = self.realTime 2006 | if not report: 2007 | return self._joinResult 2008 | import pandas as pd 2009 | try: 2010 | from pandas.plotting import register_matplotlib_converters 2011 | register_matplotlib_converters() 2012 | except: 2013 | pass 2014 | ret = json.loads(self._joinResult) 2015 | pnl = [] 2016 | index = [] 2017 | margin_suffix = '' 2018 | for ele in ret['Snapshots']: 2019 | acc = ele[1][0] 2020 | if not margin_suffix: 2021 | margin_suffix = '(%s)' % (acc['MarginCurrency'], ) 2022 | pnl.append([acc['PnL'], acc['Utilization']*100]) 2023 | index.append(pd.Timestamp(ele[0], unit='ms', tz='Asia/Shanghai')) 2024 | columns=["PnL"+margin_suffix, "Utilization(%)"] 2025 | return pd.DataFrame(pnl, index=index, columns=columns) 2026 | 2027 | class Backtest(): 2028 | def __init__(self, task, session): 2029 | self.session = session 2030 | self.task = task 2031 | self.gApis = {} 2032 | self.tpls = task['Code'] 2033 | del task['Code'] 2034 | self.ctx = VCtx(task = self.task, gApis = self.gApis, progressCallback = self.progressCallback) 2035 | 2036 | def progressCallback(self, st): 2037 | if self.session is None: 2038 | return 2039 | self.session.sendall(struct.pack('!II', json_loads(st)['TaskStatus'], len(st)) + st) 2040 | 2041 | def waitStop(self, ctx): 2042 | if self.session is None: 2043 | return 2044 | try: 2045 | buf = b'' 2046 | ack = 0 2047 | self.session.settimeout(None) 2048 | while True: 2049 | if ack > 0: 2050 | if len(buf) - 4 >= ack: 2051 | if buf[4:4+ack] == b'stop': 2052 | ctx.Join() 2053 | self.session.close() 2054 | os._exit(2) 2055 | break 2056 | elif len(buf) >= 4: 2057 | ack, = struct.unpack('!I', buf[:4]) 2058 | continue 2059 | buf += self.session.recv((ack - (len(buf) - 4)) if ack > 0 else 4) 2060 | except: 2061 | pass 2062 | 2063 | def exit_handler(self, signum, frame): 2064 | signal.signal(signal.SIGINT, signal.SIG_IGN) 2065 | self.ctx.Join() 2066 | self.session.shutdown(socket.SHUT_RDWR) 2067 | os._exit(0) 2068 | 2069 | def Run(self): 2070 | signal.signal(signal.SIGINT, self.exit_handler) 2071 | if self.session and platform.system() == 'Windows': 2072 | t = threading.Thread(target=self.waitStop, args=(self.ctx,)) 2073 | t.setDaemon(True) 2074 | t.start() 2075 | try: 2076 | initPlot = False 2077 | tplsLen = len(self.tpls) 2078 | for i in xrange(0, tplsLen): 2079 | tpl = self.tpls[i] 2080 | vv = copy.copy(self.gApis) 2081 | for pair in tpl[1]: 2082 | vv[pair[0]] = pair[1] 2083 | code = tpl[0]+"\n\nif 'init' in locals() and callable(init):\n init()\n" 2084 | if i == tplsLen - 1: 2085 | code += "\nmain()\nif 'onexit' in globals():\n onexit()" 2086 | if not initPlot and 'matplotlib' in code: 2087 | initPlot = True 2088 | try: 2089 | __import__('matplotlib').use('Agg') 2090 | except: 2091 | pass 2092 | exec(code.replace('\r\n', '\n'), vv) 2093 | except (EOFError, SystemExit): 2094 | pass 2095 | except: 2096 | etype, value, tb = sys.exc_info() 2097 | arr = [x for x in traceback.extract_tb(tb) if x[0] == ''] 2098 | if arr: 2099 | tList = ['Traceback (most recent call last):\n'] 2100 | tList = tList + traceback.format_list(arr) 2101 | else: 2102 | tList = [] 2103 | tList = tList + traceback.format_exception_only(etype, value) 2104 | self.ctx.g_LogError(''.join(tList)) 2105 | self.ctx.Join() 2106 | self.session.shutdown(socket.SHUT_RDWR) 2107 | 2108 | class DummySession(): 2109 | def send(self, *args): 2110 | pass 2111 | def sendall(self, *args): 2112 | pass 2113 | def close(self, *args): 2114 | pass 2115 | def shutdown(self, *args): 2116 | pass 2117 | 2118 | if __name__ == '__main__': 2119 | btUUID = os.getenv("BOTVS_TASK_UUID") 2120 | session = None 2121 | if btUUID == 'dummy': 2122 | session = gg['s'] 2123 | else: 2124 | session = DummySession() 2125 | if session is not None: 2126 | Backtest(__cfg__, session).Run() 2127 | 2128 | 2129 | --------------------------------------------------------------------------------