比特币是最近相当火爆的一个金融衍生品(瞧咱这口径)。比特币中国提供了一系列 API 来获取和操纵其市场内的比特币。我的小伙伴们基于其 API,完成了一套交易程序。为了提高操作的有效性和技术性,同时作为 python 学习需要,我也参与进来,仿造股票交易软件,为比特币中国绘制了一系列指标图,包括 MACD、BOLL、KDJ 等。截止上周,btc123 也开始提供了 MACD 指标图,所以把自己的实现贴到博客。

首先是获取数据,比特币中国的 API 是个很鬼怪的东西,实时交易数据的接口,返回的数据中最高最低和成交量都是基于过去24小时的,要知道比特币交易是没有休市的啊。所以获取数据过程中需要自己计算这些。这里考虑到股市一般一天实际交易4小时,所以整个设计也是默认4小时的图形展示。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# query price data from BTCChina.
from urllib import urlopen
from ast import literal_eval
import MySQLdb
import json
import yaml
import time

config = yaml.load(open('config.yaml'))
conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )

def write_db(datas):
    try:
        cur_write = conn.cursor()
        sql =  "insert into ticker(sell, buy, last, vol, high, low) values( %s, %s, %s,%s,%s,%s)"
        cur_write.execute(sql,datas)
        conn.commit()
        cur_write.close()
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])

def get_tid():
    try:
        vol_url = config['btcchina']['vol_url']
        remote_file = urlopen(vol_url)
        remote_data = remote_file.read()
        remote_file.close()
        remote_data = json.loads(str(remote_data))
        return remote_data[-1]['tid']
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])

def get_ohlc(num):
    try:
        read = conn.cursor()
        hlvsql = "select max(last),min(last) from ticker where time between date_add(now(),interval -%s minute) and now()" % num
        read.execute(hlvsql)
        high, low = read.fetchone()
        closesql = "select last from ticker where time between date_add(now(),interval -%s minute) and now() order by time desc limit 1" % num
        read.execute(closesql)
        close = read.fetchone()
        opensql = "select last from ticker where time between date_add(now(),interval -%s minute) and now() order by time asc limit 1" % num
        read.execute(opensql)
        opend = read.fetchone()
        return opend[0], high, low, close[0]
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])

def write_ohlc(data):
    try:
        cur_write = conn.cursor()
        ohlcsql =  'insert into ohlc(open, high, low, close, vol) values( %s, %s, %s, %s, %s)'
        cur_write.execute(ohlcsql, data)
        conn.commit()
        cur_write.close()
    except MySQLdb.Error,e:
        print "Mysql error %d : %s." % (e.args[0], e.args[1])
    except Exception as e:
        print("执行Mysql写入数据时出错: %s" %  e)

def instance():
    try:
    # returns something like {"high":738.88,"low":689.10,"buy":713.50,"sell":717.30,"last":717.41,"vol":4797.32000000}
        remote_file = urlopen(config['btcchina']['ticker_url'])
        remote_data = remote_file.read()
        remote_file.close()
        remote_data = json.loads(str(remote_data))['ticker']
    #   remote_data = {key:literal_eval(remote_data[key]) for key in remote_data}
    except:
        remote_data = []
    datas = []
    for key in remote_data:
        datas.append(remote_data[key])
    return datas

lastid = 0
ohlc_period = 60
next_ohlc = int(time.time()) / ohlc_period * ohlc_period
while True:
    datas = instance()
    if datas:
        write_db(datas)
    if(int(time.time()) > next_ohlc):
        next_ohlc += ohlc_period

        data = list(get_ohlc(1))

        latestid = get_tid()
        data.append(int(latestid) - int(lastid))
        lastid = latestid

        write_ohlc(data)
        time.sleep(1)

这里主要把实时数据存入ticker表,分钟统计数据存入ohlc表。然后是各指标算法。首先是 MACD :

#/*******************************************************************************
# * Author: Chenlin Rao | Renren inc.
# * Email: rao.chenlin@gmail.com
# * Last modified: 2013-11-26 22:02
# * Filename: macd.py
# * Description: 
#       EMA(12)=LastEMA(12)* 11/13 + Close * 2/13
#       EMA(26)=LastEMA(26)* 25/27 + Close * 2/27
#       
#       DIF=EMA(12)-EMA(26)
#       DEA=LastDEA * 8/10 + DIF * 2/10
#       MACD=(DIF-DEA) * 2
# * *****************************************************************************/
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import hashlib
import MySQLdb
import yaml

class MACD():

    def __init__(self):
        config = yaml.load(open('config.yml'))
        self.sleep_time = config['btcchina']['trade_option']['sleep_time']
        self.conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )

    def _getclose(self, num):
        read = self.conn.cursor()
        sql = "select close,time from ohlc order by id desc limit %s" % num
        count = read.execute(sql)
        results = read.fetchall()
        return results[::-1]

    def _ema(self, s, n):
        """
        returns an n period exponential moving average for
        the time series s

        s is a list ordered from oldest (index 0) to most
        recent (index -1)
        n is an integer

        returns a numeric array of the exponential
        moving average
        """
        if len(s) <= n:
            return "No enough item in %s" % s
        ema = []
        j = 1

        #get n sma first and calculate the next n period ema
        sma = sum(s[:n]) / n
        multiplier = 2 / float(1 + n)
        ema.append(sma)

        #EMA(current) = ( (Price(current) - EMA(prev) ) x Multiplier) + EMA(prev)
        ema.append(( (s[n] - sma) * multiplier) + sma)

        #now calculate the rest of the values
        for i in s[n+1:]:
            tmp = ( (i - ema[j]) * multiplier) + ema[j]
            j = j + 1
            ema.append(tmp)

        return ema

    def getMACD(self, n):
        array = self._getclose(n)
        prices = map(lambda x: x[0], array)
        t = map(lambda x: int(time.mktime(x[1].timetuple())) * 1000, array)

        short_ema = self._ema(prices, 12)
        long_ema = self._ema(prices, 26)
        diff = map(lambda x: x[0]-x[1], zip(short_ema[::-1], long_ema[::-1]))
        diff.reverse()

        dea = self._ema(diff, 9)

        bar = map(lambda x: 2*(x[0]-x[1]), zip(diff[::-1], dea[::-1]))
        bar.reverse()

        return zip(t[33:], diff[8:]), zip(t[33:], dea), zip(t[33:], bar)

然后是 BOLL :

#/*******************************************************************************
# * Author: Chenlin Rao | Renren inc.
# * Email: rao.chenlin@gmail.com
# * Last modified: 2013-11-26 22:02
# * Filename: macd.py
# * Description: 
#       MA=avg(close(20))
#       MD=std(close(20))
#       
#       MB=MA(20)
#       UP=MB + 2*MD
#       DN=MB - 2*MD
# * *****************************************************************************/
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random
import hashlib
import MySQLdb
import yaml
import time

class BOLL():

    def __init__(self):
        config = yaml.load(open('config.yml'))
        self.sleep_time = config['btcchina']['trade_option']['sleep_time']
        self.conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )
     
    def _getMA(self, array):
        length = len(array)
        return sum(array) / length
    
    def _getMD(self, array):
        length = len(array)
        average = sum(array) / length
        d = 0
        for i in array: d += (i - average) ** 2
        return (d/length) ** 0.5
    
    def getOHLC(self, num):
        read = self.conn.cursor()
        sql = "select time,open,high,low,close,vol from ohlc order by id desc limit %s" % num
        count = read.execute(sql)
        results = read.fetchall()
        return map(lambda x: [int(time.mktime(x[0].timetuple())) * 1000, x[1],x[2],x[3],x[4],x[5]], results[::-1])

    def _getCur(self, fromtime):
        curread = self.conn.cursor()
        cursql = "select last,vol from ticker where time between date_add('%s', interval -0 minute) and now()" % time.strftime('%F %T', time.localtime(fromtime))
        curread.execute(cursql)
        curlist = map(lambda x: x[0], curread.fetchall())
        vollist = map(lambda x: x[1], curread.fetchall())
        if len(curlist) > 0:
            return int(time.time())*1000, curlist[0], max(curlist), min(curlist), curlist[-1], sum(vollist)
        else:
            return None

    def _getClose(self, matrix):
        close = map(lambda x: x[4], matrix)
        return close
    
    def getBOLL(self, num, days):
        matrix = self.getOHLC(num)
        cur = self._getCur(matrix[-1][0]/1000)
        if cur:
            matrix.append(cur)
        array = self._getClose(matrix)
        up = []
        mb = []
        dn = []

        x = days
        while x < len(array):
            curmb = self._getMA(array[x-days:x])
            curmd = self._getMD(array[x-days:x])
            mb.append( [ matrix[x][0], curmb ] )
            up.append( [ matrix[x][0], curmb + 2 * curmd ] )
            dn.append( [ matrix[x][0], curmb - 2 * curmd ] )
            x += 1

        return matrix[days:], up, mb, dn

最后是 KDJ :

#/*******************************************************************************
# * Author: Chenlin Rao | Renren inc.
# * Email: rao.chenlin@gmail.com
# * Last modified: 2013-11-26 22:02
# * Filename: macd.py
# * Description: 
#       RSV=(close-low(9))/(high(9)-low(9))*100
#       K=SMA(RSV(3), 1)
#       D=SMA(K(3), 1)
#       J=3*K-2*D
# * *****************************************************************************/
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import hashlib
import MySQLdb
import yaml
import time

class KDJ():

    def __init__(self):
        config = yaml.load(open('config.yml'))
        self.sleep_time = config['btcchina']['trade_option']['sleep_time']
        self.conn = MySQLdb.connect(host=config['database']['host'],user=config['database']['username'],passwd=config['database']['password'],db =config['database']['databasename'],charset=config['database']['encoding'] )

    def _getHLC(self, num):
        read = self.conn.cursor()
        sql = "select high,low,close,time from ohlc order by id desc limit %s" % num
        count = read.execute(sql)
        results = read.fetchall()
        return results[::-1]

    def _avg(self, a):
        length = len(a)
        return sum(a) / length
    
    def _getMA(self, values, window):
        array = []
        x = window
        while x < len(values):
            curmb = self._avg(values[x-window:x])
            array.append( curmb )
            x += 1
        return array
    
    def _getRSV(self, arrays):
        rsv = []
        times = []
        x = 9
        while x < len(arrays):
            high = max(map(lambda x: x[0], arrays[x-9:x]))
            low = min(map(lambda x: x[1], arrays[x-9:x]))
            close = arrays[x-1][2]
            rsv.append( (close-low)/(high-low)*100 )
            t = int(time.mktime(arrays[x-1][3].timetuple())) * 1000
            times.append(t)
            x += 1
        return times, rsv

    def getKDJ(self, num):
        hlc = self._getHLC(num)
        t, rsv = self._getRSV(hlc)
        k = self._getMA(rsv,3)
        d = self._getMA(k,3)
        j = map(lambda x: 3*x[0]-2*x[1], zip(k[3:], d))
        return zip(t[2:], k), zip(t[5:], d), zip(t[5:], j)

最后通过一个简单的python web框架完成界面展示,这个叫 bottle.py 的框架是个单文件,相当方便。

#!/usr/bin/python
import json
import yaml
from macd import MACD
from boll import BOLL
from kdj import KDJ
from bottle import route, run, static_file, redirect, template
 
config = yaml.load(open('config.yml'))
color = {
    'cn':{'up':'#ff0000','dn':'#00ff00'},
    'us':{'dn':'#ff0000','up':'#00ff00'},
}

@route('/')
def index():
    redirect('/mkb/240')

@route('/mkb/<ago:int>')
def mkb(ago):
    like = config['webui']['color']
    return template('webui', ago = ago, color = color[like])

@route('/js/<filename>')
def js(filename):
    return static_file(filename, root='./js/')

@route('/boll')
def boll():
    return "boll"

@route('/macd/<day:int>')
def macd(day):
    m = MACD()
    dif, dea, bar = m.getMACD(day)
    return json.dumps({'dif':dif, 'dea':dea, 'bar':bar})

@route('/boll/<day:int>')
def boll(day):
    b = BOLL()
    ohlc, up, md, dn = b.getBOLL(day, 20)
    return json.dumps({'ohlc':ohlc, 'up':up, 'md':md, 'dn':dn})

@route('/kdj/<day:int>')
def kdj(day):
    kdj = KDJ()
    k, d, j = kdj.getKDJ(day)
    return json.dumps({'k':k, 'd':d, 'j':j})

run(host='127.0.0.1', port=8000, debug=True)

唯一的一个 html 就是具体用 highcharts 画图的地方,如下:

<html>
<head>
   <meta http-equiv="refresh" content="60">
   <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
   <script type="text/javascript" src="/js/highstock.js"></script>
   <script type="text/javascript" src="/js/highcharts.js"></script>
   <script>
    $(function () {
        Highcharts.setOptions({  
            global: {  
                useUTC: false  
            }  
        }); 
        $.getJSON('/boll/', function(bolldata) {
            var ohlc = []
                volume = [],
                dataLength = bolldata['ohlc'].length;
            for (i = 0; i < dataLength; i++) {
                ohlc.push([
                    bolldata['ohlc'][i][0],
                    bolldata['ohlc'][i][1],
                    bolldata['ohlc'][i][2],
                    bolldata['ohlc'][i][3],
                    bolldata['ohlc'][i][4],
                ]);
                volume.push([
                    bolldata['ohlc'][i][0],
                    bolldata['ohlc'][i][5],
                ])
            };
            $.getJSON('/kdj/', function(kdjdata) {
               $.getJSON('/macd/', function(macddata) {
                    $('#container').highcharts('StockChart', {
                        rangeSelector: {
                            enabled: 0
                        },
                        chart: {
                            backgroundColor: '#333333',
                        },
                	    tooltip: {
                	    	formatter: function() {
                				var s = '<b>'+ Highcharts.dateFormat('%A, %b %e, %H:%M', this.x) +'</b>';
                				$.each(this.points, function(i, point) {
                					s += '<br/>'+this.series.name+': '+parseFloat(point.y).toFixed(2);
                				});
                				return s;
                			}
                	    },
                        plotOptions: {
                            series: {
                                marker: {
                                    enabled: false
                                },
                                lineWidth: 1.1,
                            }
                        },
                        yAxis: [{
                          title: {
                              text: 'MACD(12,26,9)'
                          },
                          height: 200,
                        }, {
                          title: {
                              text: 'KDJ(9,3,3)'
                          },
                          top: 250,
                          height: 150,
                          offset: 0,
                          gridLineDashStyle: 'Dash',
                          tickPositions: [0, 20, 50, 80, 100, 200]
                        }, {
                            title: {
                                text: 'BOLL(20)'
                            },
                            top: 450,
                            height: 300,
                            offset: 0,
                        }, {
                            title: {
                                text: 'VOL'
                            },
                            top: 800,
                            height: 100,
                            offset: 0,
                        }],
                        series: [{
                            name: 'BAR',
                            color: '',
                            negativeColor: '',
                            borderColor: '#333333',
                            type: 'column',
                            data: macddata['bar'],
                            yAxis: 0,
                        }, {
                            name: 'DIFF',
                            color: '#ffffff',
                            type: 'line',
                            data: macddata['dif'],
                            lineWidth: 2,
                            yAxis: 0,
                        }, {
                            name: 'DEA',
                            color: '#ffff00',
                            type: 'line',
                            data: macddata['dea'],
                            lineWidth: 2,
                            yAxis: 0,
                        }, {
                            name: 'K',
                            color: '#ffffff',
                            type: 'line',
                            data: kdjdata['k'],
                            yAxis: 1,
                        }, {
                            name: 'D',
                            color: '#ffff00',
                            type: 'line',
                            data: kdjdata['d'],
                            yAxis: 1,
                        }, {
                            name: 'J',
                            color: '#cc99cc',
                            type: 'line',
                            data: kdjdata['j'],
                            yAxis: 1,
                        }, {
                            type: 'candlestick',
                            name: 'ohlc',
                            data: ohlc,
                            upColor: '',
                            upLineColor: '',
                            color: '',
                            lineColor: '',
                            yAxis: 2,
                        }, {
                            type: 'spline',
                            name: 'up',
                            data: bolldata['up'],
                            color: '#ffff00',
                            lineWidth: 2,
                            yAxis: 2,
                        }, {
                            type: 'spline',
                            name: 'md',
                            data: bolldata['md'],
                            color: '#ffffff',
                            lineWidth: 2,
                            yAxis: 2,
                        }, {
                            type: 'spline',
                            name: 'dn',
                            data: bolldata['dn'],
                            color: '#cc99cc',
                            lineWidth: 2,
                            yAxis: 2,
                        }, {
                            name: 'VOL',
                            borderColor: '#333333',
                            type: 'column',
                            data: volume,
                            yAxis: 3,
                        }]
                    });
                });
            });
        });
    }); 
   </script>
</head>
<body>
   <div id="container" style="min-width:800px;height:1000px;"></div>
</body>
</html>

highcharts 有个问题,就是不能跟 amcharts 或者 echarts 那样提供一个画笔工具,让用户自己在生成的图形上再涂抹线条,这个功能其实在蜡烛图上判断压力位支撑位的时候很有用。不过蜡烛图 btc123 也提供了,我也就懒得再用 amcharts 重写一遍。

效果如下: