#!/usr/bin/python # -*- coding: UTF-8 -*- # # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution # Copyright (C)2013-2016 Philemon # # 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 . # """ 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] [ ]', #-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: :[,:[,…]] 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]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')