From 0dac782b33c324508c734793d51b2d9d05a95ef4 Mon Sep 17 00:00:00 2001 From: Peter Host Date: Mon, 25 Mar 2013 22:56:07 +0100 Subject: [PATCH 01/13] make big asks/bids standout in order book --- goxtool.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/goxtool.py b/goxtool.py index 1ef0324..cf55be7 100755 --- a/goxtool.py +++ b/goxtool.py @@ -54,6 +54,7 @@ ,["book_ask", curses.COLOR_BLACK, curses.COLOR_RED] ,["book_own", curses.COLOR_BLACK, curses.COLOR_YELLOW] ,["book_vol", curses.COLOR_BLACK, curses.COLOR_BLUE] + ,["book_bigvol", curses.COLOR_BLACK, curses.COLOR_MAGENTA] ,["chart_text", curses.COLOR_BLACK, curses.COLOR_WHITE] ,["chart_up", curses.COLOR_BLACK, curses.COLOR_GREEN] @@ -254,8 +255,16 @@ def paint(self): col_bid = COLOR_PAIR["book_bid"] col_ask = COLOR_PAIR["book_ask"] col_vol = COLOR_PAIR["book_vol"] + col_bigvol = COLOR_PAIR["book_bigvol"] col_own = COLOR_PAIR["book_own"] + # get threshold for big transactions from config (if any) + vol_threshold = self.gox.config.get_safe("goxtool", "color_bigvol_threshold") + if len(vol_threshold) > 0: + vol_threshold = float(vol_threshold) + else: + vol_threshold = 0 + # print the asks # pylint: disable=C0301 book = self.gox.orderbook @@ -263,8 +272,13 @@ def paint(self): i = 0 cnt = len(book.asks) while pos >= 0 and i < cnt: + col_thisvol = col_vol + if vol_threshold != 0: + if float(goxapi.int2str(book.asks[i].volume, "BTC")) > vol_threshold: + col_thisvol = col_bigvol + self.addstr(pos, 0, goxapi.int2str(book.asks[i].price, book.gox.currency), col_ask) - self.addstr(pos, 12, goxapi.int2str(book.asks[i].volume, "BTC"), col_vol) + self.addstr(pos, 12, goxapi.int2str(book.asks[i].volume, "BTC"), col_thisvol) ownvol = book.get_own_volume_at(book.asks[i].price) if ownvol: self.addstr(pos, 28, goxapi.int2str(ownvol, "BTC"), col_own) @@ -276,8 +290,13 @@ def paint(self): i = 0 cnt = len(book.bids) while pos < self.height and i < cnt: + col_thisvol = col_vol + if vol_threshold != 0: + if float(goxapi.int2str(book.bids[i].volume, "BTC")) > vol_threshold: + col_thisvol = col_bigvol + self.addstr(pos, 0, goxapi.int2str(book.bids[i].price, book.gox.currency), col_bid) - self.addstr(pos, 12, goxapi.int2str(book.bids[i].volume, "BTC"), col_vol) + self.addstr(pos, 12, goxapi.int2str(book.bids[i].volume, "BTC"), col_thisvol) ownvol = book.get_own_volume_at(book.bids[i].price) if ownvol: self.addstr(pos, 28, goxapi.int2str(ownvol, "BTC"), col_own) From 2a06f54741b4039c367a9eba729046a60c5d37a3 Mon Sep 17 00:00:00 2001 From: prof7bit Date: Sat, 23 Mar 2013 14:50:51 +0100 Subject: [PATCH 02/13] print log message when disconnecting because of timeout --- goxapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/goxapi.py b/goxapi.py index ddb6498..7f750bf 100644 --- a/goxapi.py +++ b/goxapi.py @@ -795,6 +795,7 @@ def send_order_cancel(self, oid): def slot_timer(self, _sender, _data): """request order/lag in regular intervals""" if time.time() - self._time_last_received > 60: + self.debug("did not receive anything for a long time, disconnecting.") self.socket.close() self.connected = False self.request_order_lag() From aafa91c21d2115b4cdbcc917ce6769e9f87f6ec7 Mon Sep 17 00:00:00 2001 From: prof7bit Date: Tue, 26 Mar 2013 11:59:26 +0100 Subject: [PATCH 03/13] socketio-beta is now socketio, the old one is now socketio-old, remove socketio-beta, add socketio-old --- goxapi.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/goxapi.py b/goxapi.py index 7f750bf..de96604 100644 --- a/goxapi.py +++ b/goxapi.py @@ -542,7 +542,7 @@ class BaseClient(BaseObject): """abstract base class for SocketIOClient and WebsocketClient""" SOCKETIO_HOST = "socketio.mtgox.com" - SOCKETIO_HOST_BETA = "socketio-beta.mtgox.com" + SOCKETIO_HOST_OLD = "socketio-old.mtgox.com" WEBSOCKET_HOST = "websocket.mtgox.com" HTTP_HOST = "data.mtgox.com" @@ -987,11 +987,11 @@ def slot_keepalive_timer(self, _sender, _data): self._try_send_raw("2::") -class SocketIOBetaClient(SocketIOClient): +class SocketIOOldClient(SocketIOClient): """experimental client for the beta websocket""" def __init__(self, currency, secret, config): SocketIOClient.__init__(self, currency, secret, config) - self.hostname = self.SOCKETIO_HOST_BETA + self.hostname = self.SOCKETIO_HOST_OLD # pylint: disable=R0902 @@ -1042,8 +1042,8 @@ def __init__(self, secret, config): if use_websocket: self.client = WebsocketClient(self.currency, secret, config) else: - if FORCE_PROTOCOL == "socketio-beta": - self.client = SocketIOBetaClient(self.currency, secret, config) + if FORCE_PROTOCOL == "socketio-old": + self.client = SocketIOOldClient(self.currency, secret, config) else: self.client = SocketIOClient(self.currency, secret, config) From fba0737de27034592c50410f33527d4c724563b1 Mon Sep 17 00:00:00 2001 From: prof7bit Date: Tue, 26 Mar 2013 15:20:51 +0100 Subject: [PATCH 04/13] realtime lag channel instead of polling --- goxapi.py | 21 ++++++++++++--------- goxtool.py | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/goxapi.py b/goxapi.py index de96604..ebe9af5 100644 --- a/goxapi.py +++ b/goxapi.py @@ -609,13 +609,6 @@ def get_nonce(self): self._last_nonce = nonce return nonce - def request_order_lag(self): - """request the current order-lag""" - if FORCE_HTTP_API or self.config.get_bool("gox", "use_http_api"): - self.enqueue_http_request("money/order/lag", {}, "order_lag") - else: - self.send_signed_call("order/lag", {}, "order_lag") - def request_fulldepth(self): """start the fulldepth thread""" @@ -660,6 +653,7 @@ def channel_subscribe(self): self.send(json.dumps({"op":"mtgox.subscribe", "type":"depth"})) self.send(json.dumps({"op":"mtgox.subscribe", "type":"ticker"})) self.send(json.dumps({"op":"mtgox.subscribe", "type":"trades"})) + self.send(json.dumps({"op":"mtgox.subscribe", "type":"lag"})) if FORCE_HTTP_API or self.config.get_bool("gox", "use_http_api"): self.enqueue_http_request("money/orders", {}, "orders") @@ -793,12 +787,11 @@ def send_order_cancel(self, oid): self.send_signed_call(api, params, reqid) def slot_timer(self, _sender, _data): - """request order/lag in regular intervals""" + """check timeout (last received, dead socket?)""" if time.time() - self._time_last_received > 60: self.debug("did not receive anything for a long time, disconnecting.") self.socket.close() self.connected = False - self.request_order_lag() class WebsocketClient(BaseClient): @@ -1197,6 +1190,7 @@ def _on_op_private(self, msg): handler = getattr(self, "_on_op_private_" + private) except AttributeError: self.debug("_on_op_private() ignoring: private=%s" % private) + self.debug(pretty_format(msg)) if handler: handler(msg) @@ -1273,6 +1267,15 @@ def _on_op_private_wallet(self, msg): self.wallet[currency] = total self.signal_wallet(self, ()) + def _on_op_private_lag(self, msg): + """handle the lag message""" + self.order_lag = int(msg["lag"]["age"]) + if self.order_lag < 60000000: + text = "%0.3f s" % (int(self.order_lag / 1000) / 1000.0) + else: + text = "%d s" % (int(self.order_lag / 1000000)) + self.signal_orderlag(self, (self.order_lag, text)) + def _on_op_remark(self, msg): """handler for op=remark messages""" diff --git a/goxtool.py b/goxtool.py index cf55be7..1d206fc 100755 --- a/goxtool.py +++ b/goxtool.py @@ -518,7 +518,7 @@ def paint(self): line2 = "total bid: " + str_fiat + " " + self.gox.currency + " | " line2 += "total ask: " +str_btc + " BTC | " line2 += "ratio: " + str_ratio + " " + self.gox.currency + "/BTC | " - line2 += "order lag: " + self.order_lag_txt + line2 += "lag: " + self.order_lag_txt self.addstr(0, 0, line1, COLOR_PAIR["status_text"]) self.addstr(1, 0, line2, COLOR_PAIR["status_text"]) From 077bbe448de5f254bf1faef06de23ee5a42ce3d6 Mon Sep 17 00:00:00 2001 From: prof7bit Date: Wed, 27 Mar 2013 19:31:34 +0100 Subject: [PATCH 05/13] remove subscription for depth, trade and ticker, its subscribed automatically anyways --- goxapi.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/goxapi.py b/goxapi.py index ebe9af5..58351c1 100644 --- a/goxapi.py +++ b/goxapi.py @@ -650,9 +650,6 @@ def channel_subscribe(self): """subscribe to the needed channels and alo initiate the download of the initial full market depth""" - self.send(json.dumps({"op":"mtgox.subscribe", "type":"depth"})) - self.send(json.dumps({"op":"mtgox.subscribe", "type":"ticker"})) - self.send(json.dumps({"op":"mtgox.subscribe", "type":"trades"})) self.send(json.dumps({"op":"mtgox.subscribe", "type":"lag"})) if FORCE_HTTP_API or self.config.get_bool("gox", "use_http_api"): From 1bcb70f2730c803af6e6275110ce1d79b5cf81bd Mon Sep 17 00:00:00 2001 From: prof7bit Date: Fri, 29 Mar 2013 10:52:44 +0100 Subject: [PATCH 06/13] add README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..eff2cae --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +#goxtool.py + +Goxtool is a trading client for the MtGox Bitcon currency exchange. It is +designed to work in the Linux console (it has a curses user interface). It +can display live streaming market data and you can buy and sell with +keyboard commands. + +Goxtool also has a simple interface to plug in your own automated trading +strategies, your own code can be (re)loded at runtime, it will receive +events from the API and can act upon them. + +The user manual is here: +[http://prof7bit.github.com/goxtool/](http://prof7bit.github.com/goxtool/) + From ab57b57514d2c2d940439aef9c73a677f7205e98 Mon Sep 17 00:00:00 2001 From: prof7bit Date: Fri, 29 Mar 2013 17:24:25 +0100 Subject: [PATCH 07/13] check timer only when connected --- goxapi.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/goxapi.py b/goxapi.py index 58351c1..af99605 100644 --- a/goxapi.py +++ b/goxapi.py @@ -785,10 +785,11 @@ def send_order_cancel(self, oid): def slot_timer(self, _sender, _data): """check timeout (last received, dead socket?)""" - if time.time() - self._time_last_received > 60: - self.debug("did not receive anything for a long time, disconnecting.") - self.socket.close() - self.connected = False + if self.connected: + if time.time() - self._time_last_received > 60: + self.debug("did not receive anything for a long time, disconnecting.") + self.socket.close() + self.connected = False class WebsocketClient(BaseClient): From 58d77586ae464b0b2e1e8fa674dbbfab49d74d1e Mon Sep 17 00:00:00 2001 From: prof7bit Date: Fri, 29 Mar 2013 17:26:38 +0100 Subject: [PATCH 08/13] reset last_received timer immediately after connect --- goxapi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/goxapi.py b/goxapi.py index af99605..22591d5 100644 --- a/goxapi.py +++ b/goxapi.py @@ -816,6 +816,7 @@ def _recv_thread_func(self): self.socket = websocket.WebSocket() self.socket.connect(ws_url) + self._time_last_received = time.time() self.connected = True self.debug("connected, subscribing needed channels") self.channel_subscribe() @@ -932,6 +933,7 @@ def _recv_thread_func(self): self.socket.connect(wsp + self.hostname + "/socket.io/1", query="Currency=" + self.currency) + self._time_last_received = time.time() self.connected = True self.debug("connected") self.socket.send("1::/mtgox") From 923ab8ad7a2687449a0cf79c09799fb55b9c714b Mon Sep 17 00:00:00 2001 From: prof7bit Date: Fri, 29 Mar 2013 17:56:44 +0100 Subject: [PATCH 09/13] small optimization --- goxapi.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/goxapi.py b/goxapi.py index 22591d5..3f71a4d 100644 --- a/goxapi.py +++ b/goxapi.py @@ -1375,16 +1375,19 @@ def slot_ticker(self, dummy_sender, data): self.ask = ask self._repair_crossed_asks(ask) self._repair_crossed_bids(bid) - self.signal_changed(self, ()) + self.signal_changed(self, None) def slot_depth(self, dummy_sender, data): """Slot for signal_depth, process incoming depth message""" (typ, price, _voldiff, total_vol) = data + toa, tob = self.total_ask, self.total_bid if typ == "ask": self._update_asks(price, total_vol) if typ == "bid": self._update_bids(price, total_vol) - self.signal_changed(self, ()) + + if (toa, tob) != (self.total_ask, self.total_bid): + self.signal_changed(self, None) def slot_trade(self, dummy_sender, data): """Slot for signal_trade event, process incoming trade messages. @@ -1424,7 +1427,7 @@ def slot_trade(self, dummy_sender, data): if len(self.bids): self.bid = self.bids[0].price - self.signal_changed(self, ()) + self.signal_changed(self, None) def slot_user_order(self, dummy_sender, data): """Slot for signal_userorder, process incoming user_order mesage""" @@ -1459,7 +1462,7 @@ def slot_user_order(self, dummy_sender, data): "status:", status) self.owns.append(Order(price, volume, typ, oid, status)) - self.signal_changed(self, ()) + self.signal_changed(self, None) def slot_fulldepth(self, dummy_sender, data): """Slot for signal_fulldepth, process received fulldepth data. @@ -1486,7 +1489,7 @@ def slot_fulldepth(self, dummy_sender, data): self.bid = self.bids[0].price self.ask = self.asks[0].price - self.signal_changed(self, ()) + self.signal_changed(self, None) def _repair_crossed_bids(self, bid): """remove all bids that are higher that official current bid value, @@ -1587,7 +1590,7 @@ def have_own_oid(self, oid): def reset_own(self): """clear all own orders""" self.owns = [] - self.signal_changed(self, ()) + self.signal_changed(self, None) def add_own(self, order): """add order to the list of own orders. This method is used From 3aea08e0a3e99452a7d15711f04b1e5b3d226d8f Mon Sep 17 00:00:00 2001 From: Peter Host Date: Mon, 1 Apr 2013 19:04:09 +0200 Subject: [PATCH 10/13] modify action keys --- goxtool.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/goxtool.py b/goxtool.py index 1d206fc..581d2a8 100755 --- a/goxtool.py +++ b/goxtool.py @@ -981,11 +981,14 @@ def curses_loop(stdscr): key = conwin.win.getch() if key == ord("q"): break - if key == curses.KEY_F4: + #if key == curses.KEY_F4: + if key == ord("b"): DlgNewOrderBid(stdscr, gox).modal() - if key == curses.KEY_F5: + #if key == curses.KEY_F5: + if key == ord("s"): DlgNewOrderAsk(stdscr, gox).modal() - if key == curses.KEY_F6: + #if key == curses.KEY_F6: + if key == ord("i"): DlgCancelOrders(stdscr, gox).modal() if key == curses.KEY_RESIZE: stdscr.erase() From 8e585b611e812cf5c4dc15ba40cea18c491a77e9 Mon Sep 17 00:00:00 2001 From: Peter Host Date: Mon, 25 Mar 2013 22:56:07 +0100 Subject: [PATCH 11/13] make big asks/bids standout in order book --- goxtool.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/goxtool.py b/goxtool.py index b12b30c..1d206fc 100755 --- a/goxtool.py +++ b/goxtool.py @@ -54,6 +54,7 @@ ,["book_ask", curses.COLOR_BLACK, curses.COLOR_RED] ,["book_own", curses.COLOR_BLACK, curses.COLOR_YELLOW] ,["book_vol", curses.COLOR_BLACK, curses.COLOR_BLUE] + ,["book_bigvol", curses.COLOR_BLACK, curses.COLOR_MAGENTA] ,["chart_text", curses.COLOR_BLACK, curses.COLOR_WHITE] ,["chart_up", curses.COLOR_BLACK, curses.COLOR_GREEN] @@ -254,8 +255,16 @@ def paint(self): col_bid = COLOR_PAIR["book_bid"] col_ask = COLOR_PAIR["book_ask"] col_vol = COLOR_PAIR["book_vol"] + col_bigvol = COLOR_PAIR["book_bigvol"] col_own = COLOR_PAIR["book_own"] + # get threshold for big transactions from config (if any) + vol_threshold = self.gox.config.get_safe("goxtool", "color_bigvol_threshold") + if len(vol_threshold) > 0: + vol_threshold = float(vol_threshold) + else: + vol_threshold = 0 + # print the asks # pylint: disable=C0301 book = self.gox.orderbook @@ -263,8 +272,13 @@ def paint(self): i = 0 cnt = len(book.asks) while pos >= 0 and i < cnt: + col_thisvol = col_vol + if vol_threshold != 0: + if float(goxapi.int2str(book.asks[i].volume, "BTC")) > vol_threshold: + col_thisvol = col_bigvol + self.addstr(pos, 0, goxapi.int2str(book.asks[i].price, book.gox.currency), col_ask) - self.addstr(pos, 12, goxapi.int2str(book.asks[i].volume, "BTC"), col_vol) + self.addstr(pos, 12, goxapi.int2str(book.asks[i].volume, "BTC"), col_thisvol) ownvol = book.get_own_volume_at(book.asks[i].price) if ownvol: self.addstr(pos, 28, goxapi.int2str(ownvol, "BTC"), col_own) @@ -276,8 +290,13 @@ def paint(self): i = 0 cnt = len(book.bids) while pos < self.height and i < cnt: + col_thisvol = col_vol + if vol_threshold != 0: + if float(goxapi.int2str(book.bids[i].volume, "BTC")) > vol_threshold: + col_thisvol = col_bigvol + self.addstr(pos, 0, goxapi.int2str(book.bids[i].price, book.gox.currency), col_bid) - self.addstr(pos, 12, goxapi.int2str(book.bids[i].volume, "BTC"), col_vol) + self.addstr(pos, 12, goxapi.int2str(book.bids[i].volume, "BTC"), col_thisvol) ownvol = book.get_own_volume_at(book.bids[i].price) if ownvol: self.addstr(pos, 28, goxapi.int2str(ownvol, "BTC"), col_own) From ef31e1f3ba0d7adeffc6f70d3ddd650031473b34 Mon Sep 17 00:00:00 2001 From: Peter Host Date: Mon, 1 Apr 2013 19:04:09 +0200 Subject: [PATCH 12/13] modify action keys --- goxtool.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/goxtool.py b/goxtool.py index 1d206fc..581d2a8 100755 --- a/goxtool.py +++ b/goxtool.py @@ -981,11 +981,14 @@ def curses_loop(stdscr): key = conwin.win.getch() if key == ord("q"): break - if key == curses.KEY_F4: + #if key == curses.KEY_F4: + if key == ord("b"): DlgNewOrderBid(stdscr, gox).modal() - if key == curses.KEY_F5: + #if key == curses.KEY_F5: + if key == ord("s"): DlgNewOrderAsk(stdscr, gox).modal() - if key == curses.KEY_F6: + #if key == curses.KEY_F6: + if key == ord("i"): DlgCancelOrders(stdscr, gox).modal() if key == curses.KEY_RESIZE: stdscr.erase() From 040b48f6f4ba37f943bb0352e7f0f2baf45568dc Mon Sep 17 00:00:00 2001 From: Peter Host Date: Tue, 9 Apr 2013 22:55:16 +0200 Subject: [PATCH 13/13] modify some more action keys --- goxtool.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/goxtool.py b/goxtool.py index 581d2a8..3557850 100755 --- a/goxtool.py +++ b/goxtool.py @@ -624,7 +624,8 @@ def modal(self): self.down(1) if key_pressed == curses.KEY_UP: self.down(-1) - if key_pressed == curses.KEY_IC: + #if key_pressed == curses.KEY_IC: + if key_pressed == curses.KEY_ENTER: self.toggle_select() self.down(1) @@ -642,8 +643,10 @@ class DlgCancelOrders(DlgListItems): """modal dialog to cancel orders""" def __init__(self, stdscr, gox): self.gox = gox - hlp = [("INS", "select"), ("F8", "cancel selected"), ("F10", "exit")] - keys = [(curses.KEY_F8, self._do_cancel)] + #hlp = [("INS", "select"), ("F8", "cancel selected"), ("F10", "exit")] + hlp = [("ENTER", "select"), ("d", "cancel selected"), ("F10", "exit")] + #keys = [(curses.KEY_F8, self._do_cancel)] + keys = [(ord('d'), self._do_cancel)] DlgListItems.__init__(self, stdscr, 45, "Cancel order(s)", hlp, keys) def init_items(self):