From e2b3afb0ba56c8ea6689b458e129648dc2d373ed Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 14:39:01 -0500 Subject: [PATCH 01/52] Working PluginConsole filtering debug messages starting with '[s]', console height and trading fees --- goxtool.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/goxtool.py b/goxtool.py index 2a97e97..5510436 100755 --- a/goxtool.py +++ b/goxtool.py @@ -45,7 +45,7 @@ # HEIGHT_STATUS = 2 -HEIGHT_CON = 7 +HEIGHT_CON = 20 WIDTH_ORDERBOOK = 45 COLORS = [["con_text", curses.COLOR_BLUE, curses.COLOR_CYAN] @@ -284,11 +284,13 @@ def resize(self): def calc_size(self): """put it at the bottom of the screen""" self.height = HEIGHT_CON + self.width = self.termwidth - int(self.termwidth / 2) - 1 self.posy = self.termheight - self.height def slot_debug(self, dummy_gox, (txt)): """this slot will be connected to all debug signals.""" - self.write(txt) + if txt.startswith('[s]') == False: + self.write(txt) def write(self, txt): """write a line of text, scroll if needed""" @@ -319,6 +321,44 @@ def write(self, txt): self.win.addstr("\n" + txt, col) self.done_paint() +class PluginConsole(Win): + """The console window at the bottom""" + def __init__(self, stdscr, gox): + """create the console window and connect it to the Gox debug + callback function""" + self.gox = gox + gox.signal_debug.connect(self.slot_debug) + Win.__init__(self, stdscr) + + def paint(self): + """just empty the window after resize (I am lazy)""" + self.win.bkgd(" ", COLOR_PAIR["con_text"]) + + def resize(self): + """resize and print a log message. Old messages will have been + lost after resize because of my dumb paint() implementation, so + at least print a message indicating that fact into the + otherwise now empty console window""" + Win.resize(self) + self.write("### console has been resized") + + def calc_size(self): + """put it at the bottom of the screen""" + self.height = HEIGHT_CON + self.width = self.termwidth - int(self.termwidth / 2) - 1 + self.posy = self.termheight - self.height + self.posx = self.termwidth - int(self.termwidth / 2) + 1 + + def slot_debug(self, dummy_gox, (txt)): + """this slot will be connected to all plugin debug signals.""" + if (txt.startswith('[s]')): + self.write(txt.replace('[s]', ' ')) + + def write(self, txt): + """write a line of text, scroll if needed""" + self.win.addstr("\n" + txt, COLOR_PAIR["con_text"]) + self.done_paint() + class WinOrderBook(Win): """the orderbook window""" @@ -957,6 +997,7 @@ def paint(self): + goxapi.int2str(self.gox.wallet[currency], currency).strip() \ + " + " line1 = line1.strip(" +") + line1 += " | Fee: " + ("%f" % self.gox.trade_fee) else: line1 += "No info (yet)" @@ -1509,6 +1550,7 @@ def curses_loop(stdscr): printhook = PrintHook(gox) conwin = WinConsole(stdscr, gox) + plugwin = PluginConsole(stdscr, gox) bookwin = WinOrderBook(stdscr, gox) statuswin = WinStatus(stdscr, gox) chartwin = WinChart(stdscr, gox) @@ -1532,6 +1574,7 @@ def curses_loop(stdscr): stdscr.erase() stdscr.refresh() conwin.resize() + plugwin.resize() bookwin.resize() chartwin.resize() statuswin.resize() From a300457e80b9696901847f982e90ecea9d3b5167 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 20:13:40 -0500 Subject: [PATCH 02/52] Add balancer.py, buy.py and sell.py, copy original docs to README and add strategy usage info. --- README.md | 172 +++++++++++++++++++++++++++++++++++++++- balancer.py | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++ buy.py | 175 +++++++++++++++++++++++++++++++++++++++++ sell.py | 174 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 738 insertions(+), 4 deletions(-) create mode 100644 balancer.py create mode 100644 buy.py create mode 100644 sell.py diff --git a/README.md b/README.md index eff2cae..32a0ac8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#goxtool.py +# Goxtool 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 @@ -6,9 +6,173 @@ 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 +strategies, your own code can be reloaded at runtime, 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/) +#### Donations appreciated +prof7bit (goxtool): 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW +caktux (mods): 18zX3wb318o2Pw9ZUHgG3mmQME536Qg2Ha + + +## Installation + +Open a terminal in an empty folder or in the folder you usually use to clone repositories and clone the master branch: + +```git clone git://github.com/USERNAME/goxtool.git``` + +This will create a folder named goxtool containing all the needed files. Thats all, now it is installed and ready to use. You can now already watch live market data (without any trading functions being enabled), you can later add a MtGox API-key to it to have full access to your account but for now just proceed to the next step, start it without an account, just to make sure everything works. + + +## Usage + +Change into to the goxtool folder that was created in the previous step and start goxtool.py: + +cd goxtool + ./goxtool.py + +Keyboard commands (only the ones useful in view-only mode, without Mt.Gox account): + + `q` quit + `l` (lower case "L") reload the strategy module (see advanced usage) + `D` (shift + d) switch to depth chart view + `H` (shift + h) switch to candlestick history chart view + `S` (shift + s) toggle summing up the volume of order book levels on/off + `T` (shift + t) toggle summing up the volume in the depth chart on/off + `-` order book zoom out (increase group size) + `+` order book zoom in (decrease group size) + `,` depth chart zoom out (increase group size) + `.` depth chart zoom in (decrease group size) + +(There will be even more commands once you connect it to your Mt.Gox account) + +There is also a goxtool.ini file, it will be created on the first start. In the .ini file there are some parameters you can change, for example the currency you want to trade BTC against or some parameters regarding the network protocol. Some of the .ini settings can be overridden by command line options (use the --help option to see a list). The default protocol is websocket, the alternative would be socketio, goxtool implements both protocols, the websocket server is currently more reliable. There is also an option to use the http API for trading commands, the default is to send all commands to the streaming socket, this affects only what happens when you buy/sell/cancel, it does not affect the streaming update of order book and chart. +socketio or websocket? Which one is worse? + +The two options are socketio or websocket, the .ini setting for this is use_plain_old_websocket. To force it connecting with socketio: + +```./goxtool.py --protocol=socketio``` + + To force it connecting to the websocket server do this: + +```./goxtool.py --protocol=websocket``` + +It has turned out that websocket is currently the most reliable protocol. These options on the command line take precedence over what you have configured in the .ini file (but it won't change your .ini), you can make websocket the default (so you don't need this option anymore) by editing the ini file. + +If you experience a high lag between sending an order and the ack (the op:result) not appearing immediately which is happening during times of really high volume then you should consider also adding --use-http to make it send trading commands via http. Http makes it slightly slower to execute many trades in a row but has been much more reliable when mtgox was under heavy load or ddos or similar. If you don't need to send many orders very fast then this option won't hurt. --use-http can be combined with either socketio or websocket. + +The following is what I am currently using and I recommend it: + +```./goxtool.py --protocol=websocket --use-http``` + + +## Trading with your MtGox account + +First you will need to add an API key in MtGox, then do the following: + +```./goxtool.py --add-secret``` + +This will now ask you for your key, secret and a password (not your MtGox one) to secure those on your drive. The key and secret belong to a shared secret that is created by MtGox to authenticate your trading software against their API. You can request as many keys from MtGox as you need, every application you connect to your MtGox account should have its own key, you can also at any time delete the keys again that you no longer need. + +If you need a Key/Secret pair for goxtool, open your web browser, log in to your MtGox account, click on "Security Center", click on "Advanced API Key Creation", choose a name for your key (every key will have a name so you can later easily tell them apart), make sure you check at least the boxes "Get Info" and "Trade" (this is what the application will be allowed to do with this key) and then finally click on "Create Key". + +Now MtGox will create 2 strings of cryptic numbers and letters, the "API-Key" and the "Secret". Now copy/paste the API-Key into the terminal where it asked you for "Key", press enter, now it will ask you for "Secret", copy/paste the Secret into the terminal, press enter and now it will ask you for a passphrase. It is important to understand what is going on here. The Key/Secret from above must under no circumstances ever come into the wrong hands, therefore goxtool won't just store them in the .ini file, goxtool will encrypt it and thats what the passphrase is needed for. Choose a secure passphrase (it will ask you twice to make sure there is no typo), you will also not see anything in the console (not even "*") while typing, this is not a bug, this is intentional. Choose a strong passphrase, type it into the terminal, press enter, repeat the passphrase, press enter again and now it will tell you that it has been encrypted and saved to the .ini file and exit. + +Now start goxtool again: + +```./goxtool.py``` + +Which will ask: +```enter passphrase for secret:``` + +From now on every time you start goxtool it will ask you for the passphrase in order to be able to decrypt and use the secret. Enter your passphrase, press enter. Now goxtool will start and you will notice that now it is showing your account balance at the top of the window. Now all trading functions are enabled. + +Keyboard commands for trading: + + * `F4` : New buy order + * `F5` : New sell order + * `F6` : View orders / cancel order(s) +In the cancel dialog you can move up/down with the arrow keys, use INS to select/unselect orders (you can select multiple orders and cancel them all at once) or if you just quickly want to cancel only one order just highlight to the order and hit F8. It behaves a little bit like deleting files in midnight commander. + +When entering a new order you can move between the fields with up/down keys or move to the next field with tab or enter (but only if you entered a valid number into the previous field, decimal separator is . (not comma, even on European computers), send the order with enter. + +All dialogs can be closed with `F10` or `ESC`. + + +## Strategy modules + +Running all strategies: + +```./goxtool.py --strategy=balancer.py,buy.py,sell.py``` + + +#### Balancer + +Portfolio rebalancing bot that will buy and sell to maintain a constant asset allocation ratio of exactly 50/50 = fiat/BTC. + + * `i` for information (how much currently out of balance) + * `r` to rebalance with market order at current price (required before rebalancing) + * `p` to add initial rebalancing orders + * `c` to cancel all rebalancing orders + * `u` to update account information, order list and wallet + +```./goxtool.py --strategy=balancer.py``` + +#### Buy strategy + +Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` above your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. + + * `b` to see Buy objective + * `o` to see Buy order book + +```./goxtool.py --strategy=buy.py``` + +#### Sell strategy + +Sell strategy module. Set `sell_level` at the price you want to sell, `threshold` below your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. + + * `s` to see Sell objective + * `k` to see Sell order book + +```./goxtool.py --strategy=sell.py``` + +#### Making your own + +You can write your own trading bots. There is a file named `strategy.py`, it contains a class Strategy() which constitutes a trading bot that by default does nothing (its an empty skeleton). It has event methods (slots) connected to signals that will be fired when certain events occur. From within these methods you can then do arbitrary stuff (peek around in gox.orderbook to see where bids and asks are located, call gox.buy(), gox.sell() or gox.cancel() methods to build a fully automated trading bot or you can use the key press slot (it will be called on all letter keys except l and q) to build a semi-automatic bot that reacts to key presses or to influence parameters of your bot or anything else you can imagine. Examples of simple bots will soon follow. + +If you decide to make serious use of this then please create a new python file for your strategy. either make a copy of the default strategy.py skeleton or make a module that imports strategy and has a class Strategy(strategy.Strategy), give this module file a different name and leave strategy.py alone so it won't collide with upstream changes you pull from github. By default goxtool will load strategy.py but you can start it with the --strategy command line option to specify your own strategy module or a comma separated list of many modules: + +```./goxtool --strategy=mybot.py,myotherbot.py``` + +You can even edit the strategy while goxtool is running and then reload it at runtime (this can be very useful), just press the l key (lowercase L) and it will do the following things: + +* emit signal _strategy_unload, this will call slot_before_unload() +* free the currently running instance of Strategy() (your __del__() method should be called) +* re-import the changed module file +* create a new instance of Strategy() and call your__init__() again. + +You should persist the state of your bot (if needed) in slot_before_unload() and reload it in __init__(). Leave the __del__() method alone, its only there to print a log message to debug proper unloading! + +Please make sure that you can see the debug output from the __del__() method in the log when the strategy is reloading, you must be sure its able to free and garbage collect your strategy! If you instantiate any circular references, even something as innocent as a double linked list or even just an object holding a reference to the strategy then this will effectively keep python from being able to garbage-collect it and hold it in memory indefinitely (and keep sending it signals!). + +Use the slot_before_unload() method to del everything in your strategy that might hold any circular references. You can check that it works if you see the debug output of __del__() in the log scrolling by when you press l to reload it, the fact that __del__() was called is proof that it was properly garbage-collected. + +Trading functions do NOT block, this means they also won't return the MtGox order ID, you need to find your own way of remembering which orders you have sent already. A few moments (seconds or minutes) after you have sent them they will be acked by MtGox and it will fire orderbook.signal_changed()and when this happens you will find it in the gox.orderbook.owns list and it will have an official order ID. I know this is not optimal (because this part of the code is not yet complete, eventually there will be dedicated signals to notify your bot about the results of trading commands) and also this document is not yet a complete documentation. If you really want to dive into this: use the source, Luke. +How to keep it up to date + +Occasionally I will commit bugfixes, improvements, etc. To update your copy of goxtool (assuming you previously installed it with git clone and not by just downloading a zip file) do the following: + +```git pull``` + +and if that complains because of local uncommitted changes because you edited the strategy.py module or did other changes to the code then try this: + +``` +git stash +git pull +git stash pop +``` + +Of course you could have also have followed my previous advise to not do anything other than simple throw-away experiments in strategy.py so you can always `git reset --hard` if everything else fails and use a separate file (and probably even separate git branches) for serious bot development but this is outside the scope of this document, there exist specialized howtos for git and github elsewhere. + +Original user manual is here: +[http://prof7bit.github.com/goxtool/](http://prof7bit.github.com/goxtool/) diff --git a/balancer.py b/balancer.py new file mode 100644 index 0000000..b33155a --- /dev/null +++ b/balancer.py @@ -0,0 +1,221 @@ +""" +The portfolio rebalancing bot will buy and sell to maintain a +constant asset allocation ratio of exactly 50/50 = fiat/BTC +""" + +import strategy + +DISTANCE = 7 # percent price distance of next rebalancing orders +FIAT_COLD = 0 # Amount of Fiat stored at home but included in calculations +COIN_COLD = 0 # Amount of Coin stored at home but included in calculations + +MARKER = 7 # lowest digit of price to identify bot's own orders +COIN = 1E8 # number of satoshi per coin, this is a constant. + +def add_marker(price, marker): + """encode a marker in the price value to find bot's own orders""" + return price / 10 * 10 + marker + +def has_marker(price, marker): + """return true if the price value has the marker""" + return (price % 10) == marker + +def mark_own(price): + """return the price with our own marker embedded""" + return add_marker(price, MARKER) + +def is_own(price): + """return true if this price has our own marker""" + return has_marker(price, MARKER) + + +class Strategy(strategy.Strategy): + """a portfolio rebalancing bot""" + def __init__(self, gox): + strategy.Strategy.__init__(self, gox) + self.temp_halt = False + self.name = "%s.%s" % (__name__, self.__class__.__name__) + self.debug("[s]%s loaded" % self.name) + self.debug("[s]Press 'i' for information (how much currently out of balance)\n Press 'r' to rebalance with market order at current price (required before rebalancing)\n Press 'p' to add initial rebalancing orders\n Press 'c' to cancel all rebalancing orders\n Press 'u' to update account information, order list and wallet") + + def __del__(self): + try: + self.debug("[s]%s unloaded" % self.name) + except Exception, e: + self.debug("[s]%s exception: %s" % (self.name, e)) + + def slot_keypress(self, gox, (key)): + """a key has been pressed""" + + if key == ord("c"): + # cancel existing rebalancing orders and suspend trading + self.debug("[s]canceling all rebalancing orders") + self.temp_halt = True + self.cancel_orders() + + if key == ord("p"): + # create the initial two rebalancing orders and start trading. + # Before you do this the portfolio should already be balanced. + # use "i" to show current status and "b" to rebalance with a + # market order at current price. + self.debug("[s]adding new initial rebalancing orders") + self.temp_halt = False + self.place_orders() + + if key == ord("u"): + # update the own order list and wallet by forcing what + # normally happens only after reconnect + gox.client.channel_subscribe(False) + + if key == ord("i"): + # print some information into the log file about + # current status (how much currently out of balance) + price = (gox.orderbook.bid + gox.orderbook.ask) / 2 + vol_buy = self.get_buy_at_price(price) + price_balanced = self.get_price_where_it_was_balanced() + self.debug("[s]BTC difference at current price:", + gox.base2float(vol_buy)) + self.debug("[s]Price where it would be balanced:", + gox.quote2float(price_balanced)) + + if key == ord("r"): + # manually rebalance with market order at current price + price = (gox.orderbook.bid + gox.orderbook.ask) / 2 + vol_buy = self.get_buy_at_price(price) + if abs(vol_buy) > 0.01 * COIN: + self.temp_halt = True + self.cancel_orders() + if vol_buy > 0: + self.debug("[s]buy %f at market" % + gox.base2float(vol_buy)) + gox.buy(0, vol_buy) + else: + self.debug("[s]sell %f at market" % + gox.base2float(-vol_buy)) + gox.sell(0, -vol_buy) + + def cancel_orders(self): + """cancel all rebalancing orders, we identify + them through the marker in the price value""" + must_cancel = [] + for order in self.gox.orderbook.owns: + if is_own(order.price): + must_cancel.append(order) + + for order in must_cancel: + self.gox.cancel(order.oid) + + def get_price_where_it_was_balanced(self): + """get the price at which it was perfectly balanced, given the current + BTC and Fiat account balances. Immediately after a rebalancing order was + filled this should be pretty much excactly the price where the order was + filled (because by definition it should be quite exactly balanced then), + so even after missing the trade message due to disconnect it should be + possible to place the next 2 orders precisely around the new center""" + gox = self.gox + fiat_have = gox.quote2float(gox.wallet[gox.curr_quote]) + FIAT_COLD + btc_have = gox.base2float(gox.wallet[gox.curr_base]) + COIN_COLD + return gox.quote2int(fiat_have / btc_have) + + def get_buy_at_price(self, price_int): + """calculate amount of BTC needed to buy at price to achieve + rebalancing. price and return value are in mtgox integer format""" + gox = self.gox + fiat_have = gox.quote2float(gox.wallet[gox.curr_quote]) + FIAT_COLD + btc_have = gox.base2float(gox.wallet[gox.curr_base]) + COIN_COLD + price_then = gox.quote2float(price_int) + + btc_value_then = btc_have * price_then + diff = fiat_have - btc_value_then + diff_btc = diff / price_then + must_buy = diff_btc / 2 + return self.gox.base2int(must_buy) + + def place_orders(self): + """place two new rebalancing orders above and below center price""" + center = self.get_price_where_it_was_balanced() + self.debug( + "[s]center is %f" % self.gox.quote2float(center)) + step = int(center * DISTANCE / 100.0) + next_sell = mark_own(center + step) + next_buy = mark_own(center - step) + + sell_amount = -self.get_buy_at_price(next_sell) + buy_amount = self.get_buy_at_price(next_buy) + + if sell_amount < 0.01 * COIN: + sell_amount = int(0.01 * COIN) + self.debug("WARNING! minimal sell amount adjusted to 0.01") + + if buy_amount < 0.01 * COIN: + buy_amount = int(0.01 * COIN) + self.debug("WARNING! minimal buy amount adjusted to 0.01") + + self.debug("[s]new buy order %f at %f" % ( + self.gox.base2float(buy_amount), + self.gox.quote2float(next_buy) + )) + self.gox.buy(next_buy, buy_amount) + + self.debug("[s]new sell order %f at %f" % ( + self.gox.base2float(sell_amount), + self.gox.quote2float(next_sell) + )) + self.gox.sell(next_sell, sell_amount) + + def slot_trade(self, gox, (date, price, volume, typ, own)): + """a trade message has been receivd""" + # not interested in other people's trades + if not own: + return + + # not interested in manually entered (not bot) trades + if not is_own(price): + return + + text = {"bid": "sold", "ask": "bought"}[typ] + self.debug("[s]*** %s %f at %f" % ( + text, + gox.base2float(volume), + gox.quote2float(price) + )) + self.check_trades() + + def slot_owns_changed(self, orderbook, _dummy): + """status or amount of own open orders has changed""" + self.check_trades() + + def check_trades(self): + """find out if we need to place new orders and do it if neccesary""" + + # bot temporarily disabled + if self.temp_halt: + return + + # still waiting for submitted orders, + # can wait for next signal + if self.gox.count_submitted: + return + + # we count the open and pending orders + count = 0 + count_pending = 0 + book = self.gox.orderbook + for order in book.owns: + if is_own(order.price): + if order.status == "open": + count += 1 + else: + count_pending += 1 + + # as long as there are ANY pending orders around we + # just do nothing and wait for the next signal + if count_pending: + return + + # if count is exacty 1 then one of the orders must have been filled, + # now we cancel the other one and place two fresh orders in the + # distance of DISTANCE around center price. + if count == 1: + self.cancel_orders() + self.place_orders() diff --git a/buy.py b/buy.py new file mode 100644 index 0000000..dbd7435 --- /dev/null +++ b/buy.py @@ -0,0 +1,175 @@ +""" +trading robot - buy BTC + +save this file in the same folder as 'goxtool.py' as 'buy.py' +to load this strategy execute 'goxtool.py' with the --strategy option: + +$ ./goxtool.py --strategy buy.py + +You can make changes to this file whilst 'goxtool.py' is running. +Dynamically reload() buy pressing the 'l' key in the goxtool terminal +Other keypresses are defined in the 'slot_keypress' function below. + +Activate this strategy's BUY functionality by switching 'simulate' to False +Test first before enabling the BUY function! + +Note: the goxtool.py application swallows most Python exceptions +and outputs them to the status window and goxtool.log (in app folder). +This complicates tracing of runtime errors somewhat, but +to keep an eye on such it is recommended that the developer runs +an additional terminal with 'tail -f ./goxtool.log' to see +continuous logfile output. + +coded by tarzan (c) April 2013, modified by caktux +copying & distribution allowed - attribution appreciated +""" + +import goxapi + +# Simulate +simulate = True + +# Live or simulation notice +simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') + +# variables +global bidbuf, askbuf # comparators to avoid redundant bid/ask output +bidbuf = 0 +askbuf = 0 +buy_level = float(1) # price at which you want to buy BTC +threshold = float(100) # alert price distance from buy_level +buy_alert = float(buy_level + threshold) # alert level for user info +volume = float(1) # user specified fiat amout as volume, set 0 to use wallet full fiat balance + +class Strategy(goxapi.BaseObject): + # pylint: disable=C0111,W0613,R0201 + + def __init__(self, gox): + goxapi.BaseObject.__init__(self) + self.signal_debug.connect(gox.signal_debug) + gox.signal_keypress.connect(self.slot_keypress) + gox.signal_strategy_unload.connect(self.slot_before_unload) + gox.signal_ticker.connect(self.slot_tick) + gox.signal_depth.connect(self.slot_depth) + gox.signal_trade.connect(self.slot_trade) + gox.signal_userorder.connect(self.slot_userorder) + gox.orderbook.signal_owns_changed.connect(self.slot_owns_changed) + gox.signal_wallet.connect(self.slot_wallet_changed) + self.gox = gox + self.name = "%s.%s" % (__name__, self.__class__.__name__) + self.debug("[s]%s loaded" % self.name) + self.debug("[s]Press 'b' to see Buy objective\n Press 'o' to see Buy order book") + #get existing orders for later decision making + self.existingorders = [] + for order in self.gox.orderbook.owns: + self.existingorders.append(order.oid) + + def __del__(self): + try: + self.debug("[s]%s unloaded" % self.name) + except Exception, e: + self.debug("[s]%s exception: %s" % (self.name, e)) + + def slot_before_unload(self, _sender, _data): + self.debug("[s]%s before unload" % self.name) + + def slot_keypress(self, gox, (key)): + # some custom keypresses are caught here: + # 'b' outputs the strategy objective to the status window & log + # 'o' displays own orders + # self.debug("someone pressed the %s key" % chr(key)) + global buy_amount + if key == ord('b'): + self.debug("[s]%sObjective: BUY Bitcoins for %f %s when price reaches %f" % (simulate_or_live, buy_amount, str(self.gox.orderbook.gox.currency), buy_level)) + # self.debug("[s]Python wallet object: %s" % str(self.gox.wallet)) + # check if the user changed volume + # also ensure the buy_amount does not exceed wallet balance + # if it does, set buy_amount to wallet full fiat balance + walletbalance = goxapi.int2float(self.gox.wallet[self.gox.orderbook.gox.currency], self.gox.orderbook.gox.currency) + if volume == 0: + buy_amount = walletbalance + else: + buy_amount = volume + # if volume != 0 and volume <= walletbalance: + # if buy_amount != volume: + # buy_amount = volume + # else: + # buy_amount = walletbalance + # else: + # buy_amount = walletbalance + self.debug("[s] %sstrategy will spend %f of %f %s on next BUY" % (simulate_or_live, buy_amount, walletbalance, str(self.gox.orderbook.gox.currency))) + elif key == ord('o'): + self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) + for order in self.gox.orderbook.owns: + self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (goxapi.int2float(order.price, gox.orderbook.gox.currency), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) + + def slot_tick(self, gox, (bid, ask)): + global bidbuf, askbuf, buy_amount + # if goxapi receives a no-change tick update, don't output anything + if bid != bidbuf or ask != askbuf: + seen = 0 # var seen is a flag for default output below (=0) + self.ask = goxapi.int2float(ask, self.gox.orderbook.gox.currency) + if self.ask > buy_level and self.ask < buy_alert: + self.debug("[s] !!! buy ALERT @ %s; ask currently at %s" % (str(buy_alert), str(self.ask))) + self.debug("[s] !!! BUY for %f %s will trigger @ %f" % (buy_amount,str(self.gox.orderbook.gox.currency), buy_level)) + seen = 1 + elif self.ask <= buy_level: + # this is the condition to action gox.buy() + if simulate == False: + self.gox.buy(self.ask, buy_amount) + self.debug("[s] >>> %sBUY BTC @ %s; ask currently at %s" % (simulate_or_live, str(buy_level), str(self.ask))) + seen = 1 + if seen == 0: + # no conditions met above, so give the user default info + self.debug("Buy level @ %s (alert: %s); ask @ %s" % (str(buy_level), str(buy_alert), str(self.ask))) + # is the updated tick different from previous? + if bid != bidbuf: + bidbuf = bid + elif ask != askbuf: + askbuf = ask + + def slot_depth(self, gox, (typ, price, volume, total_volume)): + pass + + def slot_trade(self, gox, (date, price, volume, typ, own)): + """a trade message has been received. Note that this might come + before the orderbook.owns list has been updated, don't rely on the + own orders and wallet already having been updated when this fires.""" + # trade messages include trades by other traders + # if own == True then it is your own + if str(own) == 'True': + self.debug("own trade message received: date %s price %s volume %s typ %s own %s" % (str(date), str(price), str(volume), str(typ), str(own))) + + def slot_userorder(self, gox, (price, volume, typ, oid, status)): + """this comes directly from the API and owns list might not yet be + updated, if you need the new owns list then use slot_owns_changed""" + # the coder assumes that if an order id is received via + # this signal then it was not instantaneously actioned, so cancel + # at once + self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (goxapi.int2float(price, self.gox.orderbook.gox.currency), str(volume), str(typ), str(oid), str(status))) + # cancel by oid + if status not in ['pending','executing','post-pending','removed'] and oid not in self.existingorders: + if goxapi.int2float(price, self.gox.orderbook.gox.currency) == buy_level: + self.gox.cancel(oid) + + def slot_owns_changed(self, orderbook, _dummy): + """this comes *after* userorder and orderbook.owns is updated already""" + pass + + def slot_wallet_changed(self, gox, _dummy): + """this comes after the wallet has been updated""" + # buy_amount can either be manually specified or + # this strategy will query the user wallet and buy BTC using the + # FULL fiat (e.g. USD) balance + # changes to wallet balance should be picked up here - press 'w' + # to confirm. Else, restart goxtool to reload wallet + # also ensure the buy_amount does not exceed wallet balance + # if it does, set buy_amount to wallet full fiat balance + global buy_amount + walletbalance = goxapi.int2float(self.gox.wallet[self.gox.orderbook.gox.currency], self.gox.orderbook.gox.currency) + if volume != 0 and volume <= walletbalance: + buy_amount = volume + else: + buy_amount = walletbalance + +#end diff --git a/sell.py b/sell.py new file mode 100644 index 0000000..8f15077 --- /dev/null +++ b/sell.py @@ -0,0 +1,174 @@ +""" +trading robot - sell BTC + +save this file in the same folder as 'goxtool.py' as 'sell.py' +to load this strategy execute 'goxtool.py' with the --strategy option: + +$ ./goxtool.py --strategy sell.py + +You can make changes to this file whilst 'goxtool.py' is running. +Dynamically reload() buy pressing the 'l' key in the goxtool terminal +Other keypresses are defined in the 'slot_keypress' function below. + +Activate this strategy's SELL functionality by switching 'simulate' to False +Test first before enabling the SELL function! + +Note: the goxtool.py application swallows most Python exceptions +and outputs them to the status window and goxtool.log (in app folder). +This complicates tracing of runtime errors somewhat, but +to keep an eye on such it is recommended that the developer runs +an additional terminal with 'tail -f ./goxtool.log' to see +continuous logfile output. + +coded by tarzan (c) April 2013, modified by caktux +copying & distribution allowed - attribution appreciated +""" + +import goxapi + +# Simulate +simulate = True + +# Live or simulation notice +simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') + +# variables +global bidbuf, askbuf # comparators to avoid redundant bid/ask output +bidbuf = 0 +askbuf = 0 +sell_level = float(10000000) # price at which you want to sell BTC +threshold = float(100000) # alert price distance from sell_level +sell_alert = float(sell_level - threshold) # alert level for user info +volume = float(0.1) # user specified BTC volume, set 0 to sell all BTC + +class Strategy(goxapi.BaseObject): + # pylint: disable=C0111,W0613,R0201 + + def __init__(self, gox): + goxapi.BaseObject.__init__(self) + self.signal_debug.connect(gox.signal_debug) + gox.signal_keypress.connect(self.slot_keypress) + gox.signal_strategy_unload.connect(self.slot_before_unload) + gox.signal_ticker.connect(self.slot_tick) + gox.signal_depth.connect(self.slot_depth) + gox.signal_trade.connect(self.slot_trade) + gox.signal_userorder.connect(self.slot_userorder) + gox.orderbook.signal_owns_changed.connect(self.slot_owns_changed) + gox.signal_wallet.connect(self.slot_wallet_changed) + self.gox = gox + self.name = "%s.%s" % (__name__, self.__class__.__name__) + self.debug("[s]%s loaded" % self.name) + self.debug("[s]Press 's' to see Sell objective\n Press 'k' to see Sell order book") + #get existing orders for later decision making + self.existingorders = [] + for order in self.gox.orderbook.owns: + self.existingorders.append(order.oid) + + def __del__(self): + try: + self.debug("[s]%s unloaded" % self.name) + except Exception, e: + self.debug("[s]%s exception: %s" % (self.name, e)) + + def slot_before_unload(self, _sender, _data): + self.debug("[s]%s before unload" % self.name) + + def slot_keypress(self, gox, (key)): + # some custom keypresses are caught here: + # 's' outputs the strategy objective to the status window & log + # 'k' displays own orders + # self.debug("someone pressed the %s key" % chr(key)) + global sell_amount + if key == ord('s'): + self.debug("[s]%sObjective: SELL %f BTC when price reaches %f" % (simulate_or_live, sell_amount, sell_level )) + # self.debug("[s]Python wallet object: %s" % str(self.gox.wallet)) + # check if the user changed volume + # also ensure the buy_amount does not exceed wallet balance + # if it does, set sell_amount to wallet full BTC balance + walletbalance = goxapi.int2float(self.gox.wallet['BTC'], 'BTC') + if volume == 0: + sell_amount = walletbalance + else: + sell_amount = volume + # if volume != 0 and volume <= walletbalance: + # if sell_amount != volume: + # sell_amount = volume + # else: + # sell_amount = walletbalance + # else: + # sell_amount = walletbalance + self.debug("[s] %sstrategy will sell %f of %f BTC on next SELL" % (simulate_or_live, sell_amount, walletbalance)) + elif key == ord('k'): + self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) + for order in self.gox.orderbook.owns: + self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (goxapi.int2float(order.price, gox.orderbook.gox.currency), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) + + def slot_tick(self, gox, (bid, ask)): + global bidbuf, askbuf, sell_amount + # if goxapi receives a no-change tick update, don't output anything + if bid != bidbuf or ask != askbuf: + seen = 0 # var seen is a flag for default output below (=0) + self.bid = goxapi.int2float(bid, gox.orderbook.gox.currency) + if self.bid < sell_level and self.bid > sell_alert: + self.debug("[s] !!! SELL ALERT @ %s; bid currently at %s" % (str(sell_alert), str(self.bid))) + self.debug("[s] !!! SELL for %f BTC will trigger @ %f" % (sell_amount, sell_level)) + seen = 1 + elif self.bid >= sell_level: + # this is the condition to action gox.sell() + if simulate == False: + self.gox.sell(self.bid, sell_amount) + self.debug("[s] >>> %sSELL BTC @ %s; bid currently at %s" % (simulate_or_live, str(sell_level), str(self.bid))) + seen = 1 + if seen == 0: + # no conditions met above, so give the user default info + self.debug("Sell level @ %s (alert: %s); bid @ %s" % (str(sell_level), str(sell_alert), str(self.bid))) + # is the updated tick different from previous? + if bid != bidbuf: + bidbuf = bid + elif ask != askbuf: + askbuf = ask + + def slot_depth(self, gox, (typ, price, volume, total_volume)): + pass + + def slot_trade(self, gox, (date, price, volume, typ, own)): + """a trade message has been received. Note that this might come + before the orderbook.owns list has been updated, don't rely on the + own orders and wallet already having been updated when this fires.""" + # trade messages include trades by other traders + # if own == True then it is your own + if str(own) == 'True': + self.debug("own trade message received: date %s price %s volume %s typ %s own %s" % (str(date), str(price), str(volume), str(typ), str(own))) + + def slot_userorder(self, gox, (price, volume, typ, oid, status)): + """this comes directly from the API and owns list might not yet be + updated, if you need the new owns list then use slot_owns_changed""" + # the coder assumes that if an order id is received via + # this signal then it was not instantaneously actioned, so cancel + # at once + self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (goxapi.int2float(price, self.gox.orderbook.gox.currency), str(volume), str(typ), str(oid), str(status))) + # cancel by oid + if status not in ['pending','executing','post-pending','removed'] and oid not in self.existingorders: + if goxapi.int2float(price, self.gox.orderbook.gox.currency) == sell_level: + self.gox.cancel(oid) + + def slot_owns_changed(self, orderbook, _dummy): + """this comes *after* userorder and orderbook.owns is updated already""" + pass + + def slot_wallet_changed(self, gox, _dummy): + """this comes after the wallet has been updated""" + # sell_amount can either be manually specified or + # this strategy will query the user wallet and sell ALL Bitcoins + # changes to wallet balance should be picked up here - press 'w' + # to confirm. Else, restart goxtool to reload wallet + # also ensure the buy_amount does not exceed wallet balance + # if it does, set sell_amount to wallet full BTC balance + global sell_amount + walletbalance = goxapi.int2float(self.gox.wallet['BTC'], 'BTC') + if volume != 0 and volume <= walletbalance: + sell_amount = volume + else: + sell_amount = walletbalance + +#end From 616c18822562caee15dcbc6d09b473efcb8ea86a Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 20:16:13 -0500 Subject: [PATCH 03/52] h5 on donations title... --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32a0ac8..e289806 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Goxtool also has a simple interface to plug in your own automated trading strategies, your own code can be reloaded at runtime, will receive events from the API and can act upon them. -#### Donations appreciated +##### Donations appreciated prof7bit (goxtool): 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW caktux (mods): 18zX3wb318o2Pw9ZUHgG3mmQME536Qg2Ha From 8637a6622b757bff55a4665021241ec4db9b1106 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 20:17:38 -0500 Subject: [PATCH 04/52] and at the bottom --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e289806..6d074c3 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,6 @@ Goxtool also has a simple interface to plug in your own automated trading strategies, your own code can be reloaded at runtime, will receive events from the API and can act upon them. -##### Donations appreciated -prof7bit (goxtool): 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW -caktux (mods): 18zX3wb318o2Pw9ZUHgG3mmQME536Qg2Ha - ## Installation @@ -176,3 +172,9 @@ Of course you could have also have followed my previous advise to not do anythin Original user manual is here: [http://prof7bit.github.com/goxtool/](http://prof7bit.github.com/goxtool/) + +##### Donations appreciated + +prof7bit (goxtool): 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW + +caktux (mods): 18zX3wb318o2Pw9ZUHgG3mmQME536Qg2Ha From 09509f05b1db3ed34e9fb8cb7f62eaf994cb6117 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 20:22:27 -0500 Subject: [PATCH 05/52] fixing readme --- README.md | 72 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6d074c3..a695996 100644 --- a/README.md +++ b/README.md @@ -23,21 +23,23 @@ This will create a folder named goxtool containing all the needed files. Thats a Change into to the goxtool folder that was created in the previous step and start goxtool.py: +``` cd goxtool ./goxtool.py +``` Keyboard commands (only the ones useful in view-only mode, without Mt.Gox account): - `q` quit - `l` (lower case "L") reload the strategy module (see advanced usage) - `D` (shift + d) switch to depth chart view - `H` (shift + h) switch to candlestick history chart view - `S` (shift + s) toggle summing up the volume of order book levels on/off - `T` (shift + t) toggle summing up the volume in the depth chart on/off - `-` order book zoom out (increase group size) - `+` order book zoom in (decrease group size) - `,` depth chart zoom out (increase group size) - `.` depth chart zoom in (decrease group size) + * `q` quit + * `l` (lower case "L") reload the strategy module (see advanced usage) + * `D` (shift + d) switch to depth chart view + * `H` (shift + h) switch to candlestick history chart view + * `S` (shift + s) toggle summing up the volume of order book levels on/off + * `T` (shift + t) toggle summing up the volume in the depth chart on/off + * `-` order book zoom out (increase group size) + * `+` order book zoom in (decrease group size) + * `,` depth chart zoom out (increase group size) + * `.` depth chart zoom in (decrease group size) (There will be even more commands once you connect it to your Mt.Gox account) @@ -46,11 +48,15 @@ socketio or websocket? Which one is worse? The two options are socketio or websocket, the .ini setting for this is use_plain_old_websocket. To force it connecting with socketio: -```./goxtool.py --protocol=socketio``` +``` +./goxtool.py --protocol=socketio +``` To force it connecting to the websocket server do this: -```./goxtool.py --protocol=websocket``` +``` +./goxtool.py --protocol=websocket +``` It has turned out that websocket is currently the most reliable protocol. These options on the command line take precedence over what you have configured in the .ini file (but it won't change your .ini), you can make websocket the default (so you don't need this option anymore) by editing the ini file. @@ -58,14 +64,18 @@ If you experience a high lag between sending an order and the ack (the op:result The following is what I am currently using and I recommend it: -```./goxtool.py --protocol=websocket --use-http``` +``` +./goxtool.py --protocol=websocket --use-http +``` ## Trading with your MtGox account First you will need to add an API key in MtGox, then do the following: -```./goxtool.py --add-secret``` +``` +./goxtool.py --add-secret +``` This will now ask you for your key, secret and a password (not your MtGox one) to secure those on your drive. The key and secret belong to a shared secret that is created by MtGox to authenticate your trading software against their API. You can request as many keys from MtGox as you need, every application you connect to your MtGox account should have its own key, you can also at any time delete the keys again that you no longer need. @@ -75,10 +85,16 @@ Now MtGox will create 2 strings of cryptic numbers and letters, the "API-Key" an Now start goxtool again: -```./goxtool.py``` +``` +./goxtool.py +``` + Which will ask: -```enter passphrase for secret:``` + +``` +enter passphrase for secret: +``` From now on every time you start goxtool it will ask you for the passphrase in order to be able to decrypt and use the secret. Enter your passphrase, press enter. Now goxtool will start and you will notice that now it is showing your account balance at the top of the window. Now all trading functions are enabled. @@ -99,7 +115,9 @@ All dialogs can be closed with `F10` or `ESC`. Running all strategies: -```./goxtool.py --strategy=balancer.py,buy.py,sell.py``` +``` +./goxtool.py --strategy=balancer.py,buy.py,sell.py +``` #### Balancer @@ -112,7 +130,9 @@ Portfolio rebalancing bot that will buy and sell to maintain a constant asset al * `c` to cancel all rebalancing orders * `u` to update account information, order list and wallet -```./goxtool.py --strategy=balancer.py``` +``` +./goxtool.py --strategy=balancer.py +``` #### Buy strategy @@ -121,7 +141,9 @@ Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` a * `b` to see Buy objective * `o` to see Buy order book -```./goxtool.py --strategy=buy.py``` +``` +./goxtool.py --strategy=buy.py +``` #### Sell strategy @@ -130,7 +152,9 @@ Sell strategy module. Set `sell_level` at the price you want to sell, `threshold * `s` to see Sell objective * `k` to see Sell order book -```./goxtool.py --strategy=sell.py``` +``` +./goxtool.py --strategy=sell.py +``` #### Making your own @@ -138,7 +162,9 @@ You can write your own trading bots. There is a file named `strategy.py`, it con If you decide to make serious use of this then please create a new python file for your strategy. either make a copy of the default strategy.py skeleton or make a module that imports strategy and has a class Strategy(strategy.Strategy), give this module file a different name and leave strategy.py alone so it won't collide with upstream changes you pull from github. By default goxtool will load strategy.py but you can start it with the --strategy command line option to specify your own strategy module or a comma separated list of many modules: -```./goxtool --strategy=mybot.py,myotherbot.py``` +``` +./goxtool --strategy=mybot.py,myotherbot.py +``` You can even edit the strategy while goxtool is running and then reload it at runtime (this can be very useful), just press the l key (lowercase L) and it will do the following things: @@ -158,7 +184,9 @@ How to keep it up to date Occasionally I will commit bugfixes, improvements, etc. To update your copy of goxtool (assuming you previously installed it with git clone and not by just downloading a zip file) do the following: -```git pull``` +``` +git pull +``` and if that complains because of local uncommitted changes because you edited the strategy.py module or did other changes to the code then try this: From 0bcf870b8bda1af828dda83538f92dabcb0ddb79 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 20:23:23 -0500 Subject: [PATCH 06/52] fixing readme --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a695996..0f9bde9 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,16 @@ cd goxtool Keyboard commands (only the ones useful in view-only mode, without Mt.Gox account): - * `q` quit - * `l` (lower case "L") reload the strategy module (see advanced usage) - * `D` (shift + d) switch to depth chart view - * `H` (shift + h) switch to candlestick history chart view - * `S` (shift + s) toggle summing up the volume of order book levels on/off - * `T` (shift + t) toggle summing up the volume in the depth chart on/off - * `-` order book zoom out (increase group size) - * `+` order book zoom in (decrease group size) - * `,` depth chart zoom out (increase group size) - * `.` depth chart zoom in (decrease group size) +* `q` quit +* `l` (lower case "L") reload the strategy module (see advanced usage) +* `D` (shift + d) switch to depth chart view +* `H` (shift + h) switch to candlestick history chart view +* `S` (shift + s) toggle summing up the volume of order book levels on/off +* `T` (shift + t) toggle summing up the volume in the depth chart on/off +* `-` order book zoom out (increase group size) +* `+` order book zoom in (decrease group size) +* `,` depth chart zoom out (increase group size) +* `.` depth chart zoom in (decrease group size) (There will be even more commands once you connect it to your Mt.Gox account) @@ -100,9 +100,9 @@ From now on every time you start goxtool it will ask you for the passphrase in o Keyboard commands for trading: - * `F4` : New buy order - * `F5` : New sell order - * `F6` : View orders / cancel order(s) +* `F4` : New buy order +* `F5` : New sell order +* `F6` : View orders / cancel order(s) In the cancel dialog you can move up/down with the arrow keys, use INS to select/unselect orders (you can select multiple orders and cancel them all at once) or if you just quickly want to cancel only one order just highlight to the order and hit F8. It behaves a little bit like deleting files in midnight commander. @@ -124,11 +124,11 @@ Running all strategies: Portfolio rebalancing bot that will buy and sell to maintain a constant asset allocation ratio of exactly 50/50 = fiat/BTC. - * `i` for information (how much currently out of balance) - * `r` to rebalance with market order at current price (required before rebalancing) - * `p` to add initial rebalancing orders - * `c` to cancel all rebalancing orders - * `u` to update account information, order list and wallet +* `i` for information (how much currently out of balance) +* `r` to rebalance with market order at current price (required before rebalancing) +* `p` to add initial rebalancing orders +* `c` to cancel all rebalancing orders +* `u` to update account information, order list and wallet ``` ./goxtool.py --strategy=balancer.py @@ -138,8 +138,8 @@ Portfolio rebalancing bot that will buy and sell to maintain a constant asset al Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` above your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. - * `b` to see Buy objective - * `o` to see Buy order book +* `b` to see Buy objective +* `o` to see Buy order book ``` ./goxtool.py --strategy=buy.py From 6342cf1794b40a5031e18c7c2f5a4ec66211a7b9 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 20:23:50 -0500 Subject: [PATCH 07/52] fixing readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f9bde9..ac087f8 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` a Sell strategy module. Set `sell_level` at the price you want to sell, `threshold` below your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. - * `s` to see Sell objective - * `k` to see Sell order book +* `s` to see Sell objective +* `k` to see Sell order book ``` ./goxtool.py --strategy=sell.py From 0a0c43b5f33a08493bc8926557ce716f65b81daf Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 21:54:19 -0500 Subject: [PATCH 08/52] Use in readme --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ac087f8..4fdfabb 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,16 @@ cd goxtool Keyboard commands (only the ones useful in view-only mode, without Mt.Gox account): -* `q` quit -* `l` (lower case "L") reload the strategy module (see advanced usage) -* `D` (shift + d) switch to depth chart view -* `H` (shift + h) switch to candlestick history chart view -* `S` (shift + s) toggle summing up the volume of order book levels on/off -* `T` (shift + t) toggle summing up the volume in the depth chart on/off -* `-` order book zoom out (increase group size) -* `+` order book zoom in (decrease group size) -* `,` depth chart zoom out (increase group size) -* `.` depth chart zoom in (decrease group size) +- q quit +- l (lower case "L") reload the strategy module (see advanced usage) +- D (shift + d) switch to depth chart view +- H (shift + h) switch to candlestick history chart view +- S (shift + s) toggle summing up the volume of order book levels on/off +- T (shift + t) toggle summing up the volume in the depth chart on/off +- - order book zoom out (increase group size) +- + order book zoom in (decrease group size) +- , depth chart zoom out (increase group size) +- . depth chart zoom in (decrease group size) (There will be even more commands once you connect it to your Mt.Gox account) @@ -100,9 +100,9 @@ From now on every time you start goxtool it will ask you for the passphrase in o Keyboard commands for trading: -* `F4` : New buy order -* `F5` : New sell order -* `F6` : View orders / cancel order(s) +- F4 : New buy order +- F5 : New sell order +- F6 : View orders / cancel order(s) In the cancel dialog you can move up/down with the arrow keys, use INS to select/unselect orders (you can select multiple orders and cancel them all at once) or if you just quickly want to cancel only one order just highlight to the order and hit F8. It behaves a little bit like deleting files in midnight commander. @@ -124,11 +124,11 @@ Running all strategies: Portfolio rebalancing bot that will buy and sell to maintain a constant asset allocation ratio of exactly 50/50 = fiat/BTC. -* `i` for information (how much currently out of balance) -* `r` to rebalance with market order at current price (required before rebalancing) -* `p` to add initial rebalancing orders -* `c` to cancel all rebalancing orders -* `u` to update account information, order list and wallet +- i for information (how much currently out of balance) +- r to rebalance with market order at current price (required before rebalancing) +- p to add initial rebalancing orders +- c to cancel all rebalancing orders +- u to update account information, order list and wallet ``` ./goxtool.py --strategy=balancer.py @@ -138,8 +138,8 @@ Portfolio rebalancing bot that will buy and sell to maintain a constant asset al Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` above your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. -* `b` to see Buy objective -* `o` to see Buy order book +* b to see Buy objective +* o to see Buy order book ``` ./goxtool.py --strategy=buy.py @@ -149,8 +149,8 @@ Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` a Sell strategy module. Set `sell_level` at the price you want to sell, `threshold` below your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. -* `s` to see Sell objective -* `k` to see Sell order book +- s to see Sell objective +- k to see Sell order book ``` ./goxtool.py --strategy=sell.py From ee041c11c0ea0f8a18aaea69f4caf78e7da0ab55 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 22:04:09 -0500 Subject: [PATCH 09/52] fixing readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fdfabb..5ccf29e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Goxtool -Goxtool is a trading client for the MtGox Bitcon currency exchange. It is +Goxtool is a trading client for the MtGox Bitcoin 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. From 2f3fe074e18ef398d5e983d0942aa06a983db5c6 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 1 Dec 2013 22:07:31 -0500 Subject: [PATCH 10/52] fixing readme --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5ccf29e..9c14652 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ # Goxtool -Goxtool is a trading client for the MtGox Bitcoin 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 reloaded at runtime, will receive -events from the API and can act upon them. +Goxtool is a Python trading client and auto-trading bot for the MtGox Bitcoin currency exchange. It is designed to work in the Linux console and 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 reloaded at runtime, will receive events from the API and can act upon them. ## Installation From 261b793ab651ca7aafc42994f9fac1011182ebe6 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 2 Dec 2013 05:25:46 -0500 Subject: [PATCH 11/52] Add simulate flag, protect against selling below current ask price + step, add rebalancing warning, set minimum buy to 0.011 BTC, use gox.quote2float and gox.base2float in buy and sell strategies --- balancer.py | 68 ++++++++++++++++++++++++++++++++++++++++------------- buy.py | 18 +++++++------- sell.py | 18 +++++++------- 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/balancer.py b/balancer.py index b33155a..fcd2ba1 100644 --- a/balancer.py +++ b/balancer.py @@ -3,8 +3,15 @@ constant asset allocation ratio of exactly 50/50 = fiat/BTC """ +import goxapi import strategy +# Simulate +simulate = True + +# Live or simulation notice +simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') + DISTANCE = 7 # percent price distance of next rebalancing orders FIAT_COLD = 0 # Amount of Fiat stored at home but included in calculations COIN_COLD = 0 # Amount of Coin stored at home but included in calculations @@ -33,10 +40,14 @@ class Strategy(strategy.Strategy): """a portfolio rebalancing bot""" def __init__(self, gox): strategy.Strategy.__init__(self, gox) + self.ask = 0 + self.simulate_or_live = simulate_or_live + self.distance = DISTANCE + self.init_distance = float(DISTANCE) self.temp_halt = False self.name = "%s.%s" % (__name__, self.__class__.__name__) self.debug("[s]%s loaded" % self.name) - self.debug("[s]Press 'i' for information (how much currently out of balance)\n Press 'r' to rebalance with market order at current price (required before rebalancing)\n Press 'p' to add initial rebalancing orders\n Press 'c' to cancel all rebalancing orders\n Press 'u' to update account information, order list and wallet") + self.debug("[s]Press 'i' for information (how much currently out of balance)\n WARNING Rebalancing will buy or sell up to half your fiat or BTC balance\n Press 'r' to rebalance with market order at current price (required before rebalancing)\n Press 'p' to add initial rebalancing orders and start trading\n Press 'c' to cancel all rebalancing orders and suspend trading\n Press 'u' to update account information, order list and wallet") def __del__(self): try: @@ -86,13 +97,19 @@ def slot_keypress(self, gox, (key)): self.temp_halt = True self.cancel_orders() if vol_buy > 0: - self.debug("[s]buy %f at market" % - gox.base2float(vol_buy)) - gox.buy(0, vol_buy) + self.debug("[s]%s*** buying %f at market price of %f" % ( + self.simulate_or_live, + gox.base2float(vol_buy), + gox.quote2float(price))) + if simulate == False: + gox.buy(0, vol_buy) else: - self.debug("[s]sell %f at market" % - gox.base2float(-vol_buy)) - gox.sell(0, -vol_buy) + self.debug("[s]%s*** selling %f at market price of %f" % ( + self.simulate_or_live, + gox.base2float(-vol_buy), + gox.quote2float(price))) + if simulate == False: + gox.sell(0, -vol_buy) def cancel_orders(self): """cancel all rebalancing orders, we identify @@ -134,11 +151,19 @@ def get_buy_at_price(self, price_int): def place_orders(self): """place two new rebalancing orders above and below center price""" center = self.get_price_where_it_was_balanced() - self.debug( - "[s]center is %f" % self.gox.quote2float(center)) - step = int(center * DISTANCE / 100.0) + self.debug("[s]center is %f" % self.gox.quote2float(center)) + + step = int(center * self.distance / 100.0) next_sell = mark_own(center + step) next_buy = mark_own(center - step) + status_prefix = self.simulate_or_live + + # Protect against selling below current ask price + step + if self.ask != 0 and self.gox.quote2float(next_sell) < self.ask: + next_sell = self.gox.quote2int(self.ask) + step + self.debug("[s]next_sell at %f, self.ask at %f" % (self.gox.quote2float(next_sell), self.ask)) + elif self.ask == 0: + status_prefix = 'Waiting for price - ' + self.simulate_or_live sell_amount = -self.get_buy_at_price(next_sell) buy_amount = self.get_buy_at_price(next_buy) @@ -148,20 +173,31 @@ def place_orders(self): self.debug("WARNING! minimal sell amount adjusted to 0.01") if buy_amount < 0.01 * COIN: - buy_amount = int(0.01 * COIN) - self.debug("WARNING! minimal buy amount adjusted to 0.01") + buy_amount = int(0.011 * COIN) + self.debug("WARNING! minimal buy amount adjusted to 0.011") - self.debug("[s]new buy order %f at %f" % ( + self.debug("[s]%snew buy order %f at %f" % ( + status_prefix, self.gox.base2float(buy_amount), self.gox.quote2float(next_buy) )) - self.gox.buy(next_buy, buy_amount) + if simulate == False and self.ask != 0: + self.gox.buy(next_buy, buy_amount) - self.debug("[s]new sell order %f at %f" % ( + self.debug("[s]%snew sell order %f at %f" % ( + status_prefix, self.gox.base2float(sell_amount), self.gox.quote2float(next_sell) )) - self.gox.sell(next_sell, sell_amount) + if simulate == False and self.ask != 0: + self.gox.sell(next_sell, sell_amount) + + def slot_tick(self, gox, (bid, ask)): + # Set last ask price + self.ask = goxapi.int2float(ask, self.gox.orderbook.gox.currency) + if self.gox.orderbook.total_ask and self.ask != False and self.distance: + ratio = (self.gox.orderbook.total_bid / self.gox.orderbook.total_ask) / 1000 + self.debug("ratio: %f bid/ask with %f percent target distance" % (ratio, self.distance)) def slot_trade(self, gox, (date, price, volume, typ, own)): """a trade message has been receivd""" diff --git a/buy.py b/buy.py index dbd7435..c7cc1c0 100644 --- a/buy.py +++ b/buy.py @@ -48,7 +48,7 @@ def __init__(self, gox): goxapi.BaseObject.__init__(self) self.signal_debug.connect(gox.signal_debug) gox.signal_keypress.connect(self.slot_keypress) - gox.signal_strategy_unload.connect(self.slot_before_unload) + # gox.signal_strategy_unload.connect(self.slot_before_unload) gox.signal_ticker.connect(self.slot_tick) gox.signal_depth.connect(self.slot_depth) gox.signal_trade.connect(self.slot_trade) @@ -70,8 +70,8 @@ def __del__(self): except Exception, e: self.debug("[s]%s exception: %s" % (self.name, e)) - def slot_before_unload(self, _sender, _data): - self.debug("[s]%s before unload" % self.name) + # def slot_before_unload(self, _sender, _data): + # self.debug("[s]%s before unload" % self.name) def slot_keypress(self, gox, (key)): # some custom keypresses are caught here: @@ -85,7 +85,7 @@ def slot_keypress(self, gox, (key)): # check if the user changed volume # also ensure the buy_amount does not exceed wallet balance # if it does, set buy_amount to wallet full fiat balance - walletbalance = goxapi.int2float(self.gox.wallet[self.gox.orderbook.gox.currency], self.gox.orderbook.gox.currency) + walletbalance = gox.quote2float(self.gox.wallet[self.gox.orderbook.gox.currency]) if volume == 0: buy_amount = walletbalance else: @@ -101,14 +101,14 @@ def slot_keypress(self, gox, (key)): elif key == ord('o'): self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) for order in self.gox.orderbook.owns: - self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (goxapi.int2float(order.price, gox.orderbook.gox.currency), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) + self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (gox.quote2float(order.price), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) def slot_tick(self, gox, (bid, ask)): global bidbuf, askbuf, buy_amount # if goxapi receives a no-change tick update, don't output anything if bid != bidbuf or ask != askbuf: seen = 0 # var seen is a flag for default output below (=0) - self.ask = goxapi.int2float(ask, self.gox.orderbook.gox.currency) + self.ask = gox.quote2float(ask) if self.ask > buy_level and self.ask < buy_alert: self.debug("[s] !!! buy ALERT @ %s; ask currently at %s" % (str(buy_alert), str(self.ask))) self.debug("[s] !!! BUY for %f %s will trigger @ %f" % (buy_amount,str(self.gox.orderbook.gox.currency), buy_level)) @@ -146,10 +146,10 @@ def slot_userorder(self, gox, (price, volume, typ, oid, status)): # the coder assumes that if an order id is received via # this signal then it was not instantaneously actioned, so cancel # at once - self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (goxapi.int2float(price, self.gox.orderbook.gox.currency), str(volume), str(typ), str(oid), str(status))) + self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (gox.quote2float(price), str(volume), str(typ), str(oid), str(status))) # cancel by oid if status not in ['pending','executing','post-pending','removed'] and oid not in self.existingorders: - if goxapi.int2float(price, self.gox.orderbook.gox.currency) == buy_level: + if gox.quote2float(price) == buy_level: self.gox.cancel(oid) def slot_owns_changed(self, orderbook, _dummy): @@ -166,7 +166,7 @@ def slot_wallet_changed(self, gox, _dummy): # also ensure the buy_amount does not exceed wallet balance # if it does, set buy_amount to wallet full fiat balance global buy_amount - walletbalance = goxapi.int2float(self.gox.wallet[self.gox.orderbook.gox.currency], self.gox.orderbook.gox.currency) + walletbalance = gox.quote2float(self.gox.wallet[self.gox.orderbook.gox.currency]) if volume != 0 and volume <= walletbalance: buy_amount = volume else: diff --git a/sell.py b/sell.py index 8f15077..81a47fb 100644 --- a/sell.py +++ b/sell.py @@ -48,7 +48,7 @@ def __init__(self, gox): goxapi.BaseObject.__init__(self) self.signal_debug.connect(gox.signal_debug) gox.signal_keypress.connect(self.slot_keypress) - gox.signal_strategy_unload.connect(self.slot_before_unload) + # gox.signal_strategy_unload.connect(self.slot_before_unload) gox.signal_ticker.connect(self.slot_tick) gox.signal_depth.connect(self.slot_depth) gox.signal_trade.connect(self.slot_trade) @@ -70,8 +70,8 @@ def __del__(self): except Exception, e: self.debug("[s]%s exception: %s" % (self.name, e)) - def slot_before_unload(self, _sender, _data): - self.debug("[s]%s before unload" % self.name) + # def slot_before_unload(self, _sender, _data): + # self.debug("[s]%s before unload" % self.name) def slot_keypress(self, gox, (key)): # some custom keypresses are caught here: @@ -85,7 +85,7 @@ def slot_keypress(self, gox, (key)): # check if the user changed volume # also ensure the buy_amount does not exceed wallet balance # if it does, set sell_amount to wallet full BTC balance - walletbalance = goxapi.int2float(self.gox.wallet['BTC'], 'BTC') + walletbalance = gox.base2float(self.gox.wallet['BTC']) if volume == 0: sell_amount = walletbalance else: @@ -101,14 +101,14 @@ def slot_keypress(self, gox, (key)): elif key == ord('k'): self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) for order in self.gox.orderbook.owns: - self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (goxapi.int2float(order.price, gox.orderbook.gox.currency), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) + self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (gox.quote2float(order.price), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) def slot_tick(self, gox, (bid, ask)): global bidbuf, askbuf, sell_amount # if goxapi receives a no-change tick update, don't output anything if bid != bidbuf or ask != askbuf: seen = 0 # var seen is a flag for default output below (=0) - self.bid = goxapi.int2float(bid, gox.orderbook.gox.currency) + self.bid = gox.quote2float(bid) if self.bid < sell_level and self.bid > sell_alert: self.debug("[s] !!! SELL ALERT @ %s; bid currently at %s" % (str(sell_alert), str(self.bid))) self.debug("[s] !!! SELL for %f BTC will trigger @ %f" % (sell_amount, sell_level)) @@ -146,10 +146,10 @@ def slot_userorder(self, gox, (price, volume, typ, oid, status)): # the coder assumes that if an order id is received via # this signal then it was not instantaneously actioned, so cancel # at once - self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (goxapi.int2float(price, self.gox.orderbook.gox.currency), str(volume), str(typ), str(oid), str(status))) + self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (gox.quote2float(price), str(volume), str(typ), str(oid), str(status))) # cancel by oid if status not in ['pending','executing','post-pending','removed'] and oid not in self.existingorders: - if goxapi.int2float(price, self.gox.orderbook.gox.currency) == sell_level: + if gox.quote2float(price) == sell_level: self.gox.cancel(oid) def slot_owns_changed(self, orderbook, _dummy): @@ -165,7 +165,7 @@ def slot_wallet_changed(self, gox, _dummy): # also ensure the buy_amount does not exceed wallet balance # if it does, set sell_amount to wallet full BTC balance global sell_amount - walletbalance = goxapi.int2float(self.gox.wallet['BTC'], 'BTC') + walletbalance = gox.base2float(self.gox.wallet['BTC']) if volume != 0 and volume <= walletbalance: sell_amount = volume else: From 28e111526d44d83f43c22dc0c60ff86c9db1ad5d Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 2 Dec 2013 06:43:49 -0500 Subject: [PATCH 12/52] use mark_own and output cleanup --- balancer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/balancer.py b/balancer.py index fcd2ba1..4a02a94 100644 --- a/balancer.py +++ b/balancer.py @@ -10,7 +10,7 @@ simulate = True # Live or simulation notice -simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') +simulate_or_live = ('SIMULATION - ' if simulate else '') DISTANCE = 7 # percent price distance of next rebalancing orders FIAT_COLD = 0 # Amount of Fiat stored at home but included in calculations @@ -97,14 +97,14 @@ def slot_keypress(self, gox, (key)): self.temp_halt = True self.cancel_orders() if vol_buy > 0: - self.debug("[s]%s*** buying %f at market price of %f" % ( + self.debug("[s]%sbuying %f at market price of %f" % ( self.simulate_or_live, gox.base2float(vol_buy), gox.quote2float(price))) if simulate == False: gox.buy(0, vol_buy) else: - self.debug("[s]%s*** selling %f at market price of %f" % ( + self.debug("[s]%sselling %f at market price of %f" % ( self.simulate_or_live, gox.base2float(-vol_buy), gox.quote2float(price))) @@ -160,10 +160,10 @@ def place_orders(self): # Protect against selling below current ask price + step if self.ask != 0 and self.gox.quote2float(next_sell) < self.ask: - next_sell = self.gox.quote2int(self.ask) + step + next_sell = mark_own(self.gox.quote2int(self.ask) + step) self.debug("[s]next_sell at %f, self.ask at %f" % (self.gox.quote2float(next_sell), self.ask)) elif self.ask == 0: - status_prefix = 'Waiting for price - ' + self.simulate_or_live + status_prefix = 'Waiting for price, skipped ' + self.simulate_or_live sell_amount = -self.get_buy_at_price(next_sell) buy_amount = self.get_buy_at_price(next_buy) From 140aa3913b07b9e3b4fdda8a3b73917cf3248a3e Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Tue, 3 Dec 2013 15:14:30 -0500 Subject: [PATCH 13/52] Add protection against buying above current bid price, adjust sell protection to + step / 2, add get_btc_value function to clean up get_buy_at_price --- balancer.py | 39 +++++++++++++++++++++++++-------------- goxtool.py | 8 +++++--- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/balancer.py b/balancer.py index 4a02a94..69e8052 100644 --- a/balancer.py +++ b/balancer.py @@ -40,6 +40,7 @@ class Strategy(strategy.Strategy): """a portfolio rebalancing bot""" def __init__(self, gox): strategy.Strategy.__init__(self, gox) + self.bid = 0 self.ask = 0 self.simulate_or_live = simulate_or_live self.distance = DISTANCE @@ -137,17 +138,20 @@ def get_price_where_it_was_balanced(self): def get_buy_at_price(self, price_int): """calculate amount of BTC needed to buy at price to achieve rebalancing. price and return value are in mtgox integer format""" - gox = self.gox - fiat_have = gox.quote2float(gox.wallet[gox.curr_quote]) + FIAT_COLD - btc_have = gox.base2float(gox.wallet[gox.curr_base]) + COIN_COLD - price_then = gox.quote2float(price_int) - - btc_value_then = btc_have * price_then + fiat_have = self.gox.quote2float(self.gox.wallet[self.gox.curr_quote]) + FIAT_COLD + btc_value_then = self.get_btc_value(price_int) + price_then = self.gox.quote2float(price_int) diff = fiat_have - btc_value_then diff_btc = diff / price_then must_buy = diff_btc / 2 return self.gox.base2int(must_buy) + def get_btc_value(self, price_int): + btc_have = self.gox.base2float(self.gox.wallet[self.gox.curr_base]) + COIN_COLD + price_then = self.gox.quote2float(price_int) + btc_value_then = btc_have * price_then + return btc_value_then + def place_orders(self): """place two new rebalancing orders above and below center price""" center = self.get_price_where_it_was_balanced() @@ -158,12 +162,21 @@ def place_orders(self): next_buy = mark_own(center - step) status_prefix = self.simulate_or_live - # Protect against selling below current ask price + step + # Protect against selling below current ask price if self.ask != 0 and self.gox.quote2float(next_sell) < self.ask: - next_sell = mark_own(self.gox.quote2int(self.ask) + step) - self.debug("[s]next_sell at %f, self.ask at %f" % (self.gox.quote2float(next_sell), self.ask)) + bad_next_sell = float(next_sell) + next_sell = mark_own(self.gox.quote2int(self.ask) + (step / 2)) + self.debug("[s]corrected next sell at %f instead of %f, ask price at %f" % (bad_next_sell, self.gox.quote2float(next_sell), self.ask)) elif self.ask == 0: - status_prefix = 'Waiting for price, skipped ' + self.simulate_or_live + status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live + + # Protect against buying above current bid price + if self.bid != 0 and self.gox.quote2float(next_buy) > self.bid: + bad_next_buy = float(next_buy) + next_buy = mark_own(self.gox.quote2int(self.bid) - (step / 2)) + self.debug("[s]corrected next buy at %f instead of %f, ask price at %f" % (bad_next_buy, self.gox.quote2float(next_buy), self.bid)) + elif self.bid == 0: + status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live sell_amount = -self.get_buy_at_price(next_sell) buy_amount = self.get_buy_at_price(next_buy) @@ -193,11 +206,9 @@ def place_orders(self): self.gox.sell(next_sell, sell_amount) def slot_tick(self, gox, (bid, ask)): - # Set last ask price + # Set last bid/ask price + self.bid = goxapi.int2float(bid, self.gox.orderbook.gox.currency) self.ask = goxapi.int2float(ask, self.gox.orderbook.gox.currency) - if self.gox.orderbook.total_ask and self.ask != False and self.distance: - ratio = (self.gox.orderbook.total_bid / self.gox.orderbook.total_ask) / 1000 - self.debug("ratio: %f bid/ask with %f percent target distance" % (ratio, self.distance)) def slot_trade(self, gox, (date, price, volume, typ, own)): """a trade message has been receivd""" diff --git a/goxtool.py b/goxtool.py index 5510436..67cbe71 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1738,9 +1738,11 @@ def curses_loop(stdscr): print trb else: print - print "*******************************************************" - print "* Please donate: 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW *" - print "*******************************************************" + print "**************************************************************" + print "* Please donate! :) *" + print "* prof7bit (goxtool): 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW *" + print "* caktux (mods): 18zX3wb318o2Pw9ZUHgG3mmQME536Qg2Ha *" + print "**************************************************************" if __name__ == "__main__": main() From 638b7c69f84db521a485a3ae25e89e5809406a1e Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Wed, 4 Dec 2013 01:56:08 -0500 Subject: [PATCH 14/52] Show prices in status bar with important info in bold, no more blue background just a blue separator, quick order book in balancer only, more price waiting to prevent errors --- README.md | 3 +-- balancer.py | 38 +++++++++++++++++++++++++++++--------- buy.py | 6 +----- goxtool.py | 50 ++++++++++++++++++++++++++++++++++---------------- sell.py | 6 +----- 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9c14652..5eed06d 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Running all strategies: Portfolio rebalancing bot that will buy and sell to maintain a constant asset allocation ratio of exactly 50/50 = fiat/BTC. - i for information (how much currently out of balance) +- o to see order book - r to rebalance with market order at current price (required before rebalancing) - p to add initial rebalancing orders - c to cancel all rebalancing orders @@ -134,7 +135,6 @@ Portfolio rebalancing bot that will buy and sell to maintain a constant asset al Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` above your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. * b to see Buy objective -* o to see Buy order book ``` ./goxtool.py --strategy=buy.py @@ -145,7 +145,6 @@ Buy strategy module. Set `buy_level` at the price you want to buy, `threshold` a Sell strategy module. Set `sell_level` at the price you want to sell, `threshold` below your level for a log alert and `volume` in fiat (`0` for full balance). Set `simulate` to `False` to activate. - s to see Sell objective -- k to see Sell order book ``` ./goxtool.py --strategy=sell.py diff --git a/balancer.py b/balancer.py index 69e8052..ecded7a 100644 --- a/balancer.py +++ b/balancer.py @@ -48,7 +48,13 @@ def __init__(self, gox): self.temp_halt = False self.name = "%s.%s" % (__name__, self.__class__.__name__) self.debug("[s]%s loaded" % self.name) - self.debug("[s]Press 'i' for information (how much currently out of balance)\n WARNING Rebalancing will buy or sell up to half your fiat or BTC balance\n Press 'r' to rebalance with market order at current price (required before rebalancing)\n Press 'p' to add initial rebalancing orders and start trading\n Press 'c' to cancel all rebalancing orders and suspend trading\n Press 'u' to update account information, order list and wallet") + self.debug("[s]Press 'i' for information (how much currently out of balance)") + self.debug("[s]Press 'o' to see order book") + self.debug("[s]WARNING Rebalancing will buy or sell up to half your fiat or BTC balance") + self.debug("[s]Press 'r' to rebalance with market order at current price (recommended before rebalancing)") + self.debug("[s]Press 'p' to add initial rebalancing orders and start trading") + self.debug("[s]Press 'c' to cancel all rebalancing orders and suspend trading") + self.debug("[s]Press 'u' to update account information, order list and wallet") def __del__(self): try: @@ -61,7 +67,7 @@ def slot_keypress(self, gox, (key)): if key == ord("c"): # cancel existing rebalancing orders and suspend trading - self.debug("[s]canceling all rebalancing orders") + self.debug("[s]%scanceling all rebalancing orders" % self.simulate_or_live) self.temp_halt = True self.cancel_orders() @@ -70,8 +76,9 @@ def slot_keypress(self, gox, (key)): # Before you do this the portfolio should already be balanced. # use "i" to show current status and "b" to rebalance with a # market order at current price. - self.debug("[s]adding new initial rebalancing orders") + self.debug("[s]%sadding new initial rebalancing orders" % self.simulate_or_live) self.temp_halt = False + self.cancel_orders() self.place_orders() if key == ord("u"): @@ -90,6 +97,11 @@ def slot_keypress(self, gox, (key)): self.debug("[s]Price where it would be balanced:", gox.quote2float(price_balanced)) + if key == ord('o'): + self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) + for order in self.gox.orderbook.owns: + self.debug("[s] Order: price %f vol %f BTC type %s oid %s status %s" % (gox.quote2float(order.price), gox.base2float(order.volume), str(order.typ), str(order.oid), str(order.status))) + if key == ord("r"): # manually rebalance with market order at current price price = (gox.orderbook.bid + gox.orderbook.ask) / 2 @@ -121,7 +133,8 @@ def cancel_orders(self): must_cancel.append(order) for order in must_cancel: - self.gox.cancel(order.oid) + if (simulate == False): + self.gox.cancel(order.oid) def get_price_where_it_was_balanced(self): """get the price at which it was perfectly balanced, given the current @@ -131,8 +144,12 @@ def get_price_where_it_was_balanced(self): so even after missing the trade message due to disconnect it should be possible to place the next 2 orders precisely around the new center""" gox = self.gox - fiat_have = gox.quote2float(gox.wallet[gox.curr_quote]) + FIAT_COLD - btc_have = gox.base2float(gox.wallet[gox.curr_base]) + COIN_COLD + if (gox.wallet): + fiat_have = gox.quote2float(gox.wallet[gox.curr_quote]) + FIAT_COLD + btc_have = gox.base2float(gox.wallet[gox.curr_base]) + COIN_COLD + else: + self.debug('[s]Waiting for price...') + return False return gox.quote2int(fiat_have / btc_have) def get_buy_at_price(self, price_int): @@ -155,7 +172,10 @@ def get_btc_value(self, price_int): def place_orders(self): """place two new rebalancing orders above and below center price""" center = self.get_price_where_it_was_balanced() - self.debug("[s]center is %f" % self.gox.quote2float(center)) + if center: + self.debug("[s]center is %f" % self.gox.quote2float(center)) + else: + return step = int(center * self.distance / 100.0) next_sell = mark_own(center + step) @@ -183,11 +203,11 @@ def place_orders(self): if sell_amount < 0.01 * COIN: sell_amount = int(0.01 * COIN) - self.debug("WARNING! minimal sell amount adjusted to 0.01") + self.debug("[s]WARNING! minimal sell amount adjusted to 0.01") if buy_amount < 0.01 * COIN: buy_amount = int(0.011 * COIN) - self.debug("WARNING! minimal buy amount adjusted to 0.011") + self.debug("[s]WARNING! minimal buy amount adjusted to 0.011") self.debug("[s]%snew buy order %f at %f" % ( status_prefix, diff --git a/buy.py b/buy.py index c7cc1c0..77ef91d 100644 --- a/buy.py +++ b/buy.py @@ -58,7 +58,7 @@ def __init__(self, gox): self.gox = gox self.name = "%s.%s" % (__name__, self.__class__.__name__) self.debug("[s]%s loaded" % self.name) - self.debug("[s]Press 'b' to see Buy objective\n Press 'o' to see Buy order book") + self.debug("[s]Press 'b' to see Buy objective") #get existing orders for later decision making self.existingorders = [] for order in self.gox.orderbook.owns: @@ -98,10 +98,6 @@ def slot_keypress(self, gox, (key)): # else: # buy_amount = walletbalance self.debug("[s] %sstrategy will spend %f of %f %s on next BUY" % (simulate_or_live, buy_amount, walletbalance, str(self.gox.orderbook.gox.currency))) - elif key == ord('o'): - self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) - for order in self.gox.orderbook.owns: - self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (gox.quote2float(order.price), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) def slot_tick(self, gox, (bid, ask)): global bidbuf, askbuf, buy_amount diff --git a/goxtool.py b/goxtool.py index 67cbe71..4da949e 100755 --- a/goxtool.py +++ b/goxtool.py @@ -48,10 +48,11 @@ HEIGHT_CON = 20 WIDTH_ORDERBOOK = 45 -COLORS = [["con_text", curses.COLOR_BLUE, curses.COLOR_CYAN] - ,["con_text_buy", curses.COLOR_BLUE, curses.COLOR_GREEN] - ,["con_text_sell", curses.COLOR_BLUE, curses.COLOR_RED] - ,["status_text", curses.COLOR_BLUE, curses.COLOR_CYAN] +COLORS = [["con_text", curses.COLOR_BLACK, curses.COLOR_WHITE] + ,["con_text_buy", curses.COLOR_BLACK, curses.COLOR_GREEN] + ,["con_text_sell", curses.COLOR_BLACK, curses.COLOR_RED] + ,["con_separator", curses.COLOR_BLUE, curses.COLOR_WHITE] + ,["status_text", curses.COLOR_BLACK, curses.COLOR_WHITE] ,["book_text", curses.COLOR_BLACK, curses.COLOR_CYAN] ,["book_bid", curses.COLOR_BLACK, curses.COLOR_GREEN] @@ -201,7 +202,7 @@ def resize(self): self.__create_win() def addstr(self, *args): - """drop-in replacement for addstr that will never raie exceptions + """drop-in replacement for addstr that will never raise exceptions and that will cut off at end of line instead of wrapping""" if len(args) > 0: line, col = self.win.getyx() @@ -284,7 +285,7 @@ def resize(self): def calc_size(self): """put it at the bottom of the screen""" self.height = HEIGHT_CON - self.width = self.termwidth - int(self.termwidth / 2) - 1 + self.width = self.termwidth - int(self.termwidth / 2) - 2 self.posy = self.termheight - self.height def slot_debug(self, dummy_gox, (txt)): @@ -333,6 +334,8 @@ def __init__(self, stdscr, gox): def paint(self): """just empty the window after resize (I am lazy)""" self.win.bkgd(" ", COLOR_PAIR["con_text"]) + for i in range(HEIGHT_CON): + self.win.addstr("\n ", COLOR_PAIR["con_separator"]) def resize(self): """resize and print a log message. Old messages will have been @@ -356,7 +359,8 @@ def slot_debug(self, dummy_gox, (txt)): def write(self, txt): """write a line of text, scroll if needed""" - self.win.addstr("\n" + txt, COLOR_PAIR["con_text"]) + self.win.addstr("\n ", COLOR_PAIR["con_separator"]) + self.win.addstr(txt, COLOR_PAIR["con_text"]) self.done_paint() @@ -988,18 +992,31 @@ def paint(self): # # first line # - line1 = "Market: %s%s | " % (cbase, cquote) - line1 += "Account: " + self.addstr(0, 0, "Price: ", COLOR_PAIR["status_text"]) + self.addstr("%f" % self.gox.quote2float(self.gox.orderbook.bid), COLOR_PAIR["status_text"] + curses.A_BOLD) + self.addstr(" - ", COLOR_PAIR["status_text"]) + self.addstr("%f" % self.gox.quote2float(self.gox.orderbook.ask), COLOR_PAIR["status_text"] + curses.A_BOLD) + + self.addstr(" | Market: ", COLOR_PAIR["status_text"]) + self.addstr("%s%s" % (cbase, cquote), COLOR_PAIR["status_text"] + curses.A_BOLD) + + self.addstr(" | Account: ", COLOR_PAIR["status_text"]) if len(self.sorted_currency_list): + own_currencies = [] for currency in self.sorted_currency_list: if currency in self.gox.wallet: - line1 += currency + " " \ - + goxapi.int2str(self.gox.wallet[currency], currency).strip() \ - + " + " - line1 = line1.strip(" +") - line1 += " | Fee: " + ("%f" % self.gox.trade_fee) + own_currencies.append(currency) + for c, own_currency in enumerate(own_currencies): + self.addstr("%s" % own_currency, COLOR_PAIR["status_text"] + curses.A_BOLD) + self.addstr(" ", COLOR_PAIR["status_text"]) + self.addstr("%f" % goxapi.int2float(self.gox.wallet[own_currency], own_currency), COLOR_PAIR["status_text"] + curses.A_BOLD) + if (c + 1 != len(own_currencies)): + self.addstr(" + ", COLOR_PAIR["status_text"]) + self.addstr(" | Fee: ", COLOR_PAIR["status_text"]) + self.addstr("%s" % self.gox.trade_fee, COLOR_PAIR["status_text"] + curses.A_BOLD) + self.addstr(" %", COLOR_PAIR["status_text"]) else: - line1 += "No info (yet)" + self.addstr("No info (yet)", COLOR_PAIR["status_text"] + curses.A_BOLD) # # second line @@ -1020,7 +1037,8 @@ def paint(self): line2 += "o_lag: %s | " % self.order_lag_txt line2 += "s_lag: %.3f s" % (self.gox.socket_lag / 1e6) - self.addstr(0, 0, line1, COLOR_PAIR["status_text"]) + + # self.addstr(0, 0, line1, COLOR_PAIR["status_text"]) self.addstr(1, 0, line2, COLOR_PAIR["status_text"]) diff --git a/sell.py b/sell.py index 81a47fb..d079d6b 100644 --- a/sell.py +++ b/sell.py @@ -58,7 +58,7 @@ def __init__(self, gox): self.gox = gox self.name = "%s.%s" % (__name__, self.__class__.__name__) self.debug("[s]%s loaded" % self.name) - self.debug("[s]Press 's' to see Sell objective\n Press 'k' to see Sell order book") + self.debug("[s]Press 's' to see Sell objective") #get existing orders for later decision making self.existingorders = [] for order in self.gox.orderbook.owns: @@ -98,10 +98,6 @@ def slot_keypress(self, gox, (key)): # else: # sell_amount = walletbalance self.debug("[s] %sstrategy will sell %f of %f BTC on next SELL" % (simulate_or_live, sell_amount, walletbalance)) - elif key == ord('k'): - self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) - for order in self.gox.orderbook.owns: - self.debug("[s] Orders: price %f vol %s type %s oid %s status %s" % (gox.quote2float(order.price), goxapi.int2str(order.volume, "BTC"), str(order.typ), str(order.oid), str(order.status))) def slot_tick(self, gox, (bid, ask)): global bidbuf, askbuf, sell_amount From e888e12c981a914ab5c632581b173e961e2725da Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Wed, 4 Dec 2013 06:46:22 -0500 Subject: [PATCH 15/52] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5eed06d..a427f4e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Goxtool also has a simple interface to plug in your own automated trading strate Open a terminal in an empty folder or in the folder you usually use to clone repositories and clone the master branch: -```git clone git://github.com/USERNAME/goxtool.git``` +```git clone git://github.com/caktux/goxtool.git``` This will create a folder named goxtool containing all the needed files. Thats all, now it is installed and ready to use. You can now already watch live market data (without any trading functions being enabled), you can later add a MtGox API-key to it to have full access to your account but for now just proceed to the next step, start it without an account, just to make sure everything works. From 8e7f3e209acfe95980ad037368a4cd814ada1ad8 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Wed, 4 Dec 2013 06:46:41 -0500 Subject: [PATCH 16/52] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a427f4e..4a8db21 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ Goxtool also has a simple interface to plug in your own automated trading strate Open a terminal in an empty folder or in the folder you usually use to clone repositories and clone the master branch: -```git clone git://github.com/caktux/goxtool.git``` +``` +git clone git://github.com/caktux/goxtool.git +``` This will create a folder named goxtool containing all the needed files. Thats all, now it is installed and ready to use. You can now already watch live market data (without any trading functions being enabled), you can later add a MtGox API-key to it to have full access to your account but for now just proceed to the next step, start it without an account, just to make sure everything works. From a596fa17e1df9aec83fa2039b52d1ef67d1c86c6 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Thu, 5 Dec 2013 23:56:32 -0500 Subject: [PATCH 17/52] Allow overriding defaults with a json encoded user.conf file, fix some log output and some linting --- balancer.py | 27 +++++++++++++++++++-------- buy.py | 28 +++++++++++++++++++--------- sell.py | 26 ++++++++++++++++++-------- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/balancer.py b/balancer.py index ecded7a..7202159 100644 --- a/balancer.py +++ b/balancer.py @@ -5,18 +5,29 @@ import goxapi import strategy +import simplejson as json + +# Load user.conf +conf = json.load(open("user.conf")) + +# Set defaults +conf.setdefault('balancer_simulate', True) +conf.setdefault('balancer_distance', 7) +conf.setdefault('balancer_fiat_cold', 0) +conf.setdefault('balancer_coin_cold', 0) +conf.setdefault('balancer_marker', 7) # Simulate -simulate = True +simulate = conf['balancer_simulate'] # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else '') -DISTANCE = 7 # percent price distance of next rebalancing orders -FIAT_COLD = 0 # Amount of Fiat stored at home but included in calculations -COIN_COLD = 0 # Amount of Coin stored at home but included in calculations +DISTANCE = conf['balancer_distance'] # percent price distance of next rebalancing orders +FIAT_COLD = conf['balancer_fiat_cold'] # Amount of Fiat stored at home but included in calculations +COIN_COLD = conf['balancer_coin_cold'] # Amount of Coin stored at home but included in calculations -MARKER = 7 # lowest digit of price to identify bot's own orders +MARKER = conf['balancer_marker'] # lowest digit of price to identify bot's own orders COIN = 1E8 # number of satoshi per coin, this is a constant. def add_marker(price, marker): @@ -100,7 +111,7 @@ def slot_keypress(self, gox, (key)): if key == ord('o'): self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) for order in self.gox.orderbook.owns: - self.debug("[s] Order: price %f vol %f BTC type %s oid %s status %s" % (gox.quote2float(order.price), gox.base2float(order.volume), str(order.typ), str(order.oid), str(order.status))) + self.debug("[s] %s: %s: %s @ %s order id: %s" % (str(order.status), str(order.typ), gox.base2str(order.volume), gox.quote2str(order.price), str(order.oid))) if key == ord("r"): # manually rebalance with market order at current price @@ -186,7 +197,7 @@ def place_orders(self): if self.ask != 0 and self.gox.quote2float(next_sell) < self.ask: bad_next_sell = float(next_sell) next_sell = mark_own(self.gox.quote2int(self.ask) + (step / 2)) - self.debug("[s]corrected next sell at %f instead of %f, ask price at %f" % (bad_next_sell, self.gox.quote2float(next_sell), self.ask)) + self.debug("[s]corrected next sell at %f instead of %f, ask price at %f" % (self.gox.quote2float(next_sell), self.gox.quote2float(bad_next_sell), self.ask)) elif self.ask == 0: status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live @@ -194,7 +205,7 @@ def place_orders(self): if self.bid != 0 and self.gox.quote2float(next_buy) > self.bid: bad_next_buy = float(next_buy) next_buy = mark_own(self.gox.quote2int(self.bid) - (step / 2)) - self.debug("[s]corrected next buy at %f instead of %f, ask price at %f" % (bad_next_buy, self.gox.quote2float(next_buy), self.bid)) + self.debug("[s]corrected next buy at %f instead of %f, ask price at %f" % (self.gox.quote2float(next_buy), self.gox.quote2float(bad_next_buy), self.bid)) elif self.bid == 0: status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live diff --git a/buy.py b/buy.py index 77ef91d..b81d555 100644 --- a/buy.py +++ b/buy.py @@ -13,11 +13,11 @@ Activate this strategy's BUY functionality by switching 'simulate' to False Test first before enabling the BUY function! -Note: the goxtool.py application swallows most Python exceptions +Note: the goxtool.py application swallows most Python exceptions and outputs them to the status window and goxtool.log (in app folder). This complicates tracing of runtime errors somewhat, but -to keep an eye on such it is recommended that the developer runs -an additional terminal with 'tail -f ./goxtool.log' to see +to keep an eye on such it is recommended that the developer runs +an additional terminal with 'tail -f ./goxtool.log' to see continuous logfile output. coded by tarzan (c) April 2013, modified by caktux @@ -25,9 +25,19 @@ """ import goxapi +import simplejson as json + +# Load user.conf +conf = json.load(open("user.conf")) + +# Set defaults +conf.setdefault('buy_simulate', True) +conf.setdefault('buy_level', 1) +conf.setdefault('buy_volume', 1) +conf.setdefault('buy_alert', 100) # Simulate -simulate = True +simulate = conf['buy_simulate'] # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') @@ -36,10 +46,10 @@ global bidbuf, askbuf # comparators to avoid redundant bid/ask output bidbuf = 0 askbuf = 0 -buy_level = float(1) # price at which you want to buy BTC -threshold = float(100) # alert price distance from buy_level +buy_level = float(conf['buy_level']) # price at which you want to buy BTC +threshold = float(conf['buy_alert']) # alert price distance from buy_level buy_alert = float(buy_level + threshold) # alert level for user info -volume = float(1) # user specified fiat amout as volume, set 0 to use wallet full fiat balance +volume = float(conf['buy_volume']) # user specified fiat amount as volume, set to 0 to use full fiat balance class Strategy(goxapi.BaseObject): # pylint: disable=C0111,W0613,R0201 @@ -107,7 +117,7 @@ def slot_tick(self, gox, (bid, ask)): self.ask = gox.quote2float(ask) if self.ask > buy_level and self.ask < buy_alert: self.debug("[s] !!! buy ALERT @ %s; ask currently at %s" % (str(buy_alert), str(self.ask))) - self.debug("[s] !!! BUY for %f %s will trigger @ %f" % (buy_amount,str(self.gox.orderbook.gox.currency), buy_level)) + self.debug("[s] !!! BUY for %f %s will trigger @ %f" % (buy_amount, str(self.gox.orderbook.gox.currency), buy_level)) seen = 1 elif self.ask <= buy_level: # this is the condition to action gox.buy() @@ -144,7 +154,7 @@ def slot_userorder(self, gox, (price, volume, typ, oid, status)): # at once self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (gox.quote2float(price), str(volume), str(typ), str(oid), str(status))) # cancel by oid - if status not in ['pending','executing','post-pending','removed'] and oid not in self.existingorders: + if status not in ['pending', 'executing', 'post-pending', 'removed'] and oid not in self.existingorders: if gox.quote2float(price) == buy_level: self.gox.cancel(oid) diff --git a/sell.py b/sell.py index d079d6b..73a51b2 100644 --- a/sell.py +++ b/sell.py @@ -13,11 +13,11 @@ Activate this strategy's SELL functionality by switching 'simulate' to False Test first before enabling the SELL function! -Note: the goxtool.py application swallows most Python exceptions +Note: the goxtool.py application swallows most Python exceptions and outputs them to the status window and goxtool.log (in app folder). This complicates tracing of runtime errors somewhat, but -to keep an eye on such it is recommended that the developer runs -an additional terminal with 'tail -f ./goxtool.log' to see +to keep an eye on such it is recommended that the developer runs +an additional terminal with 'tail -f ./goxtool.log' to see continuous logfile output. coded by tarzan (c) April 2013, modified by caktux @@ -25,9 +25,19 @@ """ import goxapi +import simplejson as json + +# Load user.conf +conf = json.load(open("user.conf")) + +# Set defaults +conf.setdefault('sell_simulate', True) +conf.setdefault('sell_level', 10000000) +conf.setdefault('sell_volume', 0.1) +conf.setdefault('sell_alert', 100000) # Simulate -simulate = True +simulate = conf['sell_simulate'] # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') @@ -36,10 +46,10 @@ global bidbuf, askbuf # comparators to avoid redundant bid/ask output bidbuf = 0 askbuf = 0 -sell_level = float(10000000) # price at which you want to sell BTC -threshold = float(100000) # alert price distance from sell_level +sell_level = float(conf['sell_level']) # price at which you want to sell BTC +threshold = float(conf['sell_alert']) # alert price distance from sell_level sell_alert = float(sell_level - threshold) # alert level for user info -volume = float(0.1) # user specified BTC volume, set 0 to sell all BTC +volume = float(conf['sell_volume']) # user specified BTC volume, set 0 to sell all BTC class Strategy(goxapi.BaseObject): # pylint: disable=C0111,W0613,R0201 @@ -144,7 +154,7 @@ def slot_userorder(self, gox, (price, volume, typ, oid, status)): # at once self.debug("userorder message received: price %f volume %s typ %s oid %s status %s" % (gox.quote2float(price), str(volume), str(typ), str(oid), str(status))) # cancel by oid - if status not in ['pending','executing','post-pending','removed'] and oid not in self.existingorders: + if status not in ['pending', 'executing', 'post-pending', 'removed'] and oid not in self.existingorders: if gox.quote2float(price) == sell_level: self.gox.cancel(oid) From 054631684ca7a13d7fc18dec066daa4d29cd1127 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 6 Dec 2013 01:16:20 -0500 Subject: [PATCH 18/52] force data types read from user.conf --- balancer.py | 10 +++++----- buy.py | 2 +- sell.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/balancer.py b/balancer.py index 7202159..2cd8792 100644 --- a/balancer.py +++ b/balancer.py @@ -18,16 +18,16 @@ conf.setdefault('balancer_marker', 7) # Simulate -simulate = conf['balancer_simulate'] +simulate = bool(conf['balancer_simulate']) # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else '') -DISTANCE = conf['balancer_distance'] # percent price distance of next rebalancing orders -FIAT_COLD = conf['balancer_fiat_cold'] # Amount of Fiat stored at home but included in calculations -COIN_COLD = conf['balancer_coin_cold'] # Amount of Coin stored at home but included in calculations +DISTANCE = float(conf['balancer_distance']) # percent price distance of next rebalancing orders +FIAT_COLD = float(conf['balancer_fiat_cold']) # Amount of Fiat stored at home but included in calculations +COIN_COLD = float(conf['balancer_coin_cold']) # Amount of Coin stored at home but included in calculations -MARKER = conf['balancer_marker'] # lowest digit of price to identify bot's own orders +MARKER = int(conf['balancer_marker']) # lowest digit of price to identify bot's own orders COIN = 1E8 # number of satoshi per coin, this is a constant. def add_marker(price, marker): diff --git a/buy.py b/buy.py index b81d555..2776684 100644 --- a/buy.py +++ b/buy.py @@ -37,7 +37,7 @@ conf.setdefault('buy_alert', 100) # Simulate -simulate = conf['buy_simulate'] +simulate = bool(conf['buy_simulate']) # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') diff --git a/sell.py b/sell.py index 73a51b2..ab52207 100644 --- a/sell.py +++ b/sell.py @@ -37,7 +37,7 @@ conf.setdefault('sell_alert', 100000) # Simulate -simulate = conf['sell_simulate'] +simulate = bool(conf['sell_simulate']) # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') From 77f880612e8ee5c74eb1e8b6cbfaa6a384a3fb9c Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 6 Dec 2013 01:43:07 -0500 Subject: [PATCH 19/52] use int instead of bool for _simulate flags from user.conf --- balancer.py | 2 +- buy.py | 2 +- sell.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/balancer.py b/balancer.py index 2cd8792..1bcc940 100644 --- a/balancer.py +++ b/balancer.py @@ -18,7 +18,7 @@ conf.setdefault('balancer_marker', 7) # Simulate -simulate = bool(conf['balancer_simulate']) +simulate = int(conf['balancer_simulate']) # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else '') diff --git a/buy.py b/buy.py index 2776684..9d2061e 100644 --- a/buy.py +++ b/buy.py @@ -37,7 +37,7 @@ conf.setdefault('buy_alert', 100) # Simulate -simulate = bool(conf['buy_simulate']) +simulate = int(conf['buy_simulate']) # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') diff --git a/sell.py b/sell.py index ab52207..5a11c67 100644 --- a/sell.py +++ b/sell.py @@ -37,7 +37,7 @@ conf.setdefault('sell_alert', 100000) # Simulate -simulate = bool(conf['sell_simulate']) +simulate = int(conf['sell_simulate']) # Live or simulation notice simulate_or_live = ('SIMULATION - ' if simulate else 'LIVE - ') From 290d318f272978ae9ff282ddf97e7502b6383bea Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 6 Dec 2013 02:43:41 -0500 Subject: [PATCH 20/52] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4a8db21..21f96ee 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +#### Hosted version available at [https://zerogox.com](https://zerogox.com) + # Goxtool Goxtool is a Python trading client and auto-trading bot for the MtGox Bitcoin currency exchange. It is designed to work in the Linux console and has a curses user interface. It can display live streaming market data and you can buy and sell with keyboard commands. From cf7104f07e70dfa86fb631d1c6f9b9203fff30ca Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 6 Dec 2013 15:07:28 -0500 Subject: [PATCH 21/52] Merge balancer updates from prof7bit, improved distance calculation, optional trading fee compensation, forced price levels and logging --- balancer.py | 125 ++++++++++++++++++++++++++++++++++++++++++++++------ goxtool.py | 12 +++-- 2 files changed, 116 insertions(+), 21 deletions(-) diff --git a/balancer.py b/balancer.py index 1bcc940..8f76ea3 100644 --- a/balancer.py +++ b/balancer.py @@ -3,6 +3,11 @@ constant asset allocation ratio of exactly 50/50 = fiat/BTC """ +# line too long - pylint: disable=C0301 +# too many local variables - pylint: disable=R0914 + +import glob +import time import goxapi import strategy import simplejson as json @@ -16,12 +21,13 @@ conf.setdefault('balancer_fiat_cold', 0) conf.setdefault('balancer_coin_cold', 0) conf.setdefault('balancer_marker', 7) +conf.setdefault('balancer_compensate_fees', False) # Simulate -simulate = int(conf['balancer_simulate']) +SIMULATE = int(conf['balancer_simulate']) # Live or simulation notice -simulate_or_live = ('SIMULATION - ' if simulate else '') +SIMULATE_OR_LIVE = 'SIMULATION - ' if SIMULATE else '' DISTANCE = float(conf['balancer_distance']) # percent price distance of next rebalancing orders FIAT_COLD = float(conf['balancer_fiat_cold']) # Amount of Fiat stored at home but included in calculations @@ -46,6 +52,11 @@ def is_own(price): """return true if this price has our own marker""" return has_marker(price, MARKER) +def write_log(txt): + """write line to a separate logfile""" + with open("_balancer.log", "a") as logfile: + logfile.write(txt + "\n") + class Strategy(strategy.Strategy): """a portfolio rebalancing bot""" @@ -53,7 +64,7 @@ def __init__(self, gox): strategy.Strategy.__init__(self, gox) self.bid = 0 self.ask = 0 - self.simulate_or_live = simulate_or_live + self.simulate_or_live = SIMULATE_OR_LIVE self.distance = DISTANCE self.init_distance = float(DISTANCE) self.temp_halt = False @@ -102,11 +113,23 @@ def slot_keypress(self, gox, (key)): # current status (how much currently out of balance) price = (gox.orderbook.bid + gox.orderbook.ask) / 2 vol_buy = self.get_buy_at_price(price) + price_balanced = self.get_price_where_it_was_balanced() + step_factor = 1 + self.distance / 100.0 + price_sell = self.get_next_sell_price(price_balanced, step_factor) + price_buy = self.get_next_buy_price(price_balanced, step_factor) + self.debug("[s]BTC difference at current price:", gox.base2float(vol_buy)) self.debug("[s]Price where it would be balanced:", gox.quote2float(price_balanced)) + self.debug("[s]Next two orders would be at:", + gox.quote2float(price_sell), + gox.quote2float(price_buy)) + + vol = gox.base2float(gox.monthly_volume) + fee = gox.trade_fee + self.debug("monthly volume: %g / trade fee: %g%%" % (vol, fee)) if key == ord('o'): self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) @@ -125,14 +148,14 @@ def slot_keypress(self, gox, (key)): self.simulate_or_live, gox.base2float(vol_buy), gox.quote2float(price))) - if simulate == False: + if SIMULATE == False: gox.buy(0, vol_buy) else: self.debug("[s]%sselling %f at market price of %f" % ( self.simulate_or_live, gox.base2float(-vol_buy), gox.quote2float(price))) - if simulate == False: + if SIMULATE == False: gox.sell(0, -vol_buy) def cancel_orders(self): @@ -144,7 +167,7 @@ def cancel_orders(self): must_cancel.append(order) for order in must_cancel: - if (simulate == False): + if (SIMULATE == False): self.gox.cancel(order.oid) def get_price_where_it_was_balanced(self): @@ -164,17 +187,32 @@ def get_price_where_it_was_balanced(self): return gox.quote2int(fiat_have / btc_have) def get_buy_at_price(self, price_int): - """calculate amount of BTC needed to buy at price to achieve - rebalancing. price and return value are in mtgox integer format""" + """calculate amount of BTC needed to buy at price to achieve rebalancing. + Negative return value means we need to sell. price and return value is + in MtGox integer format""" + fiat_have = self.gox.quote2float(self.gox.wallet[self.gox.curr_quote]) + FIAT_COLD btc_value_then = self.get_btc_value(price_int) price_then = self.gox.quote2float(price_int) diff = fiat_have - btc_value_then diff_btc = diff / price_then must_buy = diff_btc / 2 - return self.gox.base2int(must_buy) + + # Now compensate the fees: if its a buy then buy a little bit more, + # if its a sell (must_buy is negative) then sell a little bit more. + # We only add half of the fee to distribute it 50/50 to both balances. + # (for this to work the MtGox fee settings must be at default: take + # the fee from BTC after buying and take it from USD after selling) + if conf['balancer_compensate_fees']: + must_buy *= (1 + self.gox.trade_fee / 200) + + # convert into satoshi integer + must_buy_int = self.gox.base2int(must_buy) + + return must_buy_int def get_btc_value(self, price_int): + """get total btc value in fiat at current price""" btc_have = self.gox.base2float(self.gox.wallet[self.gox.curr_base]) + COIN_COLD price_then = self.gox.quote2float(price_int) btc_value_then = btc_have * price_then @@ -188,14 +226,17 @@ def place_orders(self): else: return - step = int(center * self.distance / 100.0) - next_sell = mark_own(center + step) - next_buy = mark_own(center - step) + step_factor = 1 + self.distance / 100.0 + + next_sell = self.get_next_sell_price(center, step_factor) + next_buy = self.get_next_buy_price(center, step_factor) + status_prefix = self.simulate_or_live # Protect against selling below current ask price if self.ask != 0 and self.gox.quote2float(next_sell) < self.ask: bad_next_sell = float(next_sell) + step = int(center * self.distance / 100.0) next_sell = mark_own(self.gox.quote2int(self.ask) + (step / 2)) self.debug("[s]corrected next sell at %f instead of %f, ask price at %f" % (self.gox.quote2float(next_sell), self.gox.quote2float(bad_next_sell), self.ask)) elif self.ask == 0: @@ -204,6 +245,7 @@ def place_orders(self): # Protect against buying above current bid price if self.bid != 0 and self.gox.quote2float(next_buy) > self.bid: bad_next_buy = float(next_buy) + step = int(center * self.distance / 100.0) next_buy = mark_own(self.gox.quote2int(self.bid) - (step / 2)) self.debug("[s]corrected next buy at %f instead of %f, ask price at %f" % (self.gox.quote2float(next_buy), self.gox.quote2float(bad_next_buy), self.bid)) elif self.bid == 0: @@ -225,7 +267,7 @@ def place_orders(self): self.gox.base2float(buy_amount), self.gox.quote2float(next_buy) )) - if simulate == False and self.ask != 0: + if SIMULATE == False and self.ask != 0: self.gox.buy(next_buy, buy_amount) self.debug("[s]%snew sell order %f at %f" % ( @@ -233,9 +275,18 @@ def place_orders(self): self.gox.base2float(sell_amount), self.gox.quote2float(next_sell) )) - if simulate == False and self.ask != 0: + if SIMULATE == False and self.ask != 0: self.gox.sell(next_sell, sell_amount) + # write some account information to a separate log file + datetime = time.strftime("%Y-%m-%d %H:%M", time.localtime()) + write_log('"%s", %f, %f, %s' % ( + datetime, + self.gox.quote2float(center), + self.gox.quote2float(self.gox.wallet[self.gox.curr_quote]) + FIAT_COLD, + self.gox.base2float(self.gox.wallet[self.gox.curr_base]) + COIN_COLD + )) + def slot_tick(self, gox, (bid, ask)): # Set last bid/ask price self.bid = goxapi.int2float(bid, self.gox.orderbook.gox.currency) @@ -270,6 +321,13 @@ def check_trades(self): if self.temp_halt: return + # right after initial connection we have no + # wallet yet, we cannot trade anyways without that, + # must wait until private/info is received. + if self.gox.wallet == {}: + self.debug('[s]Waiting for info...') + return + # still waiting for submitted orders, # can wait for next signal if self.gox.count_submitted: @@ -297,3 +355,42 @@ def check_trades(self): if count == 1: self.cancel_orders() self.place_orders() + + def get_next_buy_price(self, center, step_factor): + """get the next buy price. If there is a forced price level + then it will return that, otherwise return center - step""" + price = self.get_forced_price(center, False) + if not price: + price = mark_own(int(round(center / step_factor))) + return price + + def get_next_sell_price(self, center, step_factor): + """get the next sell price. If there is a forced price level + then it will return that, otherwise return center + step""" + price = self.get_forced_price(center, True) + if not price: + price = mark_own(int(round(center * step_factor))) + return price + + def get_forced_price(self, center, need_ask): + """get externally forced price level for order""" + prices = [] + found = glob.glob("_balancer_force_*") + if len(found): + for name in found: + try: + price = self.gox.quote2int(float(name.split("_")[3])) + prices.append(price) + except: #pylint: disable=W0702 + pass + prices.sort() + if need_ask: + for price in prices: + if price > center * 1.005: + return mark_own(price) + else: + for price in reversed(prices): + if price < center * 0.995: + return mark_own(price) + + return None diff --git a/goxtool.py b/goxtool.py index 4da949e..cb53e72 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1548,8 +1548,8 @@ def set_ini(gox, setting, value, signal, signal_sender, signal_params): def main(): """main funtion, called at the start of the program""" - debug_tb = [] + def curses_loop(stdscr): """Only the code inside this function runs within the curses wrapper""" @@ -1562,6 +1562,7 @@ def curses_loop(stdscr): # we can print them. try: init_colors() + gox = goxapi.Gox(secret, config) logwriter = LogWriter(gox) @@ -1576,6 +1577,7 @@ def curses_loop(stdscr): strategy_manager = StrategyManager(gox, strat_mod_list) gox.start() + while True: key = stdscr.getch() if key == ord("q"): @@ -1643,6 +1645,7 @@ def curses_loop(stdscr): # Before we do anything we dump stacktraces of all currently running # threads to a separate logfile because this helps debugging freezes # and deadlocks that might occur if things went totally wrong. + with open("goxtool.stacktrace.log", "w") as stacklog: stacklog.write(dump_all_stacks()) @@ -1741,14 +1744,9 @@ def curses_loop(stdscr): # if its ok then we can finally enter the curses main loop if secret.prompt_decrypt() != secret.S_FAIL_FATAL: - - ### - # - # now going to enter cbreak mode and start the curses loop... + # Use curses wrapper curses.wrapper(curses_loop) # curses ended, terminal is back in normal (cooked) mode - # - ### if len(debug_tb): print "\n\n*** error(s) in curses_loop() that caused unclean shutdown:\n" From 7f421c82964c6c8452e40fc3217efcac93f2de1d Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 6 Dec 2013 16:58:50 -0500 Subject: [PATCH 22/52] Show monthly volume and trade fee in PluginConsole --- balancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer.py b/balancer.py index 8f76ea3..707be94 100644 --- a/balancer.py +++ b/balancer.py @@ -129,7 +129,7 @@ def slot_keypress(self, gox, (key)): vol = gox.base2float(gox.monthly_volume) fee = gox.trade_fee - self.debug("monthly volume: %g / trade fee: %g%%" % (vol, fee)) + self.debug("[s]Monthly volume: %g BTC / trade fee: %g%%" % (vol, fee)) if key == ord('o'): self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) From 55f9954427770a8d17bf47eb48700211feca0e85 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 6 Dec 2013 17:03:38 -0500 Subject: [PATCH 23/52] use int() for balancer_compensate_fees from conf --- balancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer.py b/balancer.py index 707be94..4e432d0 100644 --- a/balancer.py +++ b/balancer.py @@ -203,7 +203,7 @@ def get_buy_at_price(self, price_int): # We only add half of the fee to distribute it 50/50 to both balances. # (for this to work the MtGox fee settings must be at default: take # the fee from BTC after buying and take it from USD after selling) - if conf['balancer_compensate_fees']: + if int(conf['balancer_compensate_fees']): must_buy *= (1 + self.gox.trade_fee / 200) # convert into satoshi integer From acc89f0659e793dd613aaead32d973c845cd24ba Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 7 Dec 2013 01:10:28 -0500 Subject: [PATCH 24/52] buy more, sell less, and check for 0.011 buy amount instead of just correcting the minimum 0.01 --- balancer.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/balancer.py b/balancer.py index 4e432d0..7b1c71c 100644 --- a/balancer.py +++ b/balancer.py @@ -118,6 +118,8 @@ def slot_keypress(self, gox, (key)): step_factor = 1 + self.distance / 100.0 price_sell = self.get_next_sell_price(price_balanced, step_factor) price_buy = self.get_next_buy_price(price_balanced, step_factor) + sell_amount = -self.get_buy_at_price(price_sell) + buy_amount = self.get_buy_at_price(price_buy) self.debug("[s]BTC difference at current price:", gox.base2float(vol_buy)) @@ -125,7 +127,9 @@ def slot_keypress(self, gox, (key)): gox.quote2float(price_balanced)) self.debug("[s]Next two orders would be at:", gox.quote2float(price_sell), - gox.quote2float(price_buy)) + self.gox.base2float(sell_amount), + gox.quote2float(price_buy), + self.gox.base2float(buy_amount)) vol = gox.base2float(gox.monthly_volume) fee = gox.trade_fee @@ -199,12 +203,17 @@ def get_buy_at_price(self, price_int): must_buy = diff_btc / 2 # Now compensate the fees: if its a buy then buy a little bit more, - # if its a sell (must_buy is negative) then sell a little bit more. + # if its a sell (must_buy is negative) then sell a little bit less. # We only add half of the fee to distribute it 50/50 to both balances. # (for this to work the MtGox fee settings must be at default: take # the fee from BTC after buying and take it from USD after selling) if int(conf['balancer_compensate_fees']): - must_buy *= (1 + self.gox.trade_fee / 200) + # Buy a little bit more + if must_buy > 0: + must_buy *= (1 + self.gox.trade_fee / 200) + # Sell a little bit less + else: + must_buy = must_buy * 2 + (-must_buy * (1 + 0.6 / 200)) # convert into satoshi integer must_buy_int = self.gox.base2int(must_buy) @@ -258,7 +267,7 @@ def place_orders(self): sell_amount = int(0.01 * COIN) self.debug("[s]WARNING! minimal sell amount adjusted to 0.01") - if buy_amount < 0.01 * COIN: + if buy_amount < 0.011 * COIN: buy_amount = int(0.011 * COIN) self.debug("[s]WARNING! minimal buy amount adjusted to 0.011") From e9c8115880be07e9e9dc9bee5c8fb47b2177fca8 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 7 Dec 2013 21:13:19 -0500 Subject: [PATCH 25/52] Add balancer target margin, BTC total with current market in status and clearer order/socket lag format --- balancer.py | 13 ++++++++++++- goxtool.py | 12 ++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/balancer.py b/balancer.py index 7b1c71c..5c96bb2 100644 --- a/balancer.py +++ b/balancer.py @@ -22,6 +22,7 @@ conf.setdefault('balancer_coin_cold', 0) conf.setdefault('balancer_marker', 7) conf.setdefault('balancer_compensate_fees', False) +conf.setdefault('balancer_target_margin', 1) # Simulate SIMULATE = int(conf['balancer_simulate']) @@ -213,7 +214,17 @@ def get_buy_at_price(self, price_int): must_buy *= (1 + self.gox.trade_fee / 200) # Sell a little bit less else: - must_buy = must_buy * 2 + (-must_buy * (1 + 0.6 / 200)) + must_buy = must_buy * 2 + (-must_buy * (1 + self.gox.trade_fee / 200)) + + # Apply the same logic for target margin + target_margin = float(conf['balancer_target_margin']) + if target_margin: + # Buy a little bit more for profit + if must_buy > 0: + must_buy *= (1 + target_margin / 200) + # Sell a little bit less for profit + else: + must_buy = must_buy * 2 + (-must_buy * (1 + target_margin / 200)) # convert into satoshi integer must_buy_int = self.gox.base2int(must_buy) diff --git a/goxtool.py b/goxtool.py index cb53e72..1889c78 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1003,6 +1003,7 @@ def paint(self): self.addstr(" | Account: ", COLOR_PAIR["status_text"]) if len(self.sorted_currency_list): own_currencies = [] + total_btc = 0 for currency in self.sorted_currency_list: if currency in self.gox.wallet: own_currencies.append(currency) @@ -1010,8 +1011,14 @@ def paint(self): self.addstr("%s" % own_currency, COLOR_PAIR["status_text"] + curses.A_BOLD) self.addstr(" ", COLOR_PAIR["status_text"]) self.addstr("%f" % goxapi.int2float(self.gox.wallet[own_currency], own_currency), COLOR_PAIR["status_text"] + curses.A_BOLD) + if own_currency == 'BTC': + total_btc += self.gox.base2float(self.gox.wallet[own_currency]) + elif own_currency == cquote: + total_btc += self.gox.quote2float(self.gox.wallet[own_currency]) / self.gox.quote2float(self.gox.orderbook.ask) if (c + 1 != len(own_currencies)): self.addstr(" + ", COLOR_PAIR["status_text"]) + self.addstr(" | %s%s total: " % (cbase, cquote), COLOR_PAIR["status_text"]) + self.addstr("%f BTC" % total_btc, COLOR_PAIR["status_text"] + curses.A_BOLD) self.addstr(" | Fee: ", COLOR_PAIR["status_text"]) self.addstr("%s" % self.gox.trade_fee, COLOR_PAIR["status_text"] + curses.A_BOLD) self.addstr(" %", COLOR_PAIR["status_text"]) @@ -1035,8 +1042,9 @@ def paint(self): line2 += "sum_ask: %s %s | " % (str_btc, cbase) line2 += "ratio: %s %s/%s | " % (str_ratio, cquote, cbase) - line2 += "o_lag: %s | " % self.order_lag_txt - line2 += "s_lag: %.3f s" % (self.gox.socket_lag / 1e6) + line2 += "lag: %s / " % self.order_lag_txt + line2 += "%.3f s " % (self.gox.socket_lag / 1e6) + line2 += "(order / socket)" # self.addstr(0, 0, line1, COLOR_PAIR["status_text"]) self.addstr(1, 0, line2, COLOR_PAIR["status_text"]) From e2f939b0639af175feae03c66f971112b4ad896f Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 7 Dec 2013 22:04:29 -0500 Subject: [PATCH 26/52] don't show buy/sell strategy info on every tick --- buy.py | 4 ++-- sell.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/buy.py b/buy.py index 9d2061e..8a0dbf4 100644 --- a/buy.py +++ b/buy.py @@ -125,9 +125,9 @@ def slot_tick(self, gox, (bid, ask)): self.gox.buy(self.ask, buy_amount) self.debug("[s] >>> %sBUY BTC @ %s; ask currently at %s" % (simulate_or_live, str(buy_level), str(self.ask))) seen = 1 - if seen == 0: + # if seen == 0: # no conditions met above, so give the user default info - self.debug("Buy level @ %s (alert: %s); ask @ %s" % (str(buy_level), str(buy_alert), str(self.ask))) + # self.debug("Buy level @ %s (alert: %s); ask @ %s" % (str(buy_level), str(buy_alert), str(self.ask))) # is the updated tick different from previous? if bid != bidbuf: bidbuf = bid diff --git a/sell.py b/sell.py index 5a11c67..2c3b09a 100644 --- a/sell.py +++ b/sell.py @@ -125,9 +125,9 @@ def slot_tick(self, gox, (bid, ask)): self.gox.sell(self.bid, sell_amount) self.debug("[s] >>> %sSELL BTC @ %s; bid currently at %s" % (simulate_or_live, str(sell_level), str(self.bid))) seen = 1 - if seen == 0: + # if seen == 0: # no conditions met above, so give the user default info - self.debug("Sell level @ %s (alert: %s); bid @ %s" % (str(sell_level), str(sell_alert), str(self.bid))) + # self.debug("Sell level @ %s (alert: %s); bid @ %s" % (str(sell_level), str(sell_alert), str(self.bid))) # is the updated tick different from previous? if bid != bidbuf: bidbuf = bid From dc01f1e5abdfc8d3c283b47026b74a6126be12c3 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 7 Dec 2013 22:33:17 -0500 Subject: [PATCH 27/52] INS or = to select, Macs don't have an Insert key --- goxtool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/goxtool.py b/goxtool.py index 1889c78..9f0e161 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1152,7 +1152,7 @@ def modal(self): self.down(1) if key_pressed == curses.KEY_UP: self.down(-1) - if key_pressed == curses.KEY_IC: + if key_pressed in [curses.KEY_IC, ord("=")]: self.toggle_select() self.down(1) @@ -1170,7 +1170,7 @@ class DlgCancelOrders(DlgListItems): """modal dialog to cancel orders""" def __init__(self, stdscr, gox): self.gox = gox - hlp = [("INS", "select"), ("F8", "cancel selected"), ("F10", "exit")] + hlp = [("INS or =", "select"), ("F8", "cancel selected"), ("F10", "exit")] keys = [(curses.KEY_F8, self._do_cancel)] DlgListItems.__init__(self, stdscr, 45, "Cancel order(s)", hlp, keys) From ab78dffa1cec2b7ae743b6c8f5b1f812cfbd0ebf Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 7 Dec 2013 22:38:26 -0500 Subject: [PATCH 28/52] fit key help in window, update readme --- README.md | 2 +- goxtool.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21f96ee..5c87f07 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Keyboard commands for trading: - F5 : New sell order - F6 : View orders / cancel order(s) -In the cancel dialog you can move up/down with the arrow keys, use INS to select/unselect orders (you can select multiple orders and cancel them all at once) or if you just quickly want to cancel only one order just highlight to the order and hit F8. It behaves a little bit like deleting files in midnight commander. +In the cancel dialog you can move up/down with the arrow keys, use INS or = to select/unselect orders (you can select multiple orders and cancel them all at once) or if you just quickly want to cancel only one order just highlight to the order and hit F8. It behaves a little bit like deleting files in midnight commander. When entering a new order you can move between the fields with up/down keys or move to the next field with tab or enter (but only if you entered a valid number into the previous field, decimal separator is . (not comma, even on European computers), send the order with enter. diff --git a/goxtool.py b/goxtool.py index 9f0e161..28686d5 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1170,7 +1170,7 @@ class DlgCancelOrders(DlgListItems): """modal dialog to cancel orders""" def __init__(self, stdscr, gox): self.gox = gox - hlp = [("INS or =", "select"), ("F8", "cancel selected"), ("F10", "exit")] + hlp = [("INS / =", "select"), ("F8", "cancel selected"), ("F10", "exit")] keys = [(curses.KEY_F8, self._do_cancel)] DlgListItems.__init__(self, stdscr, 45, "Cancel order(s)", hlp, keys) From ab182a4e83150ed7dff4833993bc860c84ffc6bd Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Wed, 11 Dec 2013 16:47:25 -0500 Subject: [PATCH 29/52] revert to buy more, sell more and correct next_buy and next_sell to bid/ask +/- marker --- balancer.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/balancer.py b/balancer.py index 5c96bb2..ff05db6 100644 --- a/balancer.py +++ b/balancer.py @@ -204,27 +204,17 @@ def get_buy_at_price(self, price_int): must_buy = diff_btc / 2 # Now compensate the fees: if its a buy then buy a little bit more, - # if its a sell (must_buy is negative) then sell a little bit less. + # if its a sell (must_buy is negative) then sell a little bit more. # We only add half of the fee to distribute it 50/50 to both balances. # (for this to work the MtGox fee settings must be at default: take # the fee from BTC after buying and take it from USD after selling) if int(conf['balancer_compensate_fees']): - # Buy a little bit more - if must_buy > 0: - must_buy *= (1 + self.gox.trade_fee / 200) - # Sell a little bit less - else: - must_buy = must_buy * 2 + (-must_buy * (1 + self.gox.trade_fee / 200)) + must_buy *= (1 + self.gox.trade_fee / 200) # Apply the same logic for target margin target_margin = float(conf['balancer_target_margin']) if target_margin: - # Buy a little bit more for profit - if must_buy > 0: - must_buy *= (1 + target_margin / 200) - # Sell a little bit less for profit - else: - must_buy = must_buy * 2 + (-must_buy * (1 + target_margin / 200)) + must_buy *= (1 + target_margin / 200) # convert into satoshi integer must_buy_int = self.gox.base2int(must_buy) @@ -257,7 +247,7 @@ def place_orders(self): if self.ask != 0 and self.gox.quote2float(next_sell) < self.ask: bad_next_sell = float(next_sell) step = int(center * self.distance / 100.0) - next_sell = mark_own(self.gox.quote2int(self.ask) + (step / 2)) + next_sell = mark_own(self.gox.quote2int(self.ask)) self.debug("[s]corrected next sell at %f instead of %f, ask price at %f" % (self.gox.quote2float(next_sell), self.gox.quote2float(bad_next_sell), self.ask)) elif self.ask == 0: status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live @@ -266,8 +256,8 @@ def place_orders(self): if self.bid != 0 and self.gox.quote2float(next_buy) > self.bid: bad_next_buy = float(next_buy) step = int(center * self.distance / 100.0) - next_buy = mark_own(self.gox.quote2int(self.bid) - (step / 2)) - self.debug("[s]corrected next buy at %f instead of %f, ask price at %f" % (self.gox.quote2float(next_buy), self.gox.quote2float(bad_next_buy), self.bid)) + next_buy = mark_own(self.gox.quote2int(self.bid)) + self.debug("[s]corrected next buy at %f instead of %f, bid price at %f" % (self.gox.quote2float(next_buy), self.gox.quote2float(bad_next_buy), self.bid)) elif self.bid == 0: status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live From f2da6fab530ef50503bc45ea2894d11b4cd30621 Mon Sep 17 00:00:00 2001 From: gwdp Date: Wed, 11 Dec 2013 22:35:33 -0200 Subject: [PATCH 30/52] typo --- goxapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goxapi.py b/goxapi.py index c66522c..be55fcd 100644 --- a/goxapi.py +++ b/goxapi.py @@ -1229,7 +1229,7 @@ def read_block(sock): class PubnubClient(BaseClient): - """"this implements the pubnub client. + """this implements the pubnub client. THIS IS ALL INCOMPLETE AND ITS A TOTAL MESS BECAUSE I NEEDED TO HACK THIS IN A HURRY From ec75c64a0694a08a25aa29d9db814ae5a540a4ac Mon Sep 17 00:00:00 2001 From: gwdp Date: Wed, 11 Dec 2013 22:37:11 -0200 Subject: [PATCH 31/52] add `pubnub` option into args description --- goxtool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goxtool.py b/goxtool.py index 2a97e97..d388bae 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1638,7 +1638,7 @@ def curses_loop(stdscr): argp.add_argument('--strategy', action="store", default="strategy.py", help="name of strategy module files, comma separated list, default=strategy.py") argp.add_argument('--protocol', action="store", default="", - help="force protocol (socketio or websocket), ignore setting in .ini") + help="force protocol (socketio, websocket or pubnub), ignore setting in .ini") argp.add_argument('--no-fulldepth', action="store_true", default=False, help="do not download full depth (useful for debugging)") argp.add_argument('--no-depth', action="store_true", default=False, From 04e41b6c0bf03195f282459d0f92c180d671850b Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Wed, 11 Dec 2013 20:13:16 -0500 Subject: [PATCH 32/52] default to pubnub, update readme and typo --- README.md | 7 +++---- goxapi.py | 8 +++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5c87f07..b39ce7d 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,7 @@ Keyboard commands (only the ones useful in view-only mode, without Mt.Gox accoun (There will be even more commands once you connect it to your Mt.Gox account) -There is also a goxtool.ini file, it will be created on the first start. In the .ini file there are some parameters you can change, for example the currency you want to trade BTC against or some parameters regarding the network protocol. Some of the .ini settings can be overridden by command line options (use the --help option to see a list). The default protocol is websocket, the alternative would be socketio, goxtool implements both protocols, the websocket server is currently more reliable. There is also an option to use the http API for trading commands, the default is to send all commands to the streaming socket, this affects only what happens when you buy/sell/cancel, it does not affect the streaming update of order book and chart. -socketio or websocket? Which one is worse? +There is also a goxtool.ini file, it will be created on the first start. In the .ini file there are some parameters you can change, for example the currency you want to trade BTC against or some parameters regarding the network protocol. Some of the .ini settings can be overridden by command line options (use the --help option to see a list). The default protocol is now PubNub, with websocket and socketio as alternatives. Goxtool implements all three protocols with PubNub currently being more reliable. There is also an option to use the http API for trading commands with websocket and socketio, the default is to send all commands to the streaming socket when using websocket, this affects only what happens when you buy/sell/cancel, it does not affect the streaming update of order book and chart. The two options are socketio or websocket, the .ini setting for this is use_plain_old_websocket. To force it connecting with socketio: @@ -57,9 +56,9 @@ The two options are socketio or websocket, the .ini setting for this is use_plai ./goxtool.py --protocol=websocket ``` -It has turned out that websocket is currently the most reliable protocol. These options on the command line take precedence over what you have configured in the .ini file (but it won't change your .ini), you can make websocket the default (so you don't need this option anymore) by editing the ini file. +It has turned out that websocket is currently the most reliable protocol after pubnub. These options on the command line take precedence over what you have configured in the .ini file (but it won't change your .ini), you can make websocket the default (so you don't need this option anymore) by editing the ini file. -If you experience a high lag between sending an order and the ack (the op:result) not appearing immediately which is happening during times of really high volume then you should consider also adding --use-http to make it send trading commands via http. Http makes it slightly slower to execute many trades in a row but has been much more reliable when mtgox was under heavy load or ddos or similar. If you don't need to send many orders very fast then this option won't hurt. --use-http can be combined with either socketio or websocket. +If you experience a high lag between sending an order and the ack (the op:result) not appearing immediately which is happening during times of really high volume then you should consider also adding --use-http to make it send trading commands via http. Http makes it slightly slower to execute many trades in a row but has been much more reliable when mtgox was under heavy load or ddos or similar. If you don't need to send many orders very fast then this option won't hurt. --use-http can be combined with either socketio or websocket and is forced for PubNub. The following is what I am currently using and I recommend it: diff --git a/goxapi.py b/goxapi.py index be55fcd..4a87c58 100644 --- a/goxapi.py +++ b/goxapi.py @@ -231,7 +231,7 @@ class GoxConfig(SafeConfigParser): _DEFAULTS = [["gox", "base_currency", "BTC"] ,["gox", "quote_currency", "USD"] ,["gox", "use_ssl", "True"] - ,["gox", "use_plain_old_websocket", "True"] + ,["gox", "use_plain_old_websocket", "False"] ,["gox", "use_http_api", "True"] ,["gox", "use_tonce", "False"] ,["gox", "load_fulldepth", "True"] @@ -1325,7 +1325,7 @@ def _sub_thread(self, chan, name): self.debug("### conection lost: %s" % name) def _pubnub_receive(self, msg): - """callback method called by pubnub wen a message is received""" + """callback method called by pubnub when a message is received""" self.signal_recv(self, msg) self._time_last_received = time.time() return True @@ -1496,12 +1496,14 @@ def __init__(self, secret, config): self.orderbook.signal_debug.connect(self.signal_debug) use_websocket = self.config.get_bool("gox", "use_plain_old_websocket") - use_pubnub = False + use_pubnub = True if "socketio" in FORCE_PROTOCOL: use_websocket = False + use_pubnub = False if "websocket" in FORCE_PROTOCOL: use_websocket = True + use_pubnub = False if "pubnub" in FORCE_PROTOCOL: use_websocket = False use_pubnub = True From 70fab7a9c5597d606199419139b2b305504b0380 Mon Sep 17 00:00:00 2001 From: gwdp Date: Wed, 11 Dec 2013 23:56:21 -0200 Subject: [PATCH 33/52] this fixes backspace on macosx -- #11 --- goxtool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/goxtool.py b/goxtool.py index 2a97e97..8c98920 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1189,6 +1189,8 @@ def validator(self, char): if char in [10, 13, curses.KEY_ENTER, curses.ascii.BEL]: self.result = 10 return curses.ascii.BEL + if char == 127: + char = curses.KEY_BACKSPACE if char in [27, curses.KEY_F10]: self.result = -1 return curses.ascii.BEL From 22a0d4de754ee7da9d560794fccd9bb0ee8e8a22 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Wed, 11 Dec 2013 21:04:07 -0500 Subject: [PATCH 34/52] add total fiat in status --- goxtool.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/goxtool.py b/goxtool.py index 346b391..cfcf8b3 100755 --- a/goxtool.py +++ b/goxtool.py @@ -1004,6 +1004,7 @@ def paint(self): if len(self.sorted_currency_list): own_currencies = [] total_btc = 0 + total_fiat = 0 for currency in self.sorted_currency_list: if currency in self.gox.wallet: own_currencies.append(currency) @@ -1011,14 +1012,18 @@ def paint(self): self.addstr("%s" % own_currency, COLOR_PAIR["status_text"] + curses.A_BOLD) self.addstr(" ", COLOR_PAIR["status_text"]) self.addstr("%f" % goxapi.int2float(self.gox.wallet[own_currency], own_currency), COLOR_PAIR["status_text"] + curses.A_BOLD) - if own_currency == 'BTC': - total_btc += self.gox.base2float(self.gox.wallet[own_currency]) - elif own_currency == cquote: - total_btc += self.gox.quote2float(self.gox.wallet[own_currency]) / self.gox.quote2float(self.gox.orderbook.ask) + if own_currency == 'BTC' and self.gox.wallet and self.gox.orderbook.ask: + total_btc += self.gox.base2float(self.gox.wallet['BTC']) + total_fiat += self.gox.base2float(self.gox.wallet['BTC']) * self.gox.orderbook.ask + elif own_currency == cquote and self.gox.wallet and self.gox.orderbook.bid: + total_fiat += float(self.gox.wallet[own_currency]) + total_btc += self.gox.quote2float(self.gox.wallet[own_currency]) / self.gox.quote2float(self.gox.orderbook.bid) if (c + 1 != len(own_currencies)): self.addstr(" + ", COLOR_PAIR["status_text"]) self.addstr(" | %s%s total: " % (cbase, cquote), COLOR_PAIR["status_text"]) self.addstr("%f BTC" % total_btc, COLOR_PAIR["status_text"] + curses.A_BOLD) + self.addstr(" / ", COLOR_PAIR["status_text"]) + self.addstr("%f %s" % (self.gox.quote2float(total_fiat), cquote), COLOR_PAIR["status_text"] + curses.A_BOLD) self.addstr(" | Fee: ", COLOR_PAIR["status_text"]) self.addstr("%s" % self.gox.trade_fee, COLOR_PAIR["status_text"] + curses.A_BOLD) self.addstr(" %", COLOR_PAIR["status_text"]) From f1d0499135e578060171465594865f16e698a299 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Thu, 12 Dec 2013 01:42:25 -0500 Subject: [PATCH 35/52] Compensate fees on price instead of volume and apply target margin when correcting prices only --- balancer.py | 55 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/balancer.py b/balancer.py index ff05db6..d9a5b5f 100644 --- a/balancer.py +++ b/balancer.py @@ -203,19 +203,6 @@ def get_buy_at_price(self, price_int): diff_btc = diff / price_then must_buy = diff_btc / 2 - # Now compensate the fees: if its a buy then buy a little bit more, - # if its a sell (must_buy is negative) then sell a little bit more. - # We only add half of the fee to distribute it 50/50 to both balances. - # (for this to work the MtGox fee settings must be at default: take - # the fee from BTC after buying and take it from USD after selling) - if int(conf['balancer_compensate_fees']): - must_buy *= (1 + self.gox.trade_fee / 200) - - # Apply the same logic for target margin - target_margin = float(conf['balancer_target_margin']) - if target_margin: - must_buy *= (1 + target_margin / 200) - # convert into satoshi integer must_buy_int = self.gox.base2int(must_buy) @@ -243,11 +230,20 @@ def place_orders(self): status_prefix = self.simulate_or_live + target_margin = float(conf['balancer_target_margin']) + # Protect against selling below current ask price if self.ask != 0 and self.gox.quote2float(next_sell) < self.ask: bad_next_sell = float(next_sell) - step = int(center * self.distance / 100.0) - next_sell = mark_own(self.gox.quote2int(self.ask)) + + # step = int(center * self.distance / 100.0) + + # Apply target margin to corrected sell price + if target_margin: + next_sell = mark_own(int(round(self.gox.quote2int(self.ask) * (1 + target_margin / 100)))) + else: + next_sell = mark_own(self.gox.quote2int(self.ask)) + self.debug("[s]corrected next sell at %f instead of %f, ask price at %f" % (self.gox.quote2float(next_sell), self.gox.quote2float(bad_next_sell), self.ask)) elif self.ask == 0: status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live @@ -255,8 +251,15 @@ def place_orders(self): # Protect against buying above current bid price if self.bid != 0 and self.gox.quote2float(next_buy) > self.bid: bad_next_buy = float(next_buy) - step = int(center * self.distance / 100.0) - next_buy = mark_own(self.gox.quote2int(self.bid)) + + # step = int(center * self.distance / 100.0) + + # Apply target margin to corrected buy price + if target_margin: + next_buy = mark_own(int(round(self.gox.quote2int(self.bid) * 2 - (self.gox.quote2int(self.bid) * (1 + target_margin / 100))))) + else: + next_buy = mark_own(self.gox.quote2int(self.bid)) + self.debug("[s]corrected next buy at %f instead of %f, bid price at %f" % (self.gox.quote2float(next_buy), self.gox.quote2float(bad_next_buy), self.bid)) elif self.bid == 0: status_prefix = 'Waiting for price, skipping ' + self.simulate_or_live @@ -371,16 +374,26 @@ def get_next_buy_price(self, center, step_factor): then it will return that, otherwise return center - step""" price = self.get_forced_price(center, False) if not price: - price = mark_own(int(round(center / step_factor))) - return price + price = int(round(center / step_factor)) + + # Compensate the fees on buy price + if int(conf['balancer_compensate_fees']): + price = int(round(price * 2 - (price * (1 + self.gox.trade_fee / 100)))) + + return mark_own(price) def get_next_sell_price(self, center, step_factor): """get the next sell price. If there is a forced price level then it will return that, otherwise return center + step""" price = self.get_forced_price(center, True) if not price: - price = mark_own(int(round(center * step_factor))) - return price + price = int(round(center * step_factor)) + + # Compensate the fees on sell price + if int(conf['balancer_compensate_fees']): + price = int(round(price * (1 + self.gox.trade_fee / 100))) + + return mark_own(price) def get_forced_price(self, center, need_ask): """get externally forced price level for order""" From 1cc2f97cae2aa2aac3a5002381628b1468e18a98 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Thu, 12 Dec 2013 02:30:19 -0500 Subject: [PATCH 36/52] log to balancer.log, keep underscore for forced prices --- balancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer.py b/balancer.py index d9a5b5f..bd1370c 100644 --- a/balancer.py +++ b/balancer.py @@ -55,7 +55,7 @@ def is_own(price): def write_log(txt): """write line to a separate logfile""" - with open("_balancer.log", "a") as logfile: + with open("balancer.log", "a") as logfile: logfile.write(txt + "\n") From abbb6b6944b2801a1e7b3b0ca5df0858debff6f6 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 13 Dec 2013 17:56:41 -0500 Subject: [PATCH 37/52] improve fee compensation, output cleanup, use self.step_factor --- balancer.py | 87 ++++++++++++++++++++++++++++++++++++++++------------- buy.py | 2 +- sell.py | 2 +- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/balancer.py b/balancer.py index bd1370c..7a71e7b 100644 --- a/balancer.py +++ b/balancer.py @@ -67,10 +67,11 @@ def __init__(self, gox): self.ask = 0 self.simulate_or_live = SIMULATE_OR_LIVE self.distance = DISTANCE + self.step_factor = 1 + self.distance / 100.0 self.init_distance = float(DISTANCE) self.temp_halt = False self.name = "%s.%s" % (__name__, self.__class__.__name__) - self.debug("[s]%s loaded" % self.name) + self.debug("[s]%s%s loaded" % (self.simulate_or_live, self.name)) self.debug("[s]Press 'i' for information (how much currently out of balance)") self.debug("[s]Press 'o' to see order book") self.debug("[s]WARNING Rebalancing will buy or sell up to half your fiat or BTC balance") @@ -116,21 +117,29 @@ def slot_keypress(self, gox, (key)): vol_buy = self.get_buy_at_price(price) price_balanced = self.get_price_where_it_was_balanced() - step_factor = 1 + self.distance / 100.0 - price_sell = self.get_next_sell_price(price_balanced, step_factor) - price_buy = self.get_next_buy_price(price_balanced, step_factor) + self.debug("[s]center is %f" % self.gox.quote2float(price_balanced)) + price_sell = self.get_next_sell_price(price_balanced, self.step_factor) + price_buy = self.get_next_buy_price(price_balanced, self.step_factor) sell_amount = -self.get_buy_at_price(price_sell) buy_amount = self.get_buy_at_price(price_buy) self.debug("[s]BTC difference at current price:", gox.base2float(vol_buy)) - self.debug("[s]Price where it would be balanced:", - gox.quote2float(price_balanced)) - self.debug("[s]Next two orders would be at:", - gox.quote2float(price_sell), + self.debug("[s]Next two orders would be at:") + self.debug( + "[s] ask:", self.gox.base2float(sell_amount), + gox.curr_base, + "@", + gox.quote2float(price_sell), + gox.curr_quote) + self.debug( + "[s] bid:", + self.gox.base2float(buy_amount), + gox.curr_base, + "@", gox.quote2float(price_buy), - self.gox.base2float(buy_amount)) + gox.curr_quote) vol = gox.base2float(gox.monthly_volume) fee = gox.trade_fee @@ -186,6 +195,10 @@ def get_price_where_it_was_balanced(self): if (gox.wallet): fiat_have = gox.quote2float(gox.wallet[gox.curr_quote]) + FIAT_COLD btc_have = gox.base2float(gox.wallet[gox.curr_base]) + COIN_COLD + if fiat_have == 0 and btc_have and self.ask: + return gox.quote2int((gox.base2float(gox.wallet[gox.curr_base]) / 2 * self.ask) / 2) + elif btc_have == 0 and fiat_have and self.bid: + return gox.quote2int(((gox.quote2float(gox.wallet[gox.curr_quote]) / 2) / self.bid) / 2) else: self.debug('[s]Waiting for price...') return False @@ -223,10 +236,8 @@ def place_orders(self): else: return - step_factor = 1 + self.distance / 100.0 - - next_sell = self.get_next_sell_price(center, step_factor) - next_buy = self.get_next_buy_price(center, step_factor) + next_sell = self.get_next_sell_price(center, self.step_factor) + next_buy = self.get_next_buy_price(center, self.step_factor) status_prefix = self.simulate_or_live @@ -338,7 +349,6 @@ def check_trades(self): # wallet yet, we cannot trade anyways without that, # must wait until private/info is received. if self.gox.wallet == {}: - self.debug('[s]Waiting for info...') return # still waiting for submitted orders, @@ -369,6 +379,34 @@ def check_trades(self): self.cancel_orders() self.place_orders() + def price_factor_with_fees(self, price): + # Get our volume at price + bid_or_ask = 'bid' + volume_at_price = self.gox.base2float(self.get_buy_at_price(price)) + if volume_at_price < 0: + bid_or_ask = 'ask' + volume_at_price = -volume_at_price + + # Calculate volume with fees + volume_with_fees_at_price = float(volume_at_price * (1 + self.gox.trade_fee / 100)) + fees_at_price = volume_with_fees_at_price - volume_at_price + + # Return the price difference in percent + price = self.gox.quote2float(price) + price_factor_with_fees = ((price - (volume_with_fees_at_price * price)) / price) + + self.debug("[s]next %s: %f %s @ %f %s - fees: %f %s - diff: %f %%" % ( + bid_or_ask, + volume_at_price, + self.gox.curr_base, + price, + self.gox.curr_quote, + fees_at_price, + self.gox.curr_base, + price_factor_with_fees)) + + return price_factor_with_fees + def get_next_buy_price(self, center, step_factor): """get the next buy price. If there is a forced price level then it will return that, otherwise return center - step""" @@ -376,9 +414,13 @@ def get_next_buy_price(self, center, step_factor): if not price: price = int(round(center / step_factor)) - # Compensate the fees on buy price - if int(conf['balancer_compensate_fees']): - price = int(round(price * 2 - (price * (1 + self.gox.trade_fee / 100)))) + if not center: + self.debug("[s]Waiting for price...") + elif int(conf['balancer_compensate_fees']): + # Decrease our next buy price by the calculated percentage + price = price * 2 - (price * (1 + self.price_factor_with_fees(price) / 100)) + + self.debug("[s] adjusted price: %f %s" % (self.gox.quote2float(price), self.gox.curr_quote)) return mark_own(price) @@ -390,8 +432,11 @@ def get_next_sell_price(self, center, step_factor): price = int(round(center * step_factor)) # Compensate the fees on sell price - if int(conf['balancer_compensate_fees']): - price = int(round(price * (1 + self.gox.trade_fee / 100))) + if int(conf['balancer_compensate_fees']) and center: + # Increase our next sell price by the calculated percentage + price = price * (1 + self.price_factor_with_fees(price) / 100) + + self.debug("[s] adjusted price: %f %s" % (self.gox.quote2float(price), self.gox.curr_quote)) return mark_own(price) @@ -409,11 +454,11 @@ def get_forced_price(self, center, need_ask): prices.sort() if need_ask: for price in prices: - if price > center * 1.005: + if price > self.gox.quote2int(center) * self.step_factor: return mark_own(price) else: for price in reversed(prices): - if price < center * 0.995: + if price < (self.gox.quote2int(center) * 2 - (self.gox.quote2int(center) * self.step_factor)): return mark_own(price) return None diff --git a/buy.py b/buy.py index 8a0dbf4..eb6665e 100644 --- a/buy.py +++ b/buy.py @@ -67,7 +67,7 @@ def __init__(self, gox): gox.signal_wallet.connect(self.slot_wallet_changed) self.gox = gox self.name = "%s.%s" % (__name__, self.__class__.__name__) - self.debug("[s]%s loaded" % self.name) + self.debug("[s]%s%s loaded" % (simulate_or_live, self.name)) self.debug("[s]Press 'b' to see Buy objective") #get existing orders for later decision making self.existingorders = [] diff --git a/sell.py b/sell.py index 2c3b09a..ccc70c7 100644 --- a/sell.py +++ b/sell.py @@ -67,7 +67,7 @@ def __init__(self, gox): gox.signal_wallet.connect(self.slot_wallet_changed) self.gox = gox self.name = "%s.%s" % (__name__, self.__class__.__name__) - self.debug("[s]%s loaded" % self.name) + self.debug("[s]%s%s loaded" % (simulate_or_live, self.name)) self.debug("[s]Press 's' to see Sell objective") #get existing orders for later decision making self.existingorders = [] From 81d8e2b87e3832cb95dea8f72e36c5cd72dad894 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Fri, 13 Dec 2013 18:47:41 -0500 Subject: [PATCH 38/52] int(round()) and mention if live --- balancer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/balancer.py b/balancer.py index 7a71e7b..4ab3ff4 100644 --- a/balancer.py +++ b/balancer.py @@ -28,7 +28,7 @@ SIMULATE = int(conf['balancer_simulate']) # Live or simulation notice -SIMULATE_OR_LIVE = 'SIMULATION - ' if SIMULATE else '' +SIMULATE_OR_LIVE = 'SIMULATION - ' if SIMULATE else 'LIVE - ' DISTANCE = float(conf['balancer_distance']) # percent price distance of next rebalancing orders FIAT_COLD = float(conf['balancer_fiat_cold']) # Amount of Fiat stored at home but included in calculations @@ -418,7 +418,7 @@ def get_next_buy_price(self, center, step_factor): self.debug("[s]Waiting for price...") elif int(conf['balancer_compensate_fees']): # Decrease our next buy price by the calculated percentage - price = price * 2 - (price * (1 + self.price_factor_with_fees(price) / 100)) + price = int(round(price * 2 - (price * (1 + self.price_factor_with_fees(price) / 100)))) self.debug("[s] adjusted price: %f %s" % (self.gox.quote2float(price), self.gox.curr_quote)) @@ -434,7 +434,7 @@ def get_next_sell_price(self, center, step_factor): # Compensate the fees on sell price if int(conf['balancer_compensate_fees']) and center: # Increase our next sell price by the calculated percentage - price = price * (1 + self.price_factor_with_fees(price) / 100) + price = int(round(price * (1 + self.price_factor_with_fees(price) / 100))) self.debug("[s] adjusted price: %f %s" % (self.gox.quote2float(price), self.gox.curr_quote)) From eb64e9017077884d63d4af2c7e351399a44b6c2b Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 14 Dec 2013 02:48:54 -0500 Subject: [PATCH 39/52] fix forced price with self.step_factor --- balancer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/balancer.py b/balancer.py index 4ab3ff4..038cb18 100644 --- a/balancer.py +++ b/balancer.py @@ -454,11 +454,11 @@ def get_forced_price(self, center, need_ask): prices.sort() if need_ask: for price in prices: - if price > self.gox.quote2int(center) * self.step_factor: + if price > center * self.step_factor: return mark_own(price) else: for price in reversed(prices): - if price < (self.gox.quote2int(center) * 2 - (self.gox.quote2int(center) * self.step_factor)): + if price < center / self.step_factor: return mark_own(price) return None From 4bf9a8f9d7986d5bfe595d5f882fa80ad609f5ef Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 14 Dec 2013 14:29:57 -0500 Subject: [PATCH 40/52] press h for help --- balancer.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/balancer.py b/balancer.py index 038cb18..24739de 100644 --- a/balancer.py +++ b/balancer.py @@ -72,13 +72,7 @@ def __init__(self, gox): self.temp_halt = False self.name = "%s.%s" % (__name__, self.__class__.__name__) self.debug("[s]%s%s loaded" % (self.simulate_or_live, self.name)) - self.debug("[s]Press 'i' for information (how much currently out of balance)") - self.debug("[s]Press 'o' to see order book") - self.debug("[s]WARNING Rebalancing will buy or sell up to half your fiat or BTC balance") - self.debug("[s]Press 'r' to rebalance with market order at current price (recommended before rebalancing)") - self.debug("[s]Press 'p' to add initial rebalancing orders and start trading") - self.debug("[s]Press 'c' to cancel all rebalancing orders and suspend trading") - self.debug("[s]Press 'u' to update account information, order list and wallet") + self.help() def __del__(self): try: @@ -89,6 +83,9 @@ def __del__(self): def slot_keypress(self, gox, (key)): """a key has been pressed""" + if key == ord("h"): + self.help() + if key == ord("c"): # cancel existing rebalancing orders and suspend trading self.debug("[s]%scanceling all rebalancing orders" % self.simulate_or_live) @@ -172,6 +169,16 @@ def slot_keypress(self, gox, (key)): if SIMULATE == False: gox.sell(0, -vol_buy) + def help(self): + self.debug("[s]Press 'h' to see this help") + self.debug("[s]Press 'i' for information") + self.debug("[s]Press 'o' to see order book") + self.debug("[s]WARNING Rebalancing will buy or sell up to half your fiat or BTC balance") + self.debug("[s]Press 'r' to rebalance with market order at current price (recommended before rebalancing)") + self.debug("[s]Press 'p' to add initial rebalancing orders and start trading") + self.debug("[s]Press 'c' to cancel all rebalancing orders and suspend trading") + self.debug("[s]Press 'u' to update account information, order list and wallet") + def cancel_orders(self): """cancel all rebalancing orders, we identify them through the marker in the price value""" @@ -388,12 +395,12 @@ def price_factor_with_fees(self, price): volume_at_price = -volume_at_price # Calculate volume with fees - volume_with_fees_at_price = float(volume_at_price * (1 + self.gox.trade_fee / 100)) + volume_with_fees_at_price = volume_at_price * (1 + self.gox.trade_fee / 100) fees_at_price = volume_with_fees_at_price - volume_at_price # Return the price difference in percent price = self.gox.quote2float(price) - price_factor_with_fees = ((price - (volume_with_fees_at_price * price)) / price) + price_factor_with_fees = (price - volume_with_fees_at_price * price) / price self.debug("[s]next %s: %f %s @ %f %s - fees: %f %s - diff: %f %%" % ( bid_or_ask, From 6a75fa2eb82cf233c37fc42020c15a8e12ca78a4 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 16 Dec 2013 04:08:03 -0500 Subject: [PATCH 41/52] show transactions total --- goxapi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/goxapi.py b/goxapi.py index 9a7255b..5e61c54 100644 --- a/goxapi.py +++ b/goxapi.py @@ -1829,10 +1829,11 @@ def _on_op_private_depth(self, msg): delay = time.time() * 1e6 - timestamp self.socket_lag = (self.socket_lag * 2 + delay) / 3 - self.debug("depth: %s: %s @ %s total vol: %s (age: %0.2f s)" % ( + self.debug("depth: %s: %s @ %s total: %s vol: %s (age: %0.2f s)" % ( typ, self.base2str(volume), self.quote2str(price), + self.quote2str(self.quote2int(self.quote2float(price) * self.base2float(volume))), self.base2str(total_volume), delay / 1e6 )) From 11f241e261c7293298976ddde51c33d39fb2a3d2 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 16 Dec 2013 04:35:27 -0500 Subject: [PATCH 42/52] show totals in balancer also --- balancer.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/balancer.py b/balancer.py index 24739de..085e2de 100644 --- a/balancer.py +++ b/balancer.py @@ -129,6 +129,7 @@ def slot_keypress(self, gox, (key)): gox.curr_base, "@", gox.quote2float(price_sell), + self.gox.quote2str(self.gox.quote2int(self.gox.quote2float(price_sell) * self.gox.base2float(sell_amount))), gox.curr_quote) self.debug( "[s] bid:", @@ -136,6 +137,7 @@ def slot_keypress(self, gox, (key)): gox.curr_base, "@", gox.quote2float(price_buy), + self.gox.quote2str(self.gox.quote2int(self.gox.quote2float(price_buy) * self.gox.base2float(buy_amount))), gox.curr_quote) vol = gox.base2float(gox.monthly_volume) @@ -145,7 +147,13 @@ def slot_keypress(self, gox, (key)): if key == ord('o'): self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) for order in self.gox.orderbook.owns: - self.debug("[s] %s: %s: %s @ %s order id: %s" % (str(order.status), str(order.typ), gox.base2str(order.volume), gox.quote2str(order.price), str(order.oid))) + self.debug("[s] %s: %s: %s @ %s %s order id: %s" % ( + str(order.status), + str(order.typ), + gox.base2str(order.volume), + gox.quote2str(order.price), + gox.quote2str(gox.quote2int(gox.quote2float(order.price) * gox.base2float(order.volume))), + str(order.oid))) if key == ord("r"): # manually rebalance with market order at current price @@ -293,18 +301,20 @@ def place_orders(self): buy_amount = int(0.011 * COIN) self.debug("[s]WARNING! minimal buy amount adjusted to 0.011") - self.debug("[s]%snew buy order %f at %f" % ( + self.debug("[s]%snew buy order %f at %f for %f" % ( status_prefix, self.gox.base2float(buy_amount), - self.gox.quote2float(next_buy) + self.gox.quote2float(next_buy), + self.gox.quote2str(self.gox.quote2int(self.gox.quote2float(next_buy) * self.gox.base2float(buy_amount))), )) if SIMULATE == False and self.ask != 0: self.gox.buy(next_buy, buy_amount) - self.debug("[s]%snew sell order %f at %f" % ( + self.debug("[s]%snew sell order %f at %f for %f" % ( status_prefix, self.gox.base2float(sell_amount), - self.gox.quote2float(next_sell) + self.gox.quote2float(next_sell), + self.gox.quote2str(self.gox.quote2int(self.gox.quote2float(next_sell) * self.gox.base2float(sell_amount))), )) if SIMULATE == False and self.ask != 0: self.gox.sell(next_sell, sell_amount) @@ -334,11 +344,13 @@ def slot_trade(self, gox, (date, price, volume, typ, own)): return text = {"bid": "sold", "ask": "bought"}[typ] + self.debug("[s]*** %s %f at %f" % ( text, gox.base2float(volume), gox.quote2float(price) )) + self.check_trades() def slot_owns_changed(self, orderbook, _dummy): From c3cdbee99dcf9a9e0f3a23a4fa49e1b8c02c8c56 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 16 Dec 2013 04:50:36 -0500 Subject: [PATCH 43/52] quote2float in new totals and first take at fixing mtgox satoshi bug --- balancer.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/balancer.py b/balancer.py index 085e2de..f2b98f5 100644 --- a/balancer.py +++ b/balancer.py @@ -305,7 +305,7 @@ def place_orders(self): status_prefix, self.gox.base2float(buy_amount), self.gox.quote2float(next_buy), - self.gox.quote2str(self.gox.quote2int(self.gox.quote2float(next_buy) * self.gox.base2float(buy_amount))), + self.gox.quote2float(self.gox.quote2int(self.gox.quote2float(next_buy) * self.gox.base2float(buy_amount))), )) if SIMULATE == False and self.ask != 0: self.gox.buy(next_buy, buy_amount) @@ -314,7 +314,7 @@ def place_orders(self): status_prefix, self.gox.base2float(sell_amount), self.gox.quote2float(next_sell), - self.gox.quote2str(self.gox.quote2int(self.gox.quote2float(next_sell) * self.gox.base2float(sell_amount))), + self.gox.quote2float(self.gox.quote2int(self.gox.quote2float(next_sell) * self.gox.base2float(sell_amount))), )) if SIMULATE == False and self.ask != 0: self.gox.sell(next_sell, sell_amount) @@ -351,6 +351,13 @@ def slot_trade(self, gox, (date, price, volume, typ, own)): gox.quote2float(price) )) + # Fix MtGox satoshi bug + for order in self.gox.orderbook.owns: + if gox.base2float(volume) == order.volume: + self.cancel_orders() + self.place_orders() + self.debug("[s]Satoshi! %s: %s: %s @ %s order id: %s" % (str(order.status), str(order.typ), gox.base2str(order.volume), gox.quote2str(order.price), str(order.oid))) + self.check_trades() def slot_owns_changed(self, orderbook, _dummy): From 5c076e1c5a5a895e371b2e830fe35191feb10349 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 16 Dec 2013 08:45:34 -0500 Subject: [PATCH 44/52] compare directly --- balancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer.py b/balancer.py index f2b98f5..0b3b98c 100644 --- a/balancer.py +++ b/balancer.py @@ -353,7 +353,7 @@ def slot_trade(self, gox, (date, price, volume, typ, own)): # Fix MtGox satoshi bug for order in self.gox.orderbook.owns: - if gox.base2float(volume) == order.volume: + if volume == order.volume: self.cancel_orders() self.place_orders() self.debug("[s]Satoshi! %s: %s: %s @ %s order id: %s" % (str(order.status), str(order.typ), gox.base2str(order.volume), gox.quote2str(order.price), str(order.oid))) From fd976a9405634823e22fea8c123c09de82c642e3 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Thu, 19 Dec 2013 15:41:47 -0500 Subject: [PATCH 45/52] balancer: write actual trades to log instead of placed orders and move mtgox satoshi bug handling to slot_owns_changed for now --- balancer.py | 61 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/balancer.py b/balancer.py index 0b3b98c..49810b5 100644 --- a/balancer.py +++ b/balancer.py @@ -301,33 +301,26 @@ def place_orders(self): buy_amount = int(0.011 * COIN) self.debug("[s]WARNING! minimal buy amount adjusted to 0.011") - self.debug("[s]%snew buy order %f at %f for %f" % ( + self.debug("[s]%snew buy order %f at %f for %f %s" % ( status_prefix, self.gox.base2float(buy_amount), self.gox.quote2float(next_buy), self.gox.quote2float(self.gox.quote2int(self.gox.quote2float(next_buy) * self.gox.base2float(buy_amount))), + self.gox.curr_quote )) if SIMULATE == False and self.ask != 0: self.gox.buy(next_buy, buy_amount) - self.debug("[s]%snew sell order %f at %f for %f" % ( + self.debug("[s]%snew sell order %f at %f for %f %s" % ( status_prefix, self.gox.base2float(sell_amount), self.gox.quote2float(next_sell), self.gox.quote2float(self.gox.quote2int(self.gox.quote2float(next_sell) * self.gox.base2float(sell_amount))), + self.gox.curr_quote )) if SIMULATE == False and self.ask != 0: self.gox.sell(next_sell, sell_amount) - # write some account information to a separate log file - datetime = time.strftime("%Y-%m-%d %H:%M", time.localtime()) - write_log('"%s", %f, %f, %s' % ( - datetime, - self.gox.quote2float(center), - self.gox.quote2float(self.gox.wallet[self.gox.curr_quote]) + FIAT_COLD, - self.gox.base2float(self.gox.wallet[self.gox.curr_base]) + COIN_COLD - )) - def slot_tick(self, gox, (bid, ask)): # Set last bid/ask price self.bid = goxapi.int2float(bid, self.gox.orderbook.gox.currency) @@ -351,17 +344,51 @@ def slot_trade(self, gox, (date, price, volume, typ, own)): gox.quote2float(price) )) - # Fix MtGox satoshi bug - for order in self.gox.orderbook.owns: - if volume == order.volume: - self.cancel_orders() - self.place_orders() - self.debug("[s]Satoshi! %s: %s: %s @ %s order id: %s" % (str(order.status), str(order.typ), gox.base2str(order.volume), gox.quote2str(order.price), str(order.oid))) + # write some account information to a separate log file + if len(gox.wallet): + total_btc = 0 + total_fiat = 0 + for c, own_currency in enumerate(gox.wallet): + if own_currency == 'BTC' and gox.orderbook.ask: + total_btc += gox.base2float(gox.wallet['BTC']) + total_fiat += gox.base2float(gox.wallet['BTC']) * gox.orderbook.bid + elif own_currency == gox.curr_quote and gox.orderbook.bid: + total_fiat += gox.wallet[own_currency] + total_btc += gox.quote2float(gox.wallet[own_currency]) / gox.quote2float(gox.orderbook.ask) + + total_fiat = gox.quote2float(total_fiat) + fiat_ratio = (total_fiat / gox.quote2float(gox.orderbook.bid)) / total_btc + btc_ratio = (total_btc / gox.quote2float(gox.orderbook.ask)) * 100 + + datetime = time.strftime("%Y-%m-%d %H:%M", time.localtime()) + write_log('"%s", "%s", %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f' % ( + datetime, + text, + gox.base2float(volume), + gox.quote2float(price), + gox.trade_fee, + gox.quote2float(self.get_price_where_it_was_balanced()), + gox.quote2float(gox.wallet[gox.curr_quote]), + total_fiat, + FIAT_COLD, + fiat_ratio, + gox.base2float(gox.wallet[gox.curr_base]), + total_btc, + COIN_COLD, + btc_ratio + )) self.check_trades() def slot_owns_changed(self, orderbook, _dummy): """status or amount of own open orders has changed""" + + # Fix MtGox satoshi bug + for order in orderbook.owns: + if order.volume == 0.00000001 * COIN: + self.debug("[s]Satoshi! %s: %s: %s @ %s order id: %s" % (str(order.status), str(order.typ), self.gox.base2str(order.volume), self.gox.quote2str(order.price), str(order.oid))) + gox.cancel(order.oid) + self.check_trades() def check_trades(self): From 39634b312d83b73d77ff1a55e87a7ffbc36ba385 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Thu, 19 Dec 2013 16:10:35 -0500 Subject: [PATCH 46/52] missing self... --- balancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer.py b/balancer.py index 49810b5..fba123a 100644 --- a/balancer.py +++ b/balancer.py @@ -387,7 +387,7 @@ def slot_owns_changed(self, orderbook, _dummy): for order in orderbook.owns: if order.volume == 0.00000001 * COIN: self.debug("[s]Satoshi! %s: %s: %s @ %s order id: %s" % (str(order.status), str(order.typ), self.gox.base2str(order.volume), self.gox.quote2str(order.price), str(order.oid))) - gox.cancel(order.oid) + self.gox.cancel(order.oid) self.check_trades() From b82a213e76718beb92153b88812e57bc7117f5c7 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 23 Dec 2013 15:00:10 -0500 Subject: [PATCH 47/52] balancer: implement simulation wallet with logging --- balancer.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/balancer.py b/balancer.py index fba123a..e186501 100644 --- a/balancer.py +++ b/balancer.py @@ -55,7 +55,7 @@ def is_own(price): def write_log(txt): """write line to a separate logfile""" - with open("balancer.log", "a") as logfile: + with open("balancer.log" if not SIMULATE else "simulation.log", "a") as logfile: logfile.write(txt + "\n") @@ -66,6 +66,8 @@ def __init__(self, gox): self.bid = 0 self.ask = 0 self.simulate_or_live = SIMULATE_OR_LIVE + self.wallet = False + self.simulate = { 'next_sell': False, 'sell_amount': 0, 'next_buy': False, 'buy_amount': 0 } self.distance = DISTANCE self.step_factor = 1 + self.distance / 100.0 self.init_distance = float(DISTANCE) @@ -74,6 +76,16 @@ def __init__(self, gox): self.debug("[s]%s%s loaded" % (self.simulate_or_live, self.name)) self.help() + # Simulation wallet + if SIMULATE and not self.gox.wallet: + self.wallet = True + self.gox.wallet = {} + self.gox.wallet[self.gox.curr_quote] = 1000000000 + self.gox.wallet[self.gox.curr_base] = 10 * COIN + # self.gox.trade_fee = 0 + else: + self.wallet = False + def __del__(self): try: self.debug("[s]%s unloaded" % self.name) @@ -146,7 +158,14 @@ def slot_keypress(self, gox, (key)): if key == ord('o'): self.debug("[s] %i own orders in orderbook" % len(self.gox.orderbook.owns)) + profit_btc = 0 + profit_fiat = 0 for order in self.gox.orderbook.owns: + btc_diff = gox.base2float(order.volume - order.volume * gox.trade_fee / 100) if str(order.typ) == 'bid' else gox.base2float(order.volume) + profit_btc = btc_diff if not profit_btc else profit_btc - btc_diff + fiat_diff = gox.quote2float(order.price) * gox.base2float(order.volume) + profit_fiat = -fiat_diff if not profit_fiat else profit_fiat + (fiat_diff if str(order.typ) == 'ask' else -fiat_diff) + self.debug("[s] %s: %s: %s @ %s %s order id: %s" % ( str(order.status), str(order.typ), @@ -154,6 +173,23 @@ def slot_keypress(self, gox, (key)): gox.quote2str(order.price), gox.quote2str(gox.quote2int(gox.quote2float(order.price) * gox.base2float(order.volume))), str(order.oid))) + self.debug("[s] Profit would be: %f BTC / %f %s" % ( + profit_btc, + profit_fiat, + gox.curr_quote)) + + if SIMULATE and self.wallet and self.simulate['next_sell'] and self.simulate['next_buy']: + self.debug("[s]SIMULATION orders:") + self.debug("[s] %s: %s @ %s %s" % ( + 'ask', + gox.base2str(self.simulate['sell_amount']), + gox.quote2str(self.simulate['next_sell']), + gox.quote2str(gox.quote2int(gox.quote2float(self.simulate['next_sell']) * gox.base2float(self.simulate['sell_amount']))))) + self.debug("[s] %s: %s @ %s %s" % ( + 'bid', + gox.base2str(self.simulate['buy_amount']), + gox.quote2str(self.simulate['next_buy']), + gox.quote2str(gox.quote2int(gox.quote2float(self.simulate['next_buy']) * gox.base2float(self.simulate['buy_amount']))))) if key == ord("r"): # manually rebalance with market order at current price @@ -310,6 +346,8 @@ def place_orders(self): )) if SIMULATE == False and self.ask != 0: self.gox.buy(next_buy, buy_amount) + elif SIMULATE and self.wallet and self.ask != 0: + self.simulate.update({ "next_buy": next_buy, "buy_amount": buy_amount }) self.debug("[s]%snew sell order %f at %f for %f %s" % ( status_prefix, @@ -318,14 +356,32 @@ def place_orders(self): self.gox.quote2float(self.gox.quote2int(self.gox.quote2float(next_sell) * self.gox.base2float(sell_amount))), self.gox.curr_quote )) - if SIMULATE == False and self.ask != 0: + if SIMULATE == False and self.bid != 0: self.gox.sell(next_sell, sell_amount) + elif SIMULATE and self.wallet and self.bid != 0: + self.simulate.update({ "next_sell": next_sell, "sell_amount": sell_amount }) def slot_tick(self, gox, (bid, ask)): # Set last bid/ask price self.bid = goxapi.int2float(bid, self.gox.orderbook.gox.currency) self.ask = goxapi.int2float(ask, self.gox.orderbook.gox.currency) + # Simulation wallet + if SIMULATE and self.wallet: + if ask >= self.simulate['next_sell']: + gox.wallet[gox.curr_quote] += gox.base2float(self.simulate['sell_amount']) * self.simulate['next_sell'] + gox.wallet[gox.curr_base] -= gox.base2float(self.simulate['sell_amount']) + # Trigger slot_trade for simulation.log + self.slot_trade(gox, (time, self.simulate['next_sell'], self.simulate['sell_amount'], 'bid', True)) + self.place_orders() + + if bid <= self.simulate['next_buy']: + gox.wallet[gox.curr_base] += self.simulate['buy_amount'] - (self.simulate['buy_amount'] * self.gox.trade_fee / 100) + gox.wallet[gox.curr_quote] -= gox.base2float(self.simulate['buy_amount'] * self.simulate['next_buy']) + # Trigger slot_trade for simulation.log + self.slot_trade(gox, (time, self.simulate['next_buy'], self.simulate['buy_amount'], 'ask', True)) + self.place_orders() + def slot_trade(self, gox, (date, price, volume, typ, own)): """a trade message has been receivd""" # not interested in other people's trades @@ -338,7 +394,8 @@ def slot_trade(self, gox, (date, price, volume, typ, own)): text = {"bid": "sold", "ask": "bought"}[typ] - self.debug("[s]*** %s %f at %f" % ( + self.debug("[s]*** %s%s %f at %f" % ( + 'SIMULATION - ' if SIMULATE else '', text, gox.base2float(volume), gox.quote2float(price) @@ -360,7 +417,7 @@ def slot_trade(self, gox, (date, price, volume, typ, own)): fiat_ratio = (total_fiat / gox.quote2float(gox.orderbook.bid)) / total_btc btc_ratio = (total_btc / gox.quote2float(gox.orderbook.ask)) * 100 - datetime = time.strftime("%Y-%m-%d %H:%M", time.localtime()) + datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) write_log('"%s", "%s", %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f' % ( datetime, text, From 6aeb686004165946a9f7dbf7c413866a306dd5c0 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Mon, 23 Dec 2013 17:46:30 -0500 Subject: [PATCH 48/52] balancer: fix reloading in simulation --- balancer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/balancer.py b/balancer.py index e186501..2b57f24 100644 --- a/balancer.py +++ b/balancer.py @@ -67,7 +67,6 @@ def __init__(self, gox): self.ask = 0 self.simulate_or_live = SIMULATE_OR_LIVE self.wallet = False - self.simulate = { 'next_sell': False, 'sell_amount': 0, 'next_buy': False, 'buy_amount': 0 } self.distance = DISTANCE self.step_factor = 1 + self.distance / 100.0 self.init_distance = float(DISTANCE) @@ -77,17 +76,18 @@ def __init__(self, gox): self.help() # Simulation wallet - if SIMULATE and not self.gox.wallet: + if (SIMULATE and not self.gox.wallet) or (SIMULATE and self.wallet): self.wallet = True + self.simulate = { 'next_sell': 0, 'sell_amount': 0, 'next_buy': 0, 'buy_amount': 0 } self.gox.wallet = {} self.gox.wallet[self.gox.curr_quote] = 1000000000 self.gox.wallet[self.gox.curr_base] = 10 * COIN - # self.gox.trade_fee = 0 - else: - self.wallet = False + self.gox.trade_fee = 0.5 def __del__(self): try: + if SIMULATE and self.wallet: + self.gox.wallet = {} self.debug("[s]%s unloaded" % self.name) except Exception, e: self.debug("[s]%s exception: %s" % (self.name, e)) From 86f93c96e0da331be8a8e9e244ba08f60ed04f5d Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sat, 28 Dec 2013 02:39:11 -0500 Subject: [PATCH 49/52] balancer: fix simulation sell trades and fake wallet btc balance not moving --- balancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer.py b/balancer.py index 2b57f24..65d0f27 100644 --- a/balancer.py +++ b/balancer.py @@ -370,7 +370,7 @@ def slot_tick(self, gox, (bid, ask)): if SIMULATE and self.wallet: if ask >= self.simulate['next_sell']: gox.wallet[gox.curr_quote] += gox.base2float(self.simulate['sell_amount']) * self.simulate['next_sell'] - gox.wallet[gox.curr_base] -= gox.base2float(self.simulate['sell_amount']) + gox.wallet[gox.curr_base] -= self.simulate['sell_amount'] # Trigger slot_trade for simulation.log self.slot_trade(gox, (time, self.simulate['next_sell'], self.simulate['sell_amount'], 'bid', True)) self.place_orders() From 0f92d9a0087e3c8f77d68dc3c05f23d9142735a2 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Thu, 2 Jan 2014 16:14:12 -0500 Subject: [PATCH 50/52] use self.gox.curr_quote in buy.py --- buy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/buy.py b/buy.py index eb6665e..51fdf8e 100644 --- a/buy.py +++ b/buy.py @@ -90,12 +90,12 @@ def slot_keypress(self, gox, (key)): # self.debug("someone pressed the %s key" % chr(key)) global buy_amount if key == ord('b'): - self.debug("[s]%sObjective: BUY Bitcoins for %f %s when price reaches %f" % (simulate_or_live, buy_amount, str(self.gox.orderbook.gox.currency), buy_level)) + self.debug("[s]%sObjective: BUY Bitcoins for %f %s when price reaches %f" % (simulate_or_live, buy_amount, str(self.gox.curr_quote), buy_level)) # self.debug("[s]Python wallet object: %s" % str(self.gox.wallet)) # check if the user changed volume # also ensure the buy_amount does not exceed wallet balance # if it does, set buy_amount to wallet full fiat balance - walletbalance = gox.quote2float(self.gox.wallet[self.gox.orderbook.gox.currency]) + walletbalance = gox.quote2float(self.gox.wallet[self.gox.curr_quote]) if volume == 0: buy_amount = walletbalance else: @@ -107,7 +107,7 @@ def slot_keypress(self, gox, (key)): # buy_amount = walletbalance # else: # buy_amount = walletbalance - self.debug("[s] %sstrategy will spend %f of %f %s on next BUY" % (simulate_or_live, buy_amount, walletbalance, str(self.gox.orderbook.gox.currency))) + self.debug("[s] %sstrategy will spend %f of %f %s on next BUY" % (simulate_or_live, buy_amount, walletbalance, str(self.gox.curr_quote))) def slot_tick(self, gox, (bid, ask)): global bidbuf, askbuf, buy_amount @@ -117,7 +117,7 @@ def slot_tick(self, gox, (bid, ask)): self.ask = gox.quote2float(ask) if self.ask > buy_level and self.ask < buy_alert: self.debug("[s] !!! buy ALERT @ %s; ask currently at %s" % (str(buy_alert), str(self.ask))) - self.debug("[s] !!! BUY for %f %s will trigger @ %f" % (buy_amount, str(self.gox.orderbook.gox.currency), buy_level)) + self.debug("[s] !!! BUY for %f %s will trigger @ %f" % (buy_amount, str(self.gox.curr_quote), buy_level)) seen = 1 elif self.ask <= buy_level: # this is the condition to action gox.buy() @@ -172,7 +172,7 @@ def slot_wallet_changed(self, gox, _dummy): # also ensure the buy_amount does not exceed wallet balance # if it does, set buy_amount to wallet full fiat balance global buy_amount - walletbalance = gox.quote2float(self.gox.wallet[self.gox.orderbook.gox.currency]) + walletbalance = gox.quote2float(self.gox.wallet[self.gox.curr_quote]) if volume != 0 and volume <= walletbalance: buy_amount = volume else: From 797924f78878c31bcf8d57d8dee5703f2a993476 Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Sun, 5 Jan 2014 04:19:02 -0500 Subject: [PATCH 51/52] balancer: improve fee compensation once more --- balancer.py | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/balancer.py b/balancer.py index 65d0f27..590a9f2 100644 --- a/balancer.py +++ b/balancer.py @@ -489,33 +489,35 @@ def check_trades(self): self.cancel_orders() self.place_orders() - def price_factor_with_fees(self, price): + def price_with_fees(self, price): # Get our volume at price - bid_or_ask = 'bid' volume_at_price = self.gox.base2float(self.get_buy_at_price(price)) - if volume_at_price < 0: + + if volume_at_price > 0: + bid_or_ask = 'bid' + price_with_fees = price / ((1 - self.gox.trade_fee / 100) * (1 - self.gox.trade_fee / 100)) + price_with_fees = price - (price_with_fees - price) + else: bid_or_ask = 'ask' volume_at_price = -volume_at_price + price_with_fees = price / ((1 - self.gox.trade_fee / 100) * (1 - self.gox.trade_fee / 100)) - # Calculate volume with fees - volume_with_fees_at_price = volume_at_price * (1 + self.gox.trade_fee / 100) - fees_at_price = volume_with_fees_at_price - volume_at_price - - # Return the price difference in percent - price = self.gox.quote2float(price) - price_factor_with_fees = (price - volume_with_fees_at_price * price) / price + # Calculate fees + fees_at_price = volume_at_price * self.gox.trade_fee / 100 - self.debug("[s]next %s: %f %s @ %f %s - fees: %f %s - diff: %f %%" % ( + self.debug("[s]next %s: %f %s @ %f %s - fees: %f %s - new: %f %s" % ( bid_or_ask, volume_at_price, self.gox.curr_base, - price, + self.gox.quote2float(price), self.gox.curr_quote, fees_at_price, self.gox.curr_base, - price_factor_with_fees)) + self.gox.quote2float(price_with_fees), + self.gox.curr_quote)) - return price_factor_with_fees + # Return the price with fees + return price_with_fees def get_next_buy_price(self, center, step_factor): """get the next buy price. If there is a forced price level @@ -527,10 +529,8 @@ def get_next_buy_price(self, center, step_factor): if not center: self.debug("[s]Waiting for price...") elif int(conf['balancer_compensate_fees']): - # Decrease our next buy price by the calculated percentage - price = int(round(price * 2 - (price * (1 + self.price_factor_with_fees(price) / 100)))) - - self.debug("[s] adjusted price: %f %s" % (self.gox.quote2float(price), self.gox.curr_quote)) + # Decrease our next buy price + price = int(round(self.price_with_fees(price))) return mark_own(price) @@ -543,10 +543,8 @@ def get_next_sell_price(self, center, step_factor): # Compensate the fees on sell price if int(conf['balancer_compensate_fees']) and center: - # Increase our next sell price by the calculated percentage - price = int(round(price * (1 + self.price_factor_with_fees(price) / 100))) - - self.debug("[s] adjusted price: %f %s" % (self.gox.quote2float(price), self.gox.curr_quote)) + # Increase our next sell price + price = int(round(self.price_with_fees(price))) return mark_own(price) From 7bcf4b5e9b5c20451283cc206fb3e8813fbf4c9f Mon Sep 17 00:00:00 2001 From: Vincent Gariepy Date: Thu, 9 Jan 2014 04:52:52 -0500 Subject: [PATCH 52/52] Support separate buy and sell price distance, default to 5% and with fee compensation enabled --- balancer.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/balancer.py b/balancer.py index 590a9f2..b849104 100644 --- a/balancer.py +++ b/balancer.py @@ -17,11 +17,12 @@ # Set defaults conf.setdefault('balancer_simulate', True) -conf.setdefault('balancer_distance', 7) +conf.setdefault('balancer_distance', 5) +conf.setdefault('balancer_distance_sell', 5) conf.setdefault('balancer_fiat_cold', 0) conf.setdefault('balancer_coin_cold', 0) conf.setdefault('balancer_marker', 7) -conf.setdefault('balancer_compensate_fees', False) +conf.setdefault('balancer_compensate_fees', True) conf.setdefault('balancer_target_margin', 1) # Simulate @@ -30,12 +31,13 @@ # Live or simulation notice SIMULATE_OR_LIVE = 'SIMULATION - ' if SIMULATE else 'LIVE - ' -DISTANCE = float(conf['balancer_distance']) # percent price distance of next rebalancing orders -FIAT_COLD = float(conf['balancer_fiat_cold']) # Amount of Fiat stored at home but included in calculations -COIN_COLD = float(conf['balancer_coin_cold']) # Amount of Coin stored at home but included in calculations +DISTANCE = float(conf['balancer_distance']) # percent price distance of next rebalancing orders +DISTANCE_SELL = float(conf['balancer_distance_sell']) # percent price distance of next rebalancing orders +FIAT_COLD = float(conf['balancer_fiat_cold']) # Amount of Fiat stored at home but included in calculations +COIN_COLD = float(conf['balancer_coin_cold']) # Amount of Coin stored at home but included in calculations -MARKER = int(conf['balancer_marker']) # lowest digit of price to identify bot's own orders -COIN = 1E8 # number of satoshi per coin, this is a constant. +MARKER = int(conf['balancer_marker']) # lowest digit of price to identify bot's own orders +COIN = 1E8 # number of satoshi per coin, this is a constant. def add_marker(price, marker): """encode a marker in the price value to find bot's own orders""" @@ -67,9 +69,8 @@ def __init__(self, gox): self.ask = 0 self.simulate_or_live = SIMULATE_OR_LIVE self.wallet = False - self.distance = DISTANCE - self.step_factor = 1 + self.distance / 100.0 - self.init_distance = float(DISTANCE) + self.step_factor = 1 + DISTANCE / 100.0 + self.step_factor_sell = 1 + DISTANCE_SELL / 100.0 self.temp_halt = False self.name = "%s.%s" % (__name__, self.__class__.__name__) self.debug("[s]%s%s loaded" % (self.simulate_or_live, self.name)) @@ -127,7 +128,7 @@ def slot_keypress(self, gox, (key)): price_balanced = self.get_price_where_it_was_balanced() self.debug("[s]center is %f" % self.gox.quote2float(price_balanced)) - price_sell = self.get_next_sell_price(price_balanced, self.step_factor) + price_sell = self.get_next_sell_price(price_balanced, self.step_factor_sell) price_buy = self.get_next_buy_price(price_balanced, self.step_factor) sell_amount = -self.get_buy_at_price(price_sell) buy_amount = self.get_buy_at_price(price_buy) @@ -287,7 +288,7 @@ def place_orders(self): else: return - next_sell = self.get_next_sell_price(center, self.step_factor) + next_sell = self.get_next_sell_price(center, self.step_factor_sell) next_buy = self.get_next_buy_price(center, self.step_factor) status_prefix = self.simulate_or_live @@ -562,7 +563,7 @@ def get_forced_price(self, center, need_ask): prices.sort() if need_ask: for price in prices: - if price > center * self.step_factor: + if price > center * self.step_factor_sell: return mark_own(price) else: for price in reversed(prices):