├── .gitignore ├── CHANGES.TXT ├── LICENSE.TXT ├── README.md ├── btcebot ├── __init__.py ├── bot.py ├── database.py └── trader.py ├── samples ├── hello-world-bot.py └── logger-bot.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /CHANGES.TXT: -------------------------------------------------------------------------------- 1 | Changes in 0.3: 2 | - Changed version number to be in sync with btce-api library. 3 | - Use a single BTCEConnection to retrieve all depth and trade history 4 | from the public API. 5 | - In the main bot loop, request all API data in one shot before calling 6 | any trader update handlers. 7 | - In the hello-world-bot.py sample, use a single BTCEConnection to 8 | make trading calls in _attemptBuy and _attemptSell. 9 | 10 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2017 CodeReclaimers, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | btce-bot 2 | ======== 3 | 4 | NOTE: Due to the closure of BTC-e, this repository is no longer being maintained. 5 | I am leaving it here in case some of the code proves useful for the development of 6 | similar tools for other exchanges. 7 | 8 | NOTE: This is NOT a library of ready-made bots; it's a library that (hopefully) 9 | makes it easier to write your own bot, because you won't have to fuss with 10 | a bunch of low-level grunt work. It's still a work in progress, so if you'd 11 | like something implemented please feel free to ask for it! 12 | 13 | This library provides a simple framework for building trading bots for the 14 | BTC-e.com exchange site. So that you don't have to spend your time chasing 15 | down wacky dependencies, it depends only on the Python standard library and 16 | the [btce-api library](https://github.com/codereclaimers/btce-api). 17 | 18 | NOTE: BTC-e is not affiliated with this project. Use at your own risk. 19 | 20 | If you find the library useful and would like to donate (and many thanks to 21 | those that have donated!), please send some coins here: 22 | 23 | LTC LatrKXtfw66LQUURrxBzCE7cxFc9Sv8FWf 24 | BTC 16vnh6gwFYLGneBa8JUk7NaXpEt3Qojqs1 25 | DOGE 5jNqRjwxhDZT4hkG8yoGkseP576smjyNx 26 | 27 | If you are completely new to Python and/or programming, you should probably 28 | look here to get started: 29 | 30 | https://github.com/alanmcintyre/btce-bot/wiki/Getting-started 31 | 32 | -------------------------------------------------------------------------------- /btcebot/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CodeReclaimers, LLC 2 | 3 | from bot import Bot 4 | from database import MarketDatabase 5 | from trader import TraderBase -------------------------------------------------------------------------------- /btcebot/bot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CodeReclaimers, LLC 2 | 3 | import datetime 4 | import decimal 5 | import threading 6 | import time 7 | import traceback 8 | 9 | import btceapi 10 | import sys 11 | 12 | from trader import TraderBase 13 | 14 | def _runBot(bot): 15 | while bot.running: 16 | loop_start = time.time() 17 | 18 | # Collect the set of pairs for which we should get depth. 19 | depthPairs = set() 20 | for handler, pairs in bot.depthHandlers: 21 | depthPairs.update(pairs) 22 | 23 | # Get current depth 24 | depths = {} 25 | conn = btceapi.BTCEConnection() 26 | for p in depthPairs: 27 | try: 28 | asks, bids = btceapi.getDepth(p, conn) 29 | depths[p] = (datetime.datetime.now(), asks, bids) 30 | except: 31 | bot.onDepthRetrievalError(p, traceback.format_exc()) 32 | 33 | # Collect the set of pairs for which we should get trade history. 34 | tradeHistoryPairs = set() 35 | for handler, pairs in bot.tradeHistoryHandlers: 36 | tradeHistoryPairs.update(pairs) 37 | 38 | tradeHistories = {} 39 | for p in tradeHistoryPairs: 40 | try: 41 | trades = btceapi.getTradeHistory(p, conn) 42 | tradeHistories[p] = (datetime.datetime.now(), trades) 43 | except: 44 | bot.onTradeHistoryRetrievalError(p, traceback.format_exc()) 45 | conn.close() 46 | 47 | for p, (t, asks, bids) in depths.items(): 48 | for handler, pairs in bot.depthHandlers: 49 | if p in pairs: 50 | try: 51 | handler(t, p, asks, bids) 52 | except: 53 | bot.onDepthHandlingError(p, handler, traceback.format_exc()) 54 | 55 | for p, (t, trades) in tradeHistories.items(): 56 | # Merge new trades into the bot's history. 57 | bot.mergeTradeHistory(p, trades) 58 | 59 | # Provide full history to traders 60 | for handler, pairs in bot.tradeHistoryHandlers: 61 | if p in pairs: 62 | try: 63 | handler(t, p, bot.tradeHistoryItems[p]) 64 | except: 65 | exc_type, exc_value, exc_traceback = sys.exc_info() 66 | tb = traceback.format_exception(exc_type, exc_value, exc_traceback) 67 | bot.onTradeHistoryHandlingError(p, handler, tb) 68 | 69 | # Tell all bots that have requested it that we're at the end 70 | # of an update loop. 71 | for handler in bot.loopEndHandlers: 72 | try: 73 | handler(datetime.datetime.now()) 74 | except: 75 | # TODO: refactor this somewhere 76 | t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 77 | print "%s Error while calling loop end handler (%r): %s" % (t, handler, traceback.format_exc()) 78 | 79 | while bot.running and time.time() - loop_start < bot.collectionInterval: 80 | time.sleep(0.5) 81 | 82 | # Give traders and opportunity to do thread-specific cleanup. 83 | for t in bot.traders: 84 | t.onExit() 85 | 86 | 87 | class Bot(object): 88 | def __init__(self, api, bufferSpanMinutes=10): 89 | self.api = api 90 | self.bufferSpanMinutes = bufferSpanMinutes 91 | self.depthHandlers = [] 92 | self.tradeHistoryHandlers = [] 93 | self.loopEndHandlers = [] 94 | self.collectionInterval = 60.0 95 | self.running = False 96 | self.traders = set() 97 | 98 | self.tradeHistoryIds = {} 99 | self.tradeHistoryItems = {} 100 | 101 | self.errorHandlers = [] 102 | 103 | def addErrorHandler(self, handler): 104 | '''Add a handler function taking two arguments: a string describing 105 | what operation was in process, and a string containing the 106 | formatted traceback. If an exception is raised inside the handler, 107 | it will be ignored.''' 108 | # TODO: inspect function to make sure it has 109 | # the right number of arguments. 110 | self.errorHandlers.append(handler) 111 | 112 | def onDepthRetrievalError(self, pair, tracebackText): 113 | msg = "Error while retrieving %s depth" % pair 114 | for h in self.errorHandlers: 115 | try: 116 | h(msg, tracebackText) 117 | except: 118 | pass 119 | 120 | def onDepthHandlingError(self, pair, handler, tracebackText): 121 | msg = "Error in handler %r for %s depth" % (handler, pair) 122 | for h in self.errorHandlers: 123 | try: 124 | h(msg, tracebackText) 125 | except: 126 | pass 127 | 128 | def onTradeHistoryRetrievalError(self, pair, tracebackText): 129 | msg = "Error while retrieving %s trade history" % pair 130 | for h in self.errorHandlers: 131 | try: 132 | h(msg, tracebackText) 133 | except: 134 | pass 135 | 136 | def onTradeHistoryHandlingError(self, pair, handler, tracebackText): 137 | msg = "Error in handler %r for %s trade history" % (handler, pair) 138 | for h in self.errorHandlers: 139 | try: 140 | h(msg, tracebackText) 141 | except: 142 | pass 143 | 144 | def mergeTradeHistory(self, pair, history): 145 | keys = self.tradeHistoryIds.setdefault(pair, set()) 146 | prevItems = self.tradeHistoryItems.get(pair, []) 147 | newItems = [] 148 | 149 | # Remove old items 150 | now = decimal.Decimal(time.time()) 151 | dt_sec = self.bufferSpanMinutes * 60 152 | for h in prevItems: 153 | if now - h.timestamp > dt_sec: 154 | keys.remove(h.tid) 155 | else: 156 | keys.add(h.tid) 157 | newItems.append(h) 158 | 159 | # Add new items 160 | for h in history: 161 | if h.tid not in keys: 162 | keys.add(h.tid) 163 | newItems.append(h) 164 | 165 | self.tradeHistoryItems[pair] = newItems 166 | 167 | def addTrader(self, trader): 168 | if trader.onNewDepth.__func__ is not TraderBase.onNewDepth.__func__: 169 | self.addDepthHandler(trader.onNewDepth, trader.pairs) 170 | 171 | if trader.onNewTradeHistory.__func__ is not TraderBase.onNewTradeHistory.__func__: 172 | self.addTradeHistoryHandler(trader.onNewTradeHistory, trader.pairs) 173 | 174 | if trader.onLoopEnd.__func__ is not TraderBase.onLoopEnd.__func__: 175 | self.addLoopEndHandler(trader.onLoopEnd) 176 | 177 | self.traders.add(trader) 178 | 179 | def addDepthHandler(self, handler, pairs): 180 | for p in pairs: 181 | self.api.validate_pair(p) 182 | 183 | self.depthHandlers.append((handler, pairs)) 184 | 185 | def addTradeHistoryHandler(self, handler, pairs): 186 | for p in pairs: 187 | self.api.validate_pair(p) 188 | 189 | self.tradeHistoryHandlers.append((handler, pairs)) 190 | 191 | def addLoopEndHandler(self, handler): 192 | self.loopEndHandlers.append(handler) 193 | 194 | def setCollectionInterval(self, interval_seconds): 195 | self.collectionInterval = interval_seconds 196 | 197 | def start(self): 198 | self.running = True 199 | self.thread = threading.Thread(target = _runBot, args=(self,)) 200 | self.thread.start() 201 | 202 | def stop(self): 203 | self.running = False 204 | self.thread.join() 205 | 206 | -------------------------------------------------------------------------------- /btcebot/database.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CodeReclaimers, LLC 2 | 3 | import cPickle 4 | import datetime 5 | import decimal 6 | import os.path 7 | import sqlite3 8 | 9 | import btceapi 10 | from btceapi.public import Trade 11 | 12 | # Add support for conversion to/from decimal 13 | def adapt_decimal(d): 14 | return int(d*decimal.Decimal("1e8")) 15 | 16 | def convert_decimal(s): 17 | return decimal.Decimal(s) * decimal.Decimal("1e-8") 18 | 19 | sqlite3.register_adapter(decimal.Decimal, adapt_decimal) 20 | sqlite3.register_converter("DECIMAL", convert_decimal) 21 | 22 | class MarketDatabase(object): 23 | def __init__(self, database_path, all_pairs): 24 | create = not os.path.isfile(database_path) 25 | self.connection = sqlite3.connect(database_path) 26 | self.cursor = self.connection.cursor() 27 | if create: 28 | # The database is new, so create tables and populate the enumerations. 29 | self.createTables() 30 | 31 | # Pairs table 32 | pairs = zip(range(len(all_pairs)), all_pairs) 33 | self.cursor.executemany("INSERT INTO pairs VALUES(?, ?)", pairs) 34 | self.pair_to_index = dict((p, i) for i, p in pairs) 35 | self.index_to_pair = dict(pairs) 36 | 37 | # Trade types table 38 | trade_types = [(0, "bid"), (1, "ask")] 39 | self.cursor.executemany("INSERT INTO trade_types VALUES(?, ?)", trade_types) 40 | self.tradetype_to_index = dict((tt, i) for i, tt in trade_types) 41 | self.index_to_tradetype = dict(trade_types) 42 | 43 | self.connection.commit() 44 | 45 | else: 46 | # The database isn't new, so just retrieve enumerations from it. 47 | 48 | self.cursor.execute("SELECT id, name from pairs") 49 | self.index_to_pair = dict(self.cursor.fetchall()) 50 | self.pair_to_index = dict((p, i) for i, p in self.index_to_pair.items()) 51 | 52 | self.cursor.execute("SELECT id, name from trade_types") 53 | self.index_to_tradetype = dict(self.cursor.fetchall()) 54 | self.tradetype_to_index = dict((p, i) for i, p in self.index_to_tradetype.items()) 55 | 56 | def createTables(self): 57 | self.cursor.execute(''' 58 | CREATE TABLE pairs( 59 | id INT PRIMARY KEY, 60 | name TEXT 61 | );''') 62 | 63 | self.cursor.execute(''' 64 | CREATE TABLE trade_types( 65 | id INT PRIMARY KEY, 66 | name TEXT 67 | );''') 68 | 69 | self.cursor.execute(''' 70 | CREATE TABLE trade_history( 71 | tid INT PRIMARY KEY, 72 | pair INT, 73 | trade_type INT, 74 | price DECIMAL, 75 | amount DECIMAL, 76 | timestamp INT, 77 | FOREIGN KEY(pair) REFERENCES pairs(id), 78 | FOREIGN KEY(trade_type) REFERENCES trade_types(id) 79 | );''') 80 | 81 | self.cursor.execute(''' 82 | CREATE TABLE depth( 83 | timestamp INT, 84 | pair INT, 85 | asks BLOB, 86 | bids BLOB, 87 | FOREIGN KEY(pair) REFERENCES pairs(id) 88 | );''') 89 | 90 | self.connection.commit() 91 | 92 | def close(self): 93 | self.cursor = None 94 | if self.connection is not None: 95 | self.connection.close() 96 | self.connection = None 97 | 98 | def tupleFromTrade(self, t): 99 | return (t.tid, 100 | self.pair_to_index[t.pair], 101 | self.tradetype_to_index[t.type], 102 | t.price, 103 | t.amount, 104 | t.timestamp) 105 | 106 | def insertTradeHistory(self, trade_data): 107 | ''' 108 | Add one or more trades to the trade history store. If trade_data is a 109 | list, then it is assumed to be a list of multiple trades; if it is a tuple, 110 | or a btceapi.Trade object, it is assumed to represent a single trade. 111 | Tuples should be (trade id, pair id, trade type id, price, amount, date). 112 | ''' 113 | if type(trade_data) is not list: 114 | trade_data = [trade_data] 115 | 116 | if type(trade_data[0]) is Trade: 117 | trade_data = map(self.tupleFromTrade, trade_data) 118 | 119 | self.cursor.executemany("INSERT OR IGNORE INTO trade_history VALUES(?, ?, ?, ?, ?, ?)", trade_data) 120 | self.connection.commit() 121 | 122 | def retrieveTradeHistory(self, start_date, end_date, pair): 123 | vars = ("tid", "trade_type", "price", "amount", "date", "pair", "trade_type") 124 | pair_index = self.pair_to_index[pair] 125 | sql = """select tid, trade_type, price, amount, date, pairs.name, trade_types.name 126 | from trade_history, pairs, trade_types 127 | where pair == ? and date >= ? 128 | and date <= ? 129 | and trade_history.pair == pairs.id 130 | and trade_history.trade_type == trade_types.id 131 | order by date""" 132 | 133 | for row in self.cursor.execute(sql, (pair_index, start_date, end_date)): 134 | row = dict(zip(vars, row)) 135 | yield Trade(**row) 136 | 137 | def insertDepth(self, dt, pair, asks, bids): 138 | depth_data = (dt, 139 | self.pair_to_index[pair], 140 | cPickle.dumps(asks), 141 | cPickle.dumps(bids)) 142 | self.cursor.execute("INSERT INTO depth VALUES(?, ?, ?, ?)", depth_data) 143 | self.connection.commit() 144 | 145 | def retrieveDepth(self, start_date, end_date, pair): 146 | pair_index = self.pair_to_index[pair] 147 | sql = """select date, asks, bids 148 | from depth, pairs 149 | where pair == ? 150 | and date >= ? 151 | and date <= ? 152 | and depth.pair == pairs.id 153 | order by date""" 154 | 155 | depth = [] 156 | for d, asks, bids in self.cursor.execute(sql, (pair_index, start_date, end_date)): 157 | dt, frac = d.split(".") 158 | # TODO: refactor this somewhere 159 | d = datetime.datetime.strptime(dt, "%Y-%m-%d %H:%M:%S") 160 | asks = cPickle.loads(str(asks)) 161 | bids = cPickle.loads(str(bids)) 162 | yield d, asks, bids 163 | -------------------------------------------------------------------------------- /btcebot/trader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2017 CodeReclaimers, LLC 2 | 3 | class TraderBase(object): 4 | def __init__(self, pairs): 5 | self.pairs = pairs 6 | 7 | def onNewDepth(self, t, pair, asks, bids): 8 | pass 9 | 10 | def onNewTradeHistory(self, t, pair, trades): 11 | pass 12 | 13 | def onLoopEnd(self, t): 14 | pass 15 | 16 | def onExit(self): 17 | pass 18 | 19 | -------------------------------------------------------------------------------- /samples/hello-world-bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (c) 2013-2017 CodeReclaimers, LLC 3 | 4 | import decimal 5 | import time 6 | 7 | import btceapi 8 | import btcebot 9 | 10 | class RangeTrader(btcebot.TraderBase): 11 | ''' 12 | This is a simple trader that handles a single currency pair, selling 13 | all available inventory if price is above sell_price, buying with 14 | all available funds if price is below buy_price. Use for actual trading 15 | at your own risk (and remember this is just a sample, not a recommendation 16 | on how to make money trading using this framework). 17 | ''' 18 | def __init__(self, api, pair, buy_price, sell_price, live_trades = False): 19 | btcebot.TraderBase.__init__(self, (pair,)) 20 | self.api = api 21 | self.pair = pair 22 | self.buy_price = buy_price 23 | self.sell_price = sell_price 24 | self.live_trades = live_trades 25 | 26 | self.current_lowest_ask = None 27 | self.current_highest_bid = None 28 | 29 | # Apparently the API adds the fees to the amount you submit, 30 | # so dial back the order just enough to make up for the 31 | # 0.2% trade fee. 32 | self.fee_adjustment = decimal.Decimal("0.998") 33 | 34 | def _attemptBuy(self, price, amount): 35 | conn = btceapi.BTCEConnection() 36 | info = self.api.getInfo(conn) 37 | curr1, curr2 = self.pair.split("_") 38 | 39 | # Limit order to what we can afford to buy. 40 | available = getattr(info, "balance_" + curr2) 41 | max_buy = available / price 42 | buy_amount = min(max_buy, amount) * self.fee_adjustment 43 | if buy_amount >= btceapi.min_orders[self.pair]: 44 | print "attempting to buy %s %s at %s for %s %s" % (buy_amount, 45 | curr1.upper(), price, buy_amount*price, curr2.upper()) 46 | if self.live_trades: 47 | r = self.api.trade(self.pair, "buy", price, buy_amount, conn) 48 | print "\tReceived %s %s" % (r.received, curr1.upper()) 49 | # If the order didn't fill completely, cancel the remaining order 50 | if r.order_id != 0: 51 | print "\tCanceling unfilled portion of order" 52 | self.api.cancelOrder(r.order_id, conn) 53 | 54 | def _attemptSell(self, price, amount): 55 | conn = btceapi.BTCEConnection() 56 | info = self.api.getInfo(conn) 57 | curr1, curr2 = self.pair.split("_") 58 | 59 | # Limit order to what we have available to sell. 60 | available = getattr(info, "balance_" + curr1) 61 | sell_amount = min(available, amount) * self.fee_adjustment 62 | if sell_amount >= btceapi.min_orders[self.pair]: 63 | print "attempting to sell %s %s at %s for %s %s" % (sell_amount, 64 | curr1.upper(), price, sell_amount*price, curr2.upper()) 65 | if self.live_trades: 66 | r = self.api.trade(self.pair, "sell", price, sell_amount, conn) 67 | print "\tReceived %s %s" % (r.received, curr2.upper()) 68 | # If the order didn't fill completely, cancel the remaining order 69 | if r.order_id != 0: 70 | print "\tCanceling unfilled portion of order" 71 | self.api.cancelOrder(r.order_id, conn) 72 | 73 | # This overrides the onNewDepth method in the TraderBase class, so the 74 | # framework will automatically pick it up and send updates to it. 75 | def onNewDepth(self, t, pair, asks, bids): 76 | ask_price, ask_amount = asks[0] 77 | bid_price, bid_amount = bids[0] 78 | if ask_price <= self.buy_price: 79 | self._attemptBuy(ask_price, ask_amount) 80 | elif bid_price >= self.sell_price: 81 | self._attemptSell(bid_price, bid_amount) 82 | 83 | 84 | def onBotError(msg, tracebackText): 85 | tstr = time.strftime("%Y/%m/%d %H:%M:%S") 86 | print "%s - %s" % (tstr, msg) 87 | open("hello-world-bot-error.log", "a").write( 88 | "%s - %s\n%s\n%s\n" % (tstr, msg, tracebackText, "-"*80)) 89 | 90 | def run(key_file, buy_floor, sell_ceiling, live_trades): 91 | # Load the keys and create an API object from the first one. 92 | handler = btceapi.KeyHandler(key_file) 93 | key = handler.getKeys()[0] 94 | print "Trading with key %s" % key 95 | api = btceapi.TradeAPI(key, handler) 96 | 97 | # Create a trader that handles LTC/USD trades in the given range. 98 | trader = RangeTrader(api, "ltc_usd", buy_floor, sell_ceiling, live_trades) 99 | 100 | # Create a bot and add the trader to it. 101 | bot = btcebot.Bot() 102 | bot.addTrader(trader) 103 | 104 | # Add an error handler so we can print info about any failures 105 | bot.addErrorHandler(onBotError) 106 | 107 | # The bot will provide the traders with updated information every 108 | # 15 seconds. 109 | bot.setCollectionInterval(15) 110 | bot.start() 111 | print "Running; press Ctrl-C to stop" 112 | 113 | try: 114 | while 1: 115 | # you can do anything else you prefer in this loop while 116 | # the bot is running in the background 117 | time.sleep(3600) 118 | 119 | except KeyboardInterrupt: 120 | print "Stopping..." 121 | finally: 122 | bot.stop() 123 | 124 | if __name__ == '__main__': 125 | import argparse 126 | parser = argparse.ArgumentParser(description='Simple range trader example.') 127 | parser.add_argument('key_file', 128 | help='Path to a file containing key/secret/nonce data.') 129 | parser.add_argument('buy_floor', type=decimal.Decimal, 130 | help='Price at or below which we will buy.') 131 | parser.add_argument('sell_ceiling', type=decimal.Decimal, 132 | help='Price at or above which we will sell.') 133 | parser.add_argument('--live-trades', default=False, action="store_true", 134 | help='Actually make trades.') 135 | 136 | args = parser.parse_args() 137 | 138 | if args.buy_floor >= args.sell_ceiling: 139 | raise Exception("Buy price should probably be below sell price!") 140 | 141 | run(args.key_file, args.buy_floor, args.sell_ceiling, args.live_trades) 142 | -------------------------------------------------------------------------------- /samples/logger-bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (c) 2013-2017 CodeReclaimers, LLC 3 | 4 | import time 5 | 6 | import btceapi 7 | import btcebot 8 | 9 | class MarketDataLogger(btcebot.TraderBase): 10 | ''' 11 | This "trader" simply logs all of the updates it receives from the bot. 12 | ''' 13 | 14 | def __init__(self, api, pairs, database_path): 15 | self.api = api 16 | btcebot.TraderBase.__init__(self, pairs) 17 | self.database_path = database_path 18 | self.db = None 19 | self.trade_history_seen = {} 20 | 21 | def getDB(self): 22 | # The database is lazily created here instead of the constructor 23 | # so that it can be created and used in the bot's thread. 24 | if self.db is None: 25 | self.db = btcebot.MarketDatabase(self.database_path, self.api.pair_names) 26 | 27 | return self.db 28 | 29 | def onExit(self): 30 | if self.db is not None: 31 | self.db.close() 32 | 33 | # This overrides the onNewDepth method in the TraderBase class, so the 34 | # framework will automatically pick it up and send updates to it. 35 | def onNewDepth(self, t, pair, asks, bids): 36 | print "%s Entering new %s depth" % (t, pair) 37 | self.getDB().insertDepth(t, pair, asks, bids) 38 | 39 | # This overrides the onNewTradeHistory method in the TraderBase class, so the 40 | # framework will automatically pick it up and send updates to it. 41 | def onNewTradeHistory(self, t, pair, trades): 42 | history = self.trade_history_seen.setdefault(pair, set()) 43 | 44 | new_trades = filter(lambda trade: trade.tid not in history, trades) 45 | if new_trades: 46 | print "%s Entering %d new %s trades" % (t, len(new_trades), pair) 47 | self.getDB().insertTradeHistory(new_trades) 48 | history.update(t.tid for t in new_trades) 49 | 50 | 51 | def onBotError(msg, tracebackText): 52 | tstr = time.strftime("%Y/%m/%d %H:%M:%S") 53 | print "%s - %s" % (tstr, msg) 54 | open("logger-bot-error.log", "a").write( 55 | "%s - %s\n%s\n%s\n" % (tstr, msg, tracebackText, "-"*80)) 56 | 57 | def run(database_path): 58 | conn = btceapi.BTCEConnection() 59 | api = btceapi.APIInfo(conn) 60 | #logger = MarketDataLogger(api.pair_names, database_path) 61 | logger = MarketDataLogger(api, ("btc_usd", "ltc_usd"), database_path) 62 | 63 | # Create a bot and add the logger to it. 64 | bot = btcebot.Bot(api) 65 | bot.addTrader(logger) 66 | 67 | # Add an error handler so we can print info about any failures 68 | bot.addErrorHandler(onBotError) 69 | 70 | # The bot will provide the logger with updated information every 71 | # 60 seconds. 72 | bot.setCollectionInterval(60) 73 | bot.start() 74 | print "Running; press Ctrl-C to stop" 75 | 76 | try: 77 | while 1: 78 | # you can do anything else you prefer in this loop while 79 | # the bot is running in the background 80 | time.sleep(3600) 81 | 82 | except KeyboardInterrupt: 83 | print "Stopping..." 84 | finally: 85 | bot.stop() 86 | 87 | 88 | if __name__ == '__main__': 89 | import argparse 90 | parser = argparse.ArgumentParser(description='Data logging example.') 91 | parser.add_argument('--db-path', default='btce.db', 92 | help='Path to the logger database.') 93 | 94 | args = parser.parse_args() 95 | run(args.db_path) 96 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup(name='btce-bot', 5 | version='0.9', 6 | author='CodeReclaimers, LLC', 7 | author_email='alan@codereclaimers.com', 8 | url='https://github.com/CodeReclaimers/btce-bot', 9 | license="MIT", 10 | description='A framework for building trading bots for the digital currency trading site BTC-e.com.', 11 | packages=['btcebot'], 12 | classifiers=[ 13 | 'Development Status :: 2 - Pre-Alpha', 14 | 'Intended Audience :: Developers' 15 | ] 16 | ) 17 | --------------------------------------------------------------------------------