曾几何时,我看国内电视节目上有个公开竞聘互联网公司产品经理的节目,里面有一个理工女孩,被问到个人爱好的时候,她说自己平时爱好啊就是折腾代码,看着代码运行出来的各种结果,心中感到无比的甜蜜。当时我太太看了以后哈哈大笑,但是作为一个理工男,我内心深感赞同,这种甜蜜的感觉类似初恋,或者像是看到从自己肚子里生个活蹦乱跳的宝宝出来。
是的,作为一个理工男,尤其跑到澳洲这种空旷的地方来,没有任何不良嗜好,不抽烟不喝酒,也不去沾花惹草,平时除了DIY前后花园,给屋子抹抹油漆,打法空闲时间的方法就是折腾代码玩,心中有了个想法,就喜欢把它实现了。 折腾的过程犹如十月怀胎,的确有有些痛苦的,但是咱痛并快乐中啊,一旦折腾出来个产品,就如同从自己肚子里生出一个婴儿一样,这种感觉不是在雇佣/被雇佣关系中做产品能够给予的。
在加密货币市场里,有一群人,甚至是机构,他们通过“搬砖”来赚钱,他们主要利用了同一种加密货币,加密货币交易所之间价格的不同,从一个交易所买低,到另外一个交易所卖高,比如,比特币在一个交易所7000刀挂牌卖,在另外一个交易所8000刀挂牌买,当然,量要够大,这么夸张的情形我还真碰到过一次,某交易所要关门前,出现匪夷所思的低价出售。甚至很有很多人或机构搞了很多“搬砖机器人”在做这个事情,这也导致交易所之间差价也越来越小了。
我做的事情就是从各个交易所通过它们提供的API,把它们支持哪些加密货币和币的当前价格取出来,然后集成为这个样子:
以币安为例,每个交易所用一个python文件表示,只要实现两个函数:
def getSymbol(code):
symbol_list = util.requestTimeout(url='https://api.binance.com/api/v1/exchangeInfo',timeout=waittime,errMsg='cannot get exchangeInfo in binance!')
if symbol_list == None:
return None
for i in symbol_list.json()['symbols']:
symbol = i['symbol']
quoteasset= i['quoteAsset']
if symbol.startswith(code.upper()) & (quoteasset == 'USDT'):
return symbol
for i in symbol_list.json()['symbols']:
symbol = i['symbol']
quoteasset= i['quoteAsset']
if symbol.startswith(code.upper()) & (quoteasset == 'BTC'):
return symbol
return None
def getUSDPrice(symbol):
symbol = getSymbol(symbol)
if symbol ==None:
return None
url = "https://api.binance.com/api/v3/ticker/bookTicker?symbol=%s"%symbol
print(url)
response = util.requestTimeout(url=url,timeout=waittime,errMsg='cannot get bookTicker in binance')
if response is None:
return None
把这些交易所文件全部包含在exchanges文件夹里,形成一个模块,最后在主文件里,每次启动前,把所有交易所的模块加载,这样以后要添加新的交易所,只要新增加一个文件放exchanges文件夹里就OK了:
import exchanges
import pkgutil
exchange_list = []
prefix = exchanges.__name__ + "."
for importer, modname, ispkg in pkgutil.iter_modules(exchanges.__path__,prefix):
print ("Found submodule %s (is a package: %s)"% (modname, ispkg))
module = __import__(modname, fromlist="exchanges")
exchange_list.append(module)
在调用交易所的实时数据方面,我采用了多线程池的异步方式,这样不会因为某个调用出问题而全体卡着,这些都是后来经过不断测试改进后的方案:
def getRealtimeData(symbol):
results = []
pool = ThreadPool()
for exchange in exchange_list:
print(exchange)
results.append(pool.apply_async(exchange.getUSDPrice, args = (symbol,)))
price_list = [r.get() for r in results if r is not None and r.get() is not None]
pool.close()
pool.join()
if not price_list:
return None
df = pd.DataFrame(price_list)
print(df)
df = df[['source','bid','ask','trade']]
df.columns=['Exchange','BestBid','BestAsk','Last Trade Price']
return df
返回的数据我都是用的python pandas的dataframe格式,我特别喜欢pandas这个大数据处理的模块,非常方便,变化多端。
整个web框架采用了react.js,有人说那玩意不是javacript才有的吗?不错,但是python把它包装成一种叫做dash的东西,配合python自带的flask,以及plot.js,非常适合数据的可视化。但是使用过程中我发现这玩意还是刚开始,有些不成熟,也有些小bug,不过对我想做的玩意来说,足够了。
这个界面设计类似写html代码:
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = 'Cryptocurrency Exchanges Depth'
app.layout = html.Div(
[
```
html.Div(
className="app-header",
children=[
html.Div('Cryptocurrency Exchanges Depth', className="app-header--title")
]
),
html.Div(
[
html.Label('Symbol'),
dcc.Dropdown(
id='symbol',
options=[
{'label': symbol+' '+name, 'value': symbol} for symbol,name in symbols.items()
],value='BTC')
],style={'width': '20%', 'display': 'inline-block'}),
html.Div(
[
html.Div(id='my-table',style={'width': '40%', 'display': 'inline-block'}),
html.Div(
[
dcc.Tabs(id="tabs-history", value='tab-history', children=[
dcc.Tab(label='24 hours trend', value='tab-24-hours',style=tab_style, selected_style=tab_selected_style),
dcc.Tab(label='1 month trend', value='tab-1-month',style=tab_style, selected_style=tab_selected_style),
dcc.Tab(label='1 year trend', value='tab-1-year',style=tab_style, selected_style=tab_selected_style),
],style=tabs_styles),
dcc.Graph(id='trend-chart')
],style={'width': '40%', 'display': 'inline-block','float':'right'})
]),
dcc.Interval(
id='interval-component',
interval=10*1000, # in milliseconds
n_intervals=0
),
html.Div (
className="footer",
children=[
html.Div([
html.P('engineerman.club'),
html.P('2018 copyright'),
], style={'width': '49%', 'display': 'table-cell','vertical-align': 'middle'})
```
]
)
然后采用一种回调函数的方式来处理用户跟界面的互动,这里链接这界面和数据处理模块:
@app.callback(Output('my-table', 'children'), [Input('symbol', 'value'),Input('interval-component', 'n_intervals')])
def table_update(selected_dropdown_value,n):
df1 = getRealtimeData(selected_dropdown_value)
return generate_table(df1)
最后我把这个婴儿放到亚马逊的aws上运行,还给它一个子域名,看着从自己肚子里生出来的娃,内心无比欣慰: