123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029 |
- #!/usr/bin/python
- # -*- coding: UTF-8 -*-
- #
- # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
- # Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- #
- """
- btc-ticker: ticker and alarm clock for mmgen-node-tools
- """
- import sys,os,time,subprocess
- import threading as th
- from collections import OrderedDict
- from decimal import Decimal
- from mmgen.share import Opts
- from mmgen.util import msg,msg_r,die,die_pause
- from mmgen.node_tools.Global import *
- from mmgen.node_tools.Util import *
- from mmgen.node_tools.Sound import *
- from mmgen.node_tools.Term import *
- die_pause(1,'Ticker temporarily disabled') # DEBUG
- from mmgen.color import *
- init_color()
- quit = False
- sound_vol = float(100)
- audio_host,repeat_spec = '','3s:5m,1m:30m,5m:1h,30m:1d'
- # repeat_spec = '1:5m' # DEBUG
- valid_loglevels = OrderedDict([(1,'info'),(2,'data'),(3,'debug')])
- logfile = 'ticker_log.txt'
- alrm_clock = 'off'
- num_digits = 6
- proxy = None
- fx_host= 'https://finance.yahoo.com'
- sounds_dir = nt.system_data_dir+'/audio'
- sounds = {
- 'lo': [sounds_dir+'/ringtone.wav', 95],
- 'hi': [sounds_dir+'/Positive.wav', 102],
- 'conn_err': [sounds_dir+'/Rhodes.wav', 105],
- 'alrm_clock': [sounds_dir+'/Counterpoint.wav', 105],
- }
- alrm_names = tuple(sounds.keys())
- dlock,llock,alock = th.Lock(),th.Lock(),th.Lock()
- data_dir = nt.data_dir+'/lib/ticker/'
- try: os.makedirs(data_dir)
- except: pass
- def update_fx(a,b,debug=False):
- if debug:
- with open('/tmp/ticker.out') as f: text = f.read()
- else:
- text = get_url('{}/q?s={}{}'.format(fx_host,a,b),gzip_ok=True,proxy=proxy,debug=debug_curl)
- if not text: return False
- import re
- ret = re.split('yfs_l10_{}{}=.+?>'.format(a,b),text,maxsplit=1)[1].split('<',1)[0][:10]
- try:
- globals()[a+b] = float(ret)
- with open(data_dir+'/{}{}.txt'.format(a,b),'wb') as f: f.write(ret+'\n')
- return True
- except:
- return False
- def get_fx(a,b):
- try:
- with open(data_dir+'/{}{}.txt'.format(a,b)) as f:
- d = f.read().rstrip()
- globals()[a+b] = float(d)
- return True
- except:
- return update_fx(a,b)
- with open('/etc/timezone') as f:
- os.environ['TZ'] = f.read().rstrip()
- class Xch(object): pass
- class Src(object): pass
- num_xchgs = 2
- xchgs = tuple([Xch() for i in range(num_xchgs)])
- sources = OrderedDict([('tc','ticker')])
- for g in xchgs:
- for k in sources:
- setattr(g,k,Src())
- setattr(getattr(g,k),'desc',sources[k])
- for g in [xchgs[0]]:
- g.cur = 'USD'
- # g.Desc = 'OKCoin USD'
- # g.desc = 'okcoin_usd'
- # g.desc_short = 'okc'
- # g.tc.url = 'https://www.okcoin.com/api/v1/ticker.do?symbol=btc_usd'
- g.Desc = 'Gemini'
- g.desc = 'gemini'
- g.desc_short = 'gem'
- g.tc.url = 'https://api.gemini.com/v1/pubticker/btcusd'
- g.poll_secs = 60
- g.cur_sign = '$'
- g.xcur_sign = '¥'
- g.fiat_precision = 2
- g.hi_alrm = 999999
- g.lo_alrm = 1
- g.cc_unit = 'BTC'
- for g in [xchgs[1]]:
- g.cur = 'CNY'
- g.Desc = 'OKCoin CNY'
- g.desc = 'okcoin_cny'
- g.desc_short = 'okc'
- g.tc.url = 'https://www.okcoin.cn/api/v1/ticker.do?symbol=btc_cny'
- g.poll_secs = 60
- g.cur_sign = '¥'
- g.xcur_sign = '$'
- g.fiat_precision = 1
- g.hi_alrm = 999999
- g.lo_alrm = 1
- g.cc_unit = 'BTC'
- # g.cur = 'USD'
- # g.Desc = 'BitFinex'
- # g.desc = 'bitfinex'
- # g.desc_short = 'bfx'
- # g.tc.url = 'https://api.bitfinex.com/v1/pubticker/btcusd'
- # Gemini - available symbols: btcusd ethbtc ethusd
- # g.cur = 'USD'
- # g.Desc = 'Gemini'
- # g.desc = 'gemini'
- # g.desc_short = 'gem'
- # g.tc.url = 'https://api.gemini.com/v1/pubticker/btcusd'
- opts_data = {
- 'prog_name': sys.argv[0].split('/')[-1],
- 'desc': 'Price alarm for Bitcoin exchange',
- 'usage': '[opts] [<low price alarm> <high price alarm>]',
- #-b, --big-number Print big numbers
- 'options': """
- -h, --help Print this help message
- -a, --alarm-clock-only Disable ticker, use as alarm clock only
- -c, --cur-ticker-only Display only current exchange's ticker price
- -d, --debug Debug mode. Use saved HTTP data from files
- -D, --debug-curl Debug curl
- -l, --log= l Log all data to file '{lf}' at levels 'l' (comma-separated: {lls})
- -n, --num-digits= n Display 'n' number of big digits
- -p, --poll-intervals=i1[,i2] Poll servers every 'i' seconds (default: '{pi}')
- -P, --proxy= x Connect via proxy 'x' (see PROXY EXAMPLES below for format)
- -r, --repeat-spec Sleep interval/duration program for the alarm
- (default: '{r}')
- (see REPEAT SPEC FORMAT below)
- -R, --resize-window Resize window to optimum dimensions
- -t, --testing Testing mode. Don't execute shell commands
- -u, --utc Show times in UTC rather than local time
- -v, --volume= n Adjust sound volume by percentage 'n' (default: {v})
- -V, --test-volume Test the alarm volume and exit
- -x, --xchgs= x,y[,…] Display only exchanges 'x,y[,…]' (comma-separated
- list of integers, see CONFIGURED EXCHANGES below)
- """.format(
- v=int(sound_vol),
- pi = ','.join([str(g.poll_secs) for g in xchgs]),
- r=repeat_spec,
- lf=logfile,
- lls=', '.join(['%s=%s'%(i,j) for i,j in valid_loglevels.items()])
- ),
- 'notes': '''
- REPEAT SPEC FORMAT:
- <interval>:<duration>[,<interval>:<duration>[,…]]
- For example, '3s:5m,1m:2h,30m:1d' means:
- ring alarm every 3 seconds for 5 minutes,
- then every minute for 2 hours,
- then every 30 minutes for 1 day
- PROXY EXAMPLES:
- socks5h://localhost:9050 (for Tor running on localhost)
- http://192.168.0.4:8118 (for Privoxy running on host 192.168.0.4)
- CONFIGURED EXCHANGES:
- {x}
- '''.format(
- x='\n '.join(['%s - %s'%(n,x.Desc) for n,x in enumerate(xchgs,1)])
- )
- }
- opts,args = Opts.parse_opts(sys.argv,opts_data)[:2]
- debug_curl = 'debug_curl' in opts
- proxy = opts['proxy'] if 'proxy' in opts else None
- # Global var: the currently displayed exchange
- if 'alarm_clock_only' in opts:
- num_digits = 4
- mute_alrms = False
- ga = None
- xchgs = []
- else:
- mute_alrms = True
- ga = xchgs[0]
- fx_pairs = [('usd','cny')]
- for a,b in fx_pairs:
- msg_r('Getting {}/{} exchange rate...'.format(a.upper(),b.upper()))
- if get_fx(a,b):
- msg('OK')
- else:
- die_pause(1,'Unable to get {}/{} exchange rate'.format(a.upper(),b.upper()))
- if 'xchgs' in opts:
- usr_xchgs = [int(i)-1 for i in opts['xchgs'].split(',')]
- xchgs = [xchgs[i] for i in usr_xchgs]
- # Initialize some variables for active exchanges
- for g in xchgs:
- g.retry_interval = 30
- g.conn_alrm_timeout = 300
- g.tc.bal = 0.0,0.0,0.0
- if not hasattr(g,'desc_short'): g.desc_short = g.desc
- for k in sources:
- s = getattr(g,k)
- main_thrd_names = tuple([x.desc+'_ticker' for x in xchgs])
- kill_flgs = dict([(k,th.Event()) for k in main_thrd_names + ('alrm','clock','log')])
- debug = 'debug' in opts
- if debug: msg('Debugging mode. Using saved data from files')
- def do_errmsg(s,k='ticker'):
- if k == 'ticker':
- with dlock:
- blank_ticker()
- msg_r(CUR_HOME)
- msgred_r(s)
- time.sleep(5)
- def toggle_option(k):
- with dlock:
- if k in opts: del opts[k]
- else: opts[k] = True
- def get_thrd_names(): return [i.name for i in th.enumerate()]
- def start_alrm(name):
- kill_flgs['alrm'].clear()
- t = th.Thread(
- target = play_sound,
- name = name,
- kwargs = {
- 'fn': sounds[name][0], # (fn,vol)
- 'repeat_spec': repeat_spec,
- 'remote_host': audio_host,
- 'vol': sound_vol * sounds[name][1] / 100,
- 'kill_flg': kill_flgs['alrm'],
- 'testing': False
- }
- )
- t.daemon = True
- t.start()
- def start_alrm_maybe(my_thrd_name,kill_list=None):
- if mute_alrms: return
- # Allow for an empty kill list
- klist = kill_list if kill_list != None else alrm_names
- with alock: # kill alarm thrds except mine
- if any([n in klist and n != my_thrd_name for n in get_thrd_names()]):
- kill_flgs['alrm'].set()
- if not my_thrd_name: return # if thread name 'None', kill first, then return
- if not any([n in alrm_names for n in get_thrd_names()]):
- start_alrm(my_thrd_name)
- def parse_loglevel_arg(s):
- m1 = 'Invalid loglevel argument: %s\n' % s
- try: ret = [int(i) for i in s.split(',')]
- except:
- m2 = 'Loglevels must be comma-separated int values'
- return False, m1+m2
- if not set(ret) <= set(valid_loglevels):
- m2 = 'Valid loglevels: %s' % ','.join([str(i) for i in valid_loglevels])
- return False, m1+m2
- return ret,'OK'
- def set_alrm_vals(s):
- ret = []
- m1 = 'Invalid alarm argument: %s\n' % s
- for e in s.split(':'):
- try: ret.append([Decimal(i) for i in e.split(',')])
- except:
- m2 = 'Alarms must be comma-separated decimal values'
- return False, m1+m2
- if len(ret[-1]) != 2:
- m2 = 'Each element of alarm list must have 2 items (lo_alrm,hi_alrm)'
- return False, m1+m2
- if len(ret) != len(xchgs):
- m2 = 'Alarm list must be %s colon-separated comma-separated lists' % len(xchgs)
- return False, m1+m2
- for g,a in zip(xchgs,ret):
- lo,hi = a
- if lo > hi:
- m2 = 'Low alarm (%s) is greater than high alarm (%s)' % (lo,hi)
- return False, m1+m2
- setattr(g,'lo_alrm',lo)
- setattr(g,'hi_alrm',hi)
- return ret,'OK'
- def set_poll_intervals(s,xchg=None):
- ret = []
- m1 = 'Invalid poll interval argument: %s\n' % s
- m2 = 'Poll intervals must be comma-separated integer values'
- m3 = 'Poll interval must be integer value'
- for e in (s.split(','),[s])[bool(xchg)]:
- try: ret.append(float(e))
- except:
- return False, m1+(m2,m3)[bool(xchg)]
- if not xchg and len(ret) != len(xchgs):
- m2 = 'Poll interval list have %s items' % len(xchgs)
- return False, m1+m2
- for g,p in zip((xchgs,[xchg])[bool(xchg)],ret):
- setattr(g,'poll_secs',float(p))
- return ret,'OK'
- def set_xch_param(source,param,s,desc):
- ret = []
- m1 = 'Invalid %s argument: %s\n' % (desc,s)
- for e in s.split(':'):
- try: ret.append(float(e))
- except:
- m2 = '%s arg must be colon-separated float values' % desc.capitalize()
- return False, m1+m2
- if len(ret) != len(xchgs):
- m2 = '%s list must have %s colon-separated items' % (
- desc.capitalize(),len(xchgs))
- return False, m1+m2
- for g,p in zip(xchgs,ret):
- a = getattr(g,source)
- setattr(a,param,float(p))
- return ret,'OK'
- if 'log' in opts:
- loglevels,errmsg = parse_loglevel_arg(opts['log'])
- if not loglevels: die_pause(1,errmsg)
- msg('Logging at level{} {}'.format(
- ('s','')[len(loglevels)==1],
- ' '.join([valid_loglevels[i].upper() for i in loglevels])))
- else:
- loglevels = []
- if 'repeat_spec' in opts:
- repeat_spec = opts['repeat_spec']
- msg("Using program '{}' for alarm".format(repeat_spec))
- if 'num_digits' in opts:
- n = opts['num_digits']
- if len(n) != 1 or n not in '56789':
- die_pause(1,"'%s': invalid value for --num-digits option" % n)
- num_digits = int(n)
- if 'volume' in opts:
- sound_vol = int(opts['volume'])
- for k in sounds:
- sounds[k][1] = float(sounds[k][1] * sound_vol / 100)
- msg('Adjusting sound volume by {}%'.format(sound_vol))
- if 'test_volume' in opts:
- for k in sounds:
- msg("Playing '{}' ({})".format(k,sounds[k][0]))
- play_sound(
- fn=sounds[k][0],
- vol=sound_vol * sounds[k][1] / 100,
- testing='testing' in opts
- )
- sys.exit()
- if len(args) > 1: Opts.usage(opts_data)
- if len(args) == 1:
- ret,errmsg = set_alrm_vals(args[0])
- if not ret: die_pause(1,'Error: ' + errmsg)
- msg('Setting alarms to %s' % ', '.join(['{} {}'.format(i,j) for i,j in ret]))
- if 'poll_intervals' in opts:
- ret,errmsg = set_poll_intervals(opts['poll_intervals'])
- if not ret: die_pause(1,errmsg)
- msg('Polling every %s seconds' % ret)
- tmux = 'TMUX' in os.environ
- if tmux:
- subprocess.check_output(['tmux','set','set-titles','on'])
- subprocess.check_output(['tmux','set','status','off'])
- # dcmd = "date -R%s | cut -d' ' -f2-5" % ('','u')['utc' in opts]
- # subprocess.check_output(['tmux','set','status-right','#(%s)' % dcmd])
- infoW = 15
- bigDigitsW = big_digits['w']*num_digits + big_digits['pw']*1
- lPaneW = bigDigitsW+infoW+1
- topPaneH,rPaneW = 6,23
- def CUR_UP(n): return '\033[%sA' % n
- def CUR_DOWN(n): return '\033[%sB' % n
- def CUR_RIGHT(n): return '\033[%sC' % n
- def CUR_LEFT(n): return '\033[%sD' % n
- def WIN_TITLE(s): return '\033]0;%s\033\\' % s
- def WIN_RESIZE(w,h): return '\033[8;%s;%st' % (h,w)
- def WIN_CORNER(): return '\033[3;200;0t'
- CUR_UP1 = '\033[A'
- CUR_DN1 = '\033[B'
- CUR_SHOW = '\033[?25h'
- CUR_HIDE = '\033[?25l'
- BLINK = '\033[5m'
- RESET = '\033[0m'
- CUR_HOME = '\033[H'
- ERASE_ALL = '\033[0J'
- def draw_rectangle(s,l,h):
- msg_r(s + '\n'.join([l] * h))
- def blank_ticker():
- draw_rectangle(CUR_HOME,' ' * lPaneW,topPaneH)
- def blank_big_digits():
- draw_rectangle(CUR_HOME,' ' * (bigDigitsW+1),topPaneH)
- def park_cursor():
- msg_r('\r' + CUR_RIGHT(lPaneW-1))
- def colorize_bal(g,n):
- d = g.tc
- fs = '{:.%sf}' % g.fiat_precision
- return (nocolor,green,red)[
- (bool(d.save_bal[n]) and
- (d.bal[n]!=d.save_bal[n]) + (d.bal[n]<d.save_bal[n]))
- ](fs.format(d.bal[n]))
- def display_ac_info():
- ac_fmt = green('--:--') if alrm_clock == 'off' else yelbg(alrm_clock)
- info_lines = (
- '{}'.format(ac_fmt),
- 'snd vol: {}%'.format(int(sound_vol)),
- '{}'.format((yellow('unmuted'),'muted')[mute_alrms]),
- '{}'.format(blue(audio_host[:infoW] or 'localhost')),
- '',
- "'?' for help"
- )
- r = CUR_RIGHT(bigDigitsW+1)
- msg_r(CUR_HOME+r+('\n'+r).join(info_lines))
- park_cursor()
- ts = 'Alarm Clock: {}{}'.format(alrm_clock.upper(),('',' (m)')[mute_alrms])
- if tmux:
- subprocess.check_output(['tmux','set','set-titles-string',ts])
- else:
- msg_r(WIN_TITLE(ts))
- def display_ticker(g,called_by_clock=False):
- if not g: return
- d = g.tc
- log(3,'display_ticker(): ' + repr(d.__dict__))
- if not hasattr(d,'timestamp'): return # DEBUG
- avg = sum((d.bal[i] or d.save_bal[i]) for i in range(3)) / 3
- alrm,lb,hb,rst = (
- (None,'','',''),
- ('lo',BLINK,'',RESET),
- ('hi','',BLINK,RESET)
- )[(avg<g.lo_alrm)+(avg>g.hi_alrm)*2]
- start_alrm_maybe(alrm,['lo','hi'])
- if (g is not ga) and not called_by_clock: return
- lfmt = '{n:.{p}f}'.format(p=g.fiat_precision,n=d.bal[2])
- msg_r(CUR_HOME+lb+hb+display_big_digits(lfmt,pre=' '+rst))
- if (g is not ga) and called_by_clock:
- park_cursor()
- return
- hms = get_hms(d.timestamp,utc='utc' in opts)
- xcur = '{:.2f}'.format(d.bal[2]/usdcny if g.cur == 'CNY' else d.bal[2]*usdcny)
- ac_fmt = green('--:--') if alrm_clock == 'off' else yelbg(alrm_clock)
- info_lines = (
- '{} {}'.format(hms,('%ss'%int(g.poll_secs))),
- '{} bid/ask'.format(cyan(g.desc_short)),
- '{} {}'.format(colorize_bal(g,0), colorize_bal(g,1)),
- '{g.xcur_sign}{x} {h}'.format(g=g,x=xcur,
- h=('',blue(audio_host[:infoW-len(xcur)-2]))[bool(audio_host)]),
- '{} {} {}'.format(yellow(('♫','-')[mute_alrms]),
- lb+yellow(str(g.lo_alrm))+rst,
- hb+yellow(str(g.hi_alrm))+rst),
- '{} vol {}%'.format(ac_fmt,int(sound_vol))
- )
- r = CUR_RIGHT(bigDigitsW+1)
- msg_r(CUR_HOME+r+('\n'+r).join(info_lines))
- park_cursor()
- ccs = ('฿','Ł')[g.cc_unit=='LTC']
- if len(xchgs) == 2:
- x1,x2 = xchgs
- ts = '{}{:.{}f} / {}{:.{}f}'.format(
- x1.cur_sign,
- x1.tc.bal[2],
- x1.fiat_precision,
- x2.cur_sign,
- x2.tc.bal[2],
- x2.fiat_precision,
- )
- else:
- ts = '{}: {}{:.{}f} ({})'.format(
- ccs,
- g.cur_sign,
- d.bal[2],
- g.fiat_precision,
- g.Desc
- )
- if tmux:
- # subprocess.call(['tmux','rename-session',g.Desc], stderr=subprocess.PIPE)
- subprocess.check_output(['tmux','set','set-titles-string',ts])
- else:
- msg_r(WIN_TITLE(ts))
- def log(*args): # [lvl,g,src,data] OR [lvl,data]
- if 'log' in opts and args[0] in loglevels + [0]:
- if len(args) == 2:
- s = '{}: {}\n'.format(get_day_hms(),args[1])
- elif len(args) == 4:
- s = '{}: {} {} - {}\n'.format(
- get_day_hms(),
- args[1].desc_short.upper(),
- args[2].upper(),
- args[3]
- )
- with llock:
- fd = os.open(logfile,os.O_RDWR|os.O_APPEND|os.O_CREAT|os.O_SYNC)
- os.write(fd,s)
- os.close(fd)
- def get_market_data(g,d,connfail_msg='full'):
- # , post_data={}
- tcd = 'TRADING_CONSOLE_DEBUG_CONNFAIL'
- debug_connfail = tcd in os.environ and os.environ[tcd]
- null = None # hack for eval'ing Huobi trades data
- if debug:
- fn = 'debug_market_data/%s_ticker.json' % g.desc
- try:
- with open(fn) as f: text = f.read()
- log(3,'get_market_data(): {} {}'.format(g.desc,text))
- return eval(text)
- except:
- die(2,'Unable to open datafile %s' % fn)
- fail_count,conn_begin_time = 0,time.time()
- if debug_connfail:
- retry_interval,conn_alrm_timeout = 10,0
- else:
- retry_interval,conn_alrm_timeout = g.retry_interval,g.conn_alrm_timeout
- while True:
- try:
- text = get_url(d.url,proxy=proxy,debug=debug_curl)
- log(2,g,d.desc,text)
- return eval(text)
- except KeyboardInterrupt:
- die(1,'\nUser interrupt (get_market_data)\n')
- except EOFError:
- die(1,'\nEnd of file\n')
- except Exception as e:
- fail_count += 1
- if connfail_msg:
- with dlock:
- if g is ga:
- m = {
- 'short':'Connect fail ({})'.format(fail_count),
- 'full': 'Connect fail. Retry in {} seconds ({})'.format(
- retry_interval,fail_count)
- }
- dn = CUR_DOWN(topPaneH-1)
- blank_big_digits()
- msg_r(CUR_HOME+dn+m[connfail_msg]+' \b')
- k = '%s_%s' % (g.desc,d.desc)
- if time.time() - conn_begin_time > conn_alrm_timeout:
- if fail_count == 1:
- log(1,'Connect error (%s)' % k)
- if d.desc == 'ticker':
- with dlock: d.bal = d.bal[:2] + (0.0,) # can't assign to tuple, so this
- start_alrm_maybe('conn_err')
- # Sleep until user interrupt
- if kill_flgs[k].wait(retry_interval):
- kill_flgs[k].clear()
- return False
- def killwait(g,d):
- log(3,g,d.desc,'Begin wait')
- k = '%s_%s' % (g.desc,d.desc)
- if kill_flgs[k].wait(g.poll_secs):
- kill_flgs[k].clear()
- if quit: return True
- log(3,g,d.desc,'End wait')
- return False
- def log_loop():
- while True:
- for g in xchgs:
- with dlock:
- lstr = '{} last: {}{}'.format(g.desc_short.upper(),g.cur_sign,g.tc.bal[2])
- log(1,lstr)
- if kill_flgs['log'].wait(30):
- kill_flgs['log'].clear()
- if quit: return True
- def ticker_loop(g):
- d = g.tc
- while True:
- ret = get_market_data(g,d)
- log(3,'get_market_data() returned: {}'.format(repr(ret)))
- if not ret: # kill flag was set
- if quit: break
- continue
- errmsg = 'ticker_loop: HTTP returned bad data'
- with dlock:
- millisec = False
- if g.desc == 'bitfinex':
- a = ret
- bal = 'bid','ask','last_price'
- ts = ret['timestamp']
- elif g.desc == 'gemini':
- a = ret
- bal = 'bid','ask','last'
- try:
- ts = ret['volume']['timestamp']
- except:
- log(1,errmsg); continue
- millisec = True
- elif g.desc in ('okcoin_usd','okcoin_cny','huobi'):
- try:
- a = ret['ticker']
- except:
- log(1,errmsg); continue
- bal = 'buy','sell','last'
- ts = ret[('time','date')[g.desc[:6]=='okcoin']]
- else:
- die(1,"Can't handle symbol '{}'".format(g.desc))
- # okcoin.cn CNY: {"date":"1477932232","ticker":{"buy":"4844.13","high":"4873.36","last":"4844.16","low":"4660.0","sell":"4844.14","vol":"2992115.73393084"}}
- # gemini: {"bid":"1025.64","ask":"1026.93","volume":{"BTC":"1710.8752181914","USD":"1734356.065049020336","timestamp":1486377600000},"last":"1026.93"}
- # okcoin.com USD {"date":"1486581152","ticker":{"buy":"1057.59","high":"1070.0","last":"1059.63","low":"1002.51","sell":"1058.34","vol":"4118.856"}}
- try:
- d.timestamp = int(float(ts))
- except:
- log(1,errmsg); continue
- if millisec: d.timestamp /= 1000
- d.save_bal = d.bal
- try:
- d.bal = tuple([float(a[k]) for k in bal])
- except:
- log(1,errmsg); continue
- log(3,'{}: timestamp {}, bal {}'.format(g.desc,d.timestamp,d.bal))
- if killwait(g,d): break
- def clock_loop():
- ac_active = False
- ac_prefix=(' ','')['alarm_clock_only' in opts]
- def do_ticker(x):
- with dlock:
- blank_big_digits()
- display_ticker(x,called_by_clock=True)
- if kill_flgs['clock'].wait(2): sys.exit()
- while True:
- if 'alarm_clock_only' in opts:
- with dlock:
- blank_ticker()
- display_ac_info()
- elif not ac_active:
- if 'cur_ticker_only' in opts:
- do_ticker(ga)
- else:
- for x in xchgs: do_ticker(x)
- if alrm_clock == 'off' and ac_active:
- ac_active = False
- if alrm_clock != 'off' or 'alarm_clock_only' in opts:
- now = get_hms(no_secs=True)
- if now == alrm_clock:
- ac_active = True
- start_alrm_maybe('alrm_clock',['alrm_clock'])
- if not any([(n in alrm_names and n != 'alrm_clock') for n in get_thrd_names()]):
- bl,rs = (('',''),(BLINK,RESET))[ac_active]
- with dlock:
- blank_big_digits()
- msg_r(CUR_HOME+bl+display_big_digits(now,pre=ac_prefix)+rs)
- park_cursor()
- if kill_flgs['clock'].wait(2): return
- def input_loop():
- global ga,sound_vol,alrm_clock,quit,mute_alrms
- from mmgen.node_tools.Term import get_keypress
- if 'alarm_clock_only' not in opts:
- msg("Type '?' for help after the ticker starts")
- ch = get_keypress("Hit 'q' to quit, any other key to continue: ")
- if ch == 'q': die(0,'')
- if 'resize_window' in opts:
- if tmux:
- msg('\nWARNING: Window resizing doesn\'t work in tmux')
- else:
- msg_r(WIN_RESIZE(lPaneW,topPaneH))
- msg_r(WIN_CORNER())
- time.sleep(0.1)
- from mmgen.term import set_terminal_vars
- set_terminal_vars()
- from mmgen.term import get_terminal_size
- twid = get_terminal_size()[0]
- if twid < lPaneW:
- die_pause(1,'\nTerminal window must be at least %s characters wide' % lPaneW)
- msg_r(CUR_HIDE)
- # raw_input('\nPress any key to exit'); sys.exit() # DEBUG
- blank_ticker()
- for k in xchgs:
- th.Thread(target=globals()['ticker_loop'],args=[k],name=k.desc+'_ticker').start()
- th.Thread(target=clock_loop,name='clock').start()
- th.Thread(target=log_loop,name='log').start()
- time.sleep(1) # Hack for get_keypress()
- def redraw():
- blank_ticker()
- display_ticker(ga)
- help_texts = {
- 'tc': '''
- a - set alarm clock l - set low alarm
- A - set remote audio host m - mute alarms
- e - update USD/CNY rate M - write message to log
- G - set log levels p - set poll interval
- h - set high alarm t - reload ticker
- k - kill alarm v - set sound volume
- L - log current state x - cycle thru exchanges
- c - toggle display of current ticker only
- -/+ - adjust sound volume
- ''',
- 'ac': '''
- a - set alarm clock m - mute alarms
- A - set remote audio host k - kill alarm
- M - write message to log v - set sound volume
- -/+ - adjust sound volume
- '''
- }
- help_text = ['{:^{w}}'.format('KEYSTROKE COMMANDS',w=lPaneW)]
- help_text += help_texts[('tc','ac')['alarm_clock_only' in opts]].strip().split('\n')
- help_text_prompt = 'ESC or ENTER to exit, ↑ and ↓ to scroll'
- def do_help():
- scrollpos = 0
- def fmt_help_txt(pos):
- return '\n'.join(help_text[pos:pos+5])+'\n'+help_text_prompt+' '
- with dlock:
- while True:
- blank_ticker()
- ch = get_keypress(CUR_HOME+fmt_help_txt(scrollpos),esc_sequences=True)
- if ch == CUR_DN1 and scrollpos < len(help_text) - topPaneH + 1:
- scrollpos += 1
- elif ch == CUR_UP1 and scrollpos != 0:
- scrollpos -= 1
- elif ch in '\n\033': break
- blank_ticker()
- display_ticker(ga)
- def ks_msg_nolock(s,delay):
- blank_ticker()
- msg_r(CUR_HOME+s)
- time.sleep(delay)
- blank_ticker()
- display_ticker(ga)
- def ks_msg(s,delay):
- with dlock:
- ks_msg_nolock(s,delay)
- def set_value(v,prompt,vtype=int,poll=False,source=None,
- all_srcs=False,global_var=False,allow_empty=False):
- def type_chk(vtype,val):
- try:
- vtype(val); return True
- except:
- return False
- errmsg = ''
- with dlock:
- while True:
- blank_ticker()
- em = errmsg+'\n' if errmsg else ''
- termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_termattrs)
- # time.sleep(0.1)
- s = raw_input(CUR_HOME+CUR_SHOW+em+prompt)
- msg_r(CUR_HIDE)
- if not s:
- if allow_empty:
- ks_msg_nolock('Value unset',0.5)
- else:
- blank_ticker()
- display_ticker(ga)
- return False
- if v == 'alrm_clock':
- if s == 'off':
- globals()[v] = s
- if v in get_thrd_names():
- kill_flgs['alrm'].set()
- return s
- a = s.split(':')
- ok = True
- if len(a) != 2: ok = False
- if ok:
- try: h,m = int(a[0]),int(a[1])
- except: ok = False
- if ok:
- if not (24 >= h >= 0): ok = False
- if not (60 >= m >= 0): ok = False
- if ok:
- globals()[v] = '{:02d}:{:02d}'.format(h,m)
- break
- else:
- errmsg = 'Incorrect alarm clock value'
- continue
- elif v == 'loglevels':
- ret,err = parse_loglevel_arg(s)
- if ret:
- globals()[v] = ret; break
- else:
- errmsg = err; continue
- elif v == 'poll_secs':
- ret,errmsg = set_poll_intervals(s,xchg=ga)
- if ret: break
- else: continue
- elif all_srcs:
- vals = s.split(',')
- if len(vals) != len(sources):
- errmsg = 'Input must be %s comma-separated %s values'%(
- len(sources),vtype.__name__)
- continue
- for k,val in zip(sources,vals):
- ret = type_chk(vtype,val)
- if ret: setattr(getattr(ga,k),v,vtype(val))
- else: break
- if ret: break
- elif source:
- if type_chk(vtype,s):
- setattr(getattr(ga,source),v,vtype(s)); break
- else:
- if type_chk(vtype,s):
- if v == 'hi_alrm' and vtype(s) < ga.lo_alrm:
- errmsg = 'High alarm must be >= low alarm'
- continue
- if v == 'lo_alrm' and vtype(s) > ga.hi_alrm:
- errmsg = 'Low alarm must be <= high alarm'
- continue
- if global_var:
- globals()[v] = vtype(s); break
- else:
- setattr(ga,v,vtype(s)); break
- errmsg = 'Value%s must be of type %s' % (
- ('','s')[all_srcs],vtype.__name__)
- redraw()
- if poll:
- kill_flgs[ga.desc+'_ticker'].set()
- return True
- while True:
- ch = get_keypress()
- if ch == 'q':
- quit = True
- for k in kill_flgs: kill_flgs[k].set()
- for i in th.enumerate():
- if i.name in main_thrd_names + alrm_names + ('clock',): i.join()
- msg('')
- break
- elif ch == 'a':
- m1 = 'Current alarm clock: %s' % alrm_clock
- m2 = "Enter alarm clock time or 'off': "
- if set_value('alrm_clock',m1+'\n'+m2):
- ks_msg("Alarm clock set to %s" % alrm_clock,1.5)
- elif ch == 'A':
- set_value('audio_host','Enter remote audio host: ',
- vtype=str,global_var=True,allow_empty=True)
- elif ch in 'H?': do_help()
- elif ch == 'k':
- if 'alrm_clock' in get_thrd_names(): alrm_clock = 'off'
- a = any([i in alrm_names for i in get_thrd_names()])
- log(3,'\n alrm_names: {}\n get_thrd_names(): {}'.format(
- alrm_names,get_thrd_names()))
- if a: kill_flgs['alrm'].set()
- ks_msg(('No active alarms','Killing alarm')[a],0.5)
- elif ch == 'M':
- with dlock:
- blank_ticker()
- termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_termattrs)
- s = raw_input(CUR_HOME+'Enter log message: ')
- log(0,'USR_MSG: '+s)
- display_ticker(ga)
- elif ch == 'm':
- mute_alrms = not mute_alrms
- if mute_alrms: kill_flgs['alrm'].set()
- ks_msg(('Unm','M')[mute_alrms] + 'uting alarms',1)
- with dlock:
- blank_ticker()
- display_ticker(ga)
- elif ch == 'v': set_value('sound_vol','Enter sound volume: ',global_var=True)
- elif ch in '-+':
- with dlock:
- sound_vol += (1,-1)[ch=='-']
- blank_ticker(); display_ticker(ga)
- elif not 'alarm_clock_only' in opts:
- if ch == 'c':
- toggle_option('cur_ticker_only')
- ks_msg('Displaying %s' %
- ('all tickers','current ticker only')['cur_ticker_only' in opts],0.5)
- elif ch == 'G': set_value('loglevels','Enter log levels: ',global_var=True)
- elif ch == 'h': set_value('hi_alrm','Enter high alarm: ',Decimal)
- elif ch == 'l': set_value('lo_alrm','Enter low alarm: ',Decimal)
- elif ch == 'L':
- ks_msg('Logging current ticker at %s' % get_hms(),1.5)
- with dlock:
- log(0,'{} CUR_STATE\n BID/ASK/LAST {}\n'.format(ga.desc.upper(),ga.tc.bal))
- elif ch == 'p': set_value('poll_secs',
- 'Enter poll interval: ',float,poll=True)
- elif ch == 't':
- key = '%s_ticker' % (ga.desc)
- if key in kill_flgs:
- kill_flgs[key].set()
- ks_msg('Reloading ticker data',1)
- else:
- ks_msg('%s: no such key' % key,1)
- elif ch == 'x':
- for x in range(len(xchgs)):
- if ga is xchgs[x]: break
- new_g = xchgs[x+1 if x+1 < len(xchgs) else 0]
- ks_msg('Switching to exchange %s' % new_g.Desc,0.5)
- with dlock:
- ga = new_g
- redraw()
- elif ch == 'e':
- for a,b in fx_pairs:
- ks_msg('Updating {}/{} rate'.format(a.upper(),b.upper()),0.5)
- ret = update_fx(a,b)
- m,d = (('Unable to update',2),('Updated',0.7))[bool(ret)]
- ks_msg('{} {}/{} rate'.format(m,a.upper(),b.upper()),d)
- def launch(name,*args,**kwargs):
- def at_exit():
- # msg_r(CUR_HOME+ERASE_ALL) # DEBUG
- msg_r(CUR_SHOW)
- termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_termattrs)
- for k in kill_flgs: kill_flgs[k].set()
- for i in th.enumerate():
- if i.name in main_thrd_names + alrm_names + ('clock',): i.join()
- if 'log' in opts: log(0,'Exiting')
- import atexit
- atexit.register(at_exit)
- log(0,'Starting log')
- try:
- globals()[name](*args,**kwargs)
- except KeyboardInterrupt:
- quit = True
- for k in kill_flgs: kill_flgs[k].set()
- sys.stderr.write('\nUser interrupt\n')
- import termios
- old_termattrs = termios.tcgetattr(sys.stdin.fileno())
- launch('input_loop')
|