diff --git a/README.md b/README.md
index eff2cae..b39ce7d 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,205 @@
-#goxtool.py
+#### Hosted version available at [https://zerogox.com](https://zerogox.com)
-Goxtool is a trading client for the MtGox Bitcon currency exchange. It is
-designed to work in the Linux console (it has a curses user interface). It
-can display live streaming market data and you can buy and sell with
-keyboard commands.
+# Goxtool
-Goxtool also has a simple interface to plug in your own automated trading
-strategies, your own code can be (re)loded at runtime, it will receive
-events from the API and can act upon them.
+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.
-The user manual is here:
+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
+
+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
+```
+
+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 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:
+
+```
+./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 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 and is forced for PubNub.
+
+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 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.
+
+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)
+- 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
+- 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
+
+```
+./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
+
+```
+./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/)
+##### Donations appreciated
+
+prof7bit (goxtool): 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW
+
+caktux (mods): 18zX3wb318o2Pw9ZUHgG3mmQME536Qg2Ha
diff --git a/balancer.py b/balancer.py
new file mode 100644
index 0000000..b849104
--- /dev/null
+++ b/balancer.py
@@ -0,0 +1,573 @@
+"""
+The portfolio rebalancing bot will buy and sell to maintain a
+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
+
+# Load user.conf
+conf = json.load(open("user.conf"))
+
+# Set defaults
+conf.setdefault('balancer_simulate', True)
+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', True)
+conf.setdefault('balancer_target_margin', 1)
+
+# Simulate
+SIMULATE = int(conf['balancer_simulate'])
+
+# Live or simulation notice
+SIMULATE_OR_LIVE = 'SIMULATION - ' if SIMULATE else 'LIVE - '
+
+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.
+
+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)
+
+def write_log(txt):
+ """write line to a separate logfile"""
+ with open("balancer.log" if not SIMULATE else "simulation.log", "a") as logfile:
+ logfile.write(txt + "\n")
+
+
+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.wallet = False
+ 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))
+ self.help()
+
+ # Simulation 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.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))
+
+ 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)
+ 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]%sadding new initial rebalancing orders" % self.simulate_or_live)
+ self.temp_halt = False
+ self.cancel_orders()
+ 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]center is %f" % self.gox.quote2float(price_balanced))
+ 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)
+
+ self.debug("[s]BTC difference at current price:",
+ gox.base2float(vol_buy))
+ 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),
+ self.gox.quote2str(self.gox.quote2int(self.gox.quote2float(price_sell) * self.gox.base2float(sell_amount))),
+ gox.curr_quote)
+ self.debug(
+ "[s] bid:",
+ self.gox.base2float(buy_amount),
+ 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)
+ fee = gox.trade_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))
+ 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),
+ gox.base2str(order.volume),
+ 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
+ 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]%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]%sselling %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 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"""
+ must_cancel = []
+ for order in self.gox.orderbook.owns:
+ if is_own(order.price):
+ must_cancel.append(order)
+
+ for order in must_cancel:
+ 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
+ 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
+ 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
+ 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.
+ 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
+
+ # 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
+ 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()
+ if center:
+ self.debug("[s]center is %f" % self.gox.quote2float(center))
+ else:
+ return
+
+ 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
+
+ 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)
+
+ # 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
+
+ # 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)
+
+ # 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
+
+ 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("[s]WARNING! minimal sell amount adjusted to 0.01")
+
+ if buy_amount < 0.011 * COIN:
+ 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 %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)
+ 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,
+ 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.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] -= 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
+ 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%s %f at %f" % (
+ 'SIMULATION - ' if SIMULATE else '',
+ text,
+ gox.base2float(volume),
+ gox.quote2float(price)
+ ))
+
+ # 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:%S", 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)))
+ self.gox.cancel(order.oid)
+
+ 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
+
+ # 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 == {}:
+ 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()
+
+ def price_with_fees(self, price):
+ # Get our volume at price
+ volume_at_price = self.gox.base2float(self.get_buy_at_price(price))
+
+ 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 fees
+ fees_at_price = volume_at_price * self.gox.trade_fee / 100
+
+ 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,
+ self.gox.quote2float(price),
+ self.gox.curr_quote,
+ fees_at_price,
+ self.gox.curr_base,
+ self.gox.quote2float(price_with_fees),
+ self.gox.curr_quote))
+
+ # 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
+ then it will return that, otherwise return center - step"""
+ price = self.get_forced_price(center, False)
+ if not price:
+ price = int(round(center / step_factor))
+
+ if not center:
+ self.debug("[s]Waiting for price...")
+ elif int(conf['balancer_compensate_fees']):
+ # Decrease our next buy price
+ price = int(round(self.price_with_fees(price)))
+
+ 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 = int(round(center * step_factor))
+
+ # Compensate the fees on sell price
+ if int(conf['balancer_compensate_fees']) and center:
+ # Increase our next sell price
+ price = int(round(self.price_with_fees(price)))
+
+ return mark_own(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 * self.step_factor_sell:
+ return mark_own(price)
+ else:
+ for price in reversed(prices):
+ if price < center / self.step_factor:
+ return mark_own(price)
+
+ return None
diff --git a/buy.py b/buy.py
new file mode 100644
index 0000000..51fdf8e
--- /dev/null
+++ b/buy.py
@@ -0,0 +1,181 @@
+"""
+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
+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 = int(conf['buy_simulate'])
+
+# 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(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(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
+
+ 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%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 = []
+ 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.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.curr_quote])
+ 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.curr_quote)))
+
+ 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 = 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.curr_quote), 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" % (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 gox.quote2float(price) == 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 = gox.quote2float(self.gox.wallet[self.gox.curr_quote])
+ if volume != 0 and volume <= walletbalance:
+ buy_amount = volume
+ else:
+ buy_amount = walletbalance
+
+#end
diff --git a/goxapi.py b/goxapi.py
index 5155dc7..83c75a4 100644
--- a/goxapi.py
+++ b/goxapi.py
@@ -235,7 +235,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", "True"]
,["gox", "load_fulldepth", "True"]
@@ -1603,12 +1603,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
@@ -1914,10 +1916,11 @@ def _on_op_private_depth(self, msg):
delay = time.time() * 1e6 - timestamp
- 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
))
diff --git a/goxtool.py b/goxtool.py
index 2a97e97..e0e0fd0 100755
--- a/goxtool.py
+++ b/goxtool.py
@@ -45,13 +45,14 @@
#
HEIGHT_STATUS = 2
-HEIGHT_CON = 7
+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,11 +285,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) - 2
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 +322,47 @@ 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"])
+ 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
+ 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 ", COLOR_PAIR["con_separator"])
+ self.win.addstr(txt, COLOR_PAIR["con_text"])
+ self.done_paint()
+
class WinOrderBook(Win):
"""the orderbook window"""
@@ -948,17 +992,43 @@ 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 = []
+ total_btc = 0
+ total_fiat = 0
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(" +")
+ 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 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"])
else:
- line1 += "No info (yet)"
+ self.addstr("No info (yet)", COLOR_PAIR["status_text"] + curses.A_BOLD)
#
# second line
@@ -977,9 +1047,11 @@ 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)
- self.addstr(0, 0, line1, COLOR_PAIR["status_text"])
+ 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"])
@@ -1085,7 +1157,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)
@@ -1103,7 +1175,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 / =", "select"), ("F8", "cancel selected"), ("F10", "exit")]
keys = [(curses.KEY_F8, self._do_cancel)]
DlgListItems.__init__(self, stdscr, 45, "Cancel order(s)", hlp, keys)
@@ -1189,6 +1261,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
@@ -1489,8 +1563,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"""
@@ -1503,12 +1577,14 @@ def curses_loop(stdscr):
# we can print them.
try:
init_colors()
+
gox = goxapi.Gox(secret, config)
logwriter = LogWriter(gox)
printhook = PrintHook(gox)
conwin = WinConsole(stdscr, gox)
+ plugwin = PluginConsole(stdscr, gox)
bookwin = WinOrderBook(stdscr, gox)
statuswin = WinStatus(stdscr, gox)
chartwin = WinChart(stdscr, gox)
@@ -1516,6 +1592,7 @@ def curses_loop(stdscr):
strategy_manager = StrategyManager(gox, strat_mod_list)
gox.start()
+
while True:
key = stdscr.getch()
if key == ord("q"):
@@ -1532,6 +1609,7 @@ def curses_loop(stdscr):
stdscr.erase()
stdscr.refresh()
conwin.resize()
+ plugwin.resize()
bookwin.resize()
chartwin.resize()
statuswin.resize()
@@ -1582,6 +1660,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())
@@ -1638,7 +1717,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,
@@ -1680,14 +1759,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"
@@ -1695,9 +1769,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()
diff --git a/sell.py b/sell.py
new file mode 100644
index 0000000..ccc70c7
--- /dev/null
+++ b/sell.py
@@ -0,0 +1,180 @@
+"""
+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
+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 = int(conf['sell_simulate'])
+
+# 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(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(conf['sell_volume']) # 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%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 = []
+ 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 = gox.base2float(self.gox.wallet['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))
+
+ 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 = 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))
+ 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" % (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 gox.quote2float(price) == 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 = gox.base2float(self.gox.wallet['BTC'])
+ if volume != 0 and volume <= walletbalance:
+ sell_amount = volume
+ else:
+ sell_amount = walletbalance
+
+#end