├── indicator ├── __init__.py ├── base.py ├── timesum.py ├── candlestick.py ├── timeminmax.py └── ma.py ├── pylintrc ├── .gitignore ├── plot_pickled.py ├── strategy_plot.py ├── exchange_connection.py ├── strategy_trailing_stoploss.py ├── strategy_simple_mean_reversion.py ├── strategy_simple_trend_follower.py ├── strategy_volume_trend_follower.py ├── mtgoxdata ├── mtgox-plot-prices-to-file.py └── download-mtgox-data.py ├── strategy.py ├── README.md ├── strategy_logic_trailing_stoploss.py ├── strategy_logic_simple_trend_follower.py ├── strategy_logic_simple_mean_reversion.py ├── strategy_logic_volume_trend_follower.py ├── websocket.py └── goxtool.py /indicator/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["base", "ma", "candlestick", "timesum", "timeminmax"] -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [REPORTS] 2 | output-format=parseable 3 | include-ids=yes 4 | reports=no 5 | 6 | [MESSAGES CONTROL] 7 | disable=I0011 8 | -------------------------------------------------------------------------------- /.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 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Pickles 39 | .pickle 40 | 41 | # logs 42 | .log 43 | -------------------------------------------------------------------------------- /plot_pickled.py: -------------------------------------------------------------------------------- 1 | import strategy_plot 2 | from strategy_core_trailing_stoploss import StrategyCoreTrailingStoploss 3 | from exchange_connection import MockExchangeConnection 4 | 5 | def plotStrategyCorePerformance(debugData): 6 | 7 | subplots = 1 8 | splot = strategy_plot.StrategyPlot(debugData, subplots) 9 | splot.Plot("RawPrice",1, "y-") 10 | splot.Plot("Sell", 1, "go") 11 | splot.Plot("Buy", 1, "y^") 12 | splot.Plot("PriceEmaFast", 1, "b-") 13 | 14 | splot.Show() 15 | 16 | def main(): 17 | # Test this strategy core by mocking ExchangeConnection 18 | # And by feeding it the prerecorded data 19 | xcon = MockExchangeConnection() 20 | score = StrategyCoreTrailingStoploss(xcon, debug = True) 21 | score.Load() 22 | plotStrategyCorePerformance(score._debugData) 23 | 24 | if __name__ == "__main__": 25 | main() -------------------------------------------------------------------------------- /indicator/base.py: -------------------------------------------------------------------------------- 1 | class Updatable: 2 | def Update(self, data): 3 | pass 4 | 5 | class Runnable: 6 | def Run(self): 7 | pass 8 | def Stop(self): 9 | pass 10 | 11 | class Indicator(Updatable): 12 | 13 | def _checkData(self, data): 14 | if (data==None): 15 | return False 16 | 17 | if (len(data)==0): 18 | return False 19 | 20 | if ("now" not in data): 21 | return False 22 | 23 | if ("value" not in data): 24 | return False 25 | 26 | if ( (not isinstance(data["value"], float)) and (not isinstance(data["value"], int)) ): 27 | return False 28 | 29 | if ( (not isinstance(data["now"], float)) and (not isinstance(data["now"], int)) ): 30 | return False 31 | 32 | return True 33 | -------------------------------------------------------------------------------- /strategy_plot.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | import pickle 4 | import sqlite3 5 | import calendar 6 | import math 7 | from indicator.ma import ExponentialMovingAverage as ema 8 | from indicator.ma import SimpleMovingAverage as sma 9 | from indicator.candlestick import CandleStick 10 | from indicator.timesum import TimeSum 11 | from exchange_connection import ExchangeConnection, MockExchangeConnection 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | import matplotlib.dates as pltdates 15 | 16 | class StrategyPlot: 17 | 18 | def __init__(self, debugData, numberSubplots): 19 | self.debugData = debugData 20 | self.numberSubplots = numberSubplots 21 | self.xfrom = None 22 | self.xto = None 23 | 24 | # Get the right x scale 25 | time = [] 26 | for item in debugData["RawPrice"]: 27 | time.append( datetime.datetime.fromtimestamp(item["now"]) ) 28 | self.SetXLimits(time[0], time[-1]) 29 | 30 | def SetXLimits(self, xfrom, xto): 31 | self.xfrom = xfrom 32 | self.xto = xto 33 | 34 | def Plot(self, plotName, subplot, format="r-"): 35 | 36 | if (plotName not in self.debugData): 37 | return 38 | 39 | time = [] 40 | value = [] 41 | for item in self.debugData[plotName]: 42 | time.append( datetime.datetime.fromtimestamp(item["now"]) ) 43 | value.append(item["value"]) 44 | 45 | plt.subplot(self.numberSubplots,1,subplot) 46 | plt.plot(time, value, format) 47 | plt.ylabel(plotName) 48 | if ( (self.xfrom != None) and (self.xto != None) ): 49 | plt.xlim([self.xfrom, self.xto]) 50 | 51 | def Show(self): 52 | plt.show() 53 | -------------------------------------------------------------------------------- /indicator/timesum.py: -------------------------------------------------------------------------------- 1 | # Candle sticks 2 | import datetime 3 | import time 4 | from base import Indicator 5 | 6 | # Indicator to represent a sum of values over a time window 7 | # For example, the volume of all trades during a time period 8 | class TimeSum(Indicator): 9 | 10 | Value = 0.0 11 | 12 | # Open timestamp 13 | OpenTime = datetime.datetime.fromtimestamp(time.time()); 14 | 15 | # Close timestamp 16 | CloseTime = OpenTime; 17 | 18 | def __init__(self, openTime, closeTime): 19 | if (isinstance(openTime, datetime.datetime)): 20 | self.OpenTime = openTime 21 | if (isinstance(closeTime, datetime.datetime)): 22 | self.CloseTime = closeTime 23 | 24 | self._is_closed = False 25 | 26 | # Returns true if candle stick accumulated enough data to represent the 27 | # time span between Opening and Closing timestamps 28 | def IsAccurate(self): 29 | return self._is_closed 30 | 31 | def Update(self, data): 32 | 33 | if ( self.CloseTime < self.OpenTime ): 34 | self._resetValue(0.0) 35 | self._is_closed = False 36 | return 37 | 38 | if (not self._checkData(data)): 39 | return 40 | 41 | _current_timestamp = datetime.datetime.fromtimestamp(data["now"]) 42 | _value = data["value"] 43 | 44 | if (_current_timestamp >= self.CloseTime): 45 | self._is_closed = True 46 | 47 | if (_current_timestamp <= self.CloseTime and _current_timestamp >= self.OpenTime): 48 | self._updateData(_value) 49 | 50 | def _resetValue(self, value): 51 | self.Value = value 52 | 53 | # Update the running timestamps of the data 54 | def _updateData(self, value): 55 | # Update the values in case the current datapoint is a current High/Low 56 | self.Value += value 57 | -------------------------------------------------------------------------------- /exchange_connection.py: -------------------------------------------------------------------------------- 1 | import goxapi 2 | 3 | class ExchangeConnection: 4 | def __init__(self, gox): 5 | self.gox = gox 6 | def AvailableBTC(self): 7 | btc = self.gox.wallet[self.gox.curr_base] 8 | return self.gox.base2float(btc) 9 | def AvailableUSD(self): 10 | usd = self.gox.wallet[self.gox.curr_quote] 11 | return self.gox.quote2float(usd) 12 | def SellBTC(self,amount): 13 | print "Selling " + str(amount) + " BTC" 14 | self.gox.sell(0, self.gox.base2int(amount)) 15 | def BuyBTC(self,amount): 16 | print "Buying " + str(amount) + " BTC" 17 | self.gox.buy(0, self.gox.base2int(amount)) 18 | 19 | class MockExchangeConnection(ExchangeConnection): 20 | def __init__(self, availableBTC = 0.0, availableUSD = 10.0, currentPrice = 200.0): 21 | self.availableBTC = availableBTC 22 | self.availableUSD = availableUSD 23 | self.currentPrice = currentPrice 24 | def SetBTCPrice(self, price): 25 | self.currentPrice = price 26 | def AvailableBTC(self): 27 | return self.availableBTC 28 | def AvailableUSD(self): 29 | return self.availableUSD 30 | def SellBTC(self, amount): 31 | if (amount <= 0.0): 32 | return 33 | if (self.availableBTC < amount): 34 | print "SellBTC: not enough BTC to sell: I was asked to sell " + str(amount) + ", but I only have " + str(self.availableBTC) 35 | return 36 | self.availableUSD += amount * self.currentPrice 37 | self.availableBTC -= amount 38 | self.availableUSD = self.availableUSD - self.availableUSD * 0.006 39 | def BuyBTC(self, amount): 40 | if (amount <= 0.0): 41 | return 42 | if (self.availableUSD / self.currentPrice < amount): 43 | print "BuyBTC: not enough USD to buy BTC: I was asked to buy " + str(amount) + "BTC, but I only have " + str(self.availableUSD) + "USD, and the current price is " + str(self.currentPrice) 44 | return 45 | self.availableUSD -= self.currentPrice * amount 46 | self.availableBTC += amount 47 | self.availableBTC -= self.availableBTC * 0.006 -------------------------------------------------------------------------------- /strategy_trailing_stoploss.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import goxapi 3 | import time 4 | import datetime 5 | from strategy_logic_trailing_stoploss import StrategyLogicTrailingStoploss 6 | from exchange_connection import ExchangeConnection 7 | 8 | class Strategy(goxapi.BaseObject): 9 | # pylint: disable=C0111,W0613,R0201 10 | 11 | def __init__(self, gox): 12 | goxapi.BaseObject.__init__(self) 13 | self.signal_debug.connect(gox.signal_debug) 14 | gox.signal_keypress.connect(self.slot_keypress) 15 | gox.signal_strategy_unload.connect(self.slot_before_unload) 16 | gox.signal_trade.connect(self.slot_trade) 17 | self.gox = gox 18 | self.name = "%s.%s" % \ 19 | (self.__class__.__module__, self.__class__.__name__) 20 | 21 | self.exchangeConnection = ExchangeConnection(self.gox) 22 | self.strategy_logic = StrategyLogicTrailingStoploss(self.exchangeConnection) 23 | self.strategy_logic.Load() 24 | 25 | self.debug("%s loaded" % self.name) 26 | 27 | def slot_before_unload(self, _sender, _data): 28 | self.debug("%s before_unload" % self.name) 29 | self.strategy_logic.Save() 30 | 31 | def slot_keypress(self, gox, (key)): 32 | 33 | # sell all BTC as market order 34 | if ( key == ord('s') ): 35 | self.strategy_logic.ConvertAllToUSD() 36 | 37 | # spend all USD to buy BTC as market order 38 | if ( key == ord('b') ): 39 | self.strategy_logic.ConvertAllToBTC() 40 | 41 | #dump the state of strategy_logic 42 | if ( key == ord('d') ): 43 | self.strategy_logic.Save() 44 | 45 | # immediately cancel all orders 46 | if ( key == ord('c') ): 47 | for order in gox.orderbook.owns: 48 | gox.cancel(order.oid) 49 | 50 | def slot_trade(self, gox, (date, price, volume, typ, own)): 51 | # a trade message has been received 52 | # update price indicators and potentially trigger trading logic 53 | data = {"now": float(time.time()), "value": gox.quote2float(price) } 54 | self.strategy_logic.UpdatePrice(data) 55 | self.strategy_logic.Act() 56 | -------------------------------------------------------------------------------- /strategy_simple_mean_reversion.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import goxapi 3 | import time 4 | import datetime 5 | from strategy_logic_simple_mean_reversion import StrategyLogicSimpleMeanReversion 6 | from exchange_connection import ExchangeConnection 7 | 8 | class Strategy(goxapi.BaseObject): 9 | # pylint: disable=C0111,W0613,R0201 10 | 11 | def __init__(self, gox): 12 | goxapi.BaseObject.__init__(self) 13 | self.signal_debug.connect(gox.signal_debug) 14 | gox.signal_keypress.connect(self.slot_keypress) 15 | gox.signal_strategy_unload.connect(self.slot_before_unload) 16 | gox.signal_trade.connect(self.slot_trade) 17 | self.gox = gox 18 | self.name = "%s.%s" % \ 19 | (self.__class__.__module__, self.__class__.__name__) 20 | 21 | self.exchangeConnection = ExchangeConnection(self.gox) 22 | self.strategy_logic = StrategyLogicSimpleMeanReversion(self.exchangeConnection) 23 | self.strategy_logic.Load() 24 | 25 | self.debug("%s loaded" % self.name) 26 | 27 | def __del__(self): 28 | self.debug("%s unloaded" % self.name) 29 | 30 | def slot_before_unload(self, _sender, _data): 31 | self.debug("%s before_unload" % self.name) 32 | self.strategy_logic.Save() 33 | 34 | def slot_keypress(self, gox, (key)): 35 | 36 | # sell all BTC as market order 37 | if ( key == ord('s') ): 38 | self.strategy_logic.ConvertAllToUSD() 39 | 40 | # spend all USD to buy BTC as market order 41 | if ( key == ord('b') ): 42 | self.strategy_logic.ConvertAllToBTC() 43 | 44 | #dump the state of strategy_logic 45 | if ( key == ord('d') ): 46 | self.strategy_logic.Save() 47 | 48 | # immediately cancel all orders 49 | if ( key == ord('c') ): 50 | for order in gox.orderbook.owns: 51 | gox.cancel(order.oid) 52 | 53 | def slot_trade(self, gox, (date, price, volume, typ, own)): 54 | # a trade message has been received 55 | 56 | # update price indicators 57 | data = {"now": float(time.time()), "value": gox.quote2float(price) } 58 | self.strategy_logic.UpdatePrice(data) 59 | 60 | # Trigger the trading logic 61 | self.strategy_logic.Act() 62 | -------------------------------------------------------------------------------- /strategy_simple_trend_follower.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import goxapi 3 | import time 4 | import datetime 5 | from strategy_logic_simple_trend_follower import StrategyLogicSimpleTrendFollower 6 | from exchange_connection import ExchangeConnection 7 | 8 | class Strategy(goxapi.BaseObject): 9 | # pylint: disable=C0111,W0613,R0201 10 | 11 | def __init__(self, gox): 12 | goxapi.BaseObject.__init__(self) 13 | self.signal_debug.connect(gox.signal_debug) 14 | gox.signal_keypress.connect(self.slot_keypress) 15 | gox.signal_strategy_unload.connect(self.slot_before_unload) 16 | gox.signal_trade.connect(self.slot_trade) 17 | self.gox = gox 18 | self.name = "%s.%s" % \ 19 | (self.__class__.__module__, self.__class__.__name__) 20 | 21 | self.exchangeConnection = ExchangeConnection(self.gox) 22 | self.strategy_logic = StrategyLogicSimpleTrendFollower(self.exchangeConnection) 23 | self.strategy_logic.Load() 24 | 25 | self.debug("%s loaded" % self.name) 26 | 27 | def __del__(self): 28 | self.debug("%s unloaded" % self.name) 29 | 30 | def slot_before_unload(self, _sender, _data): 31 | self.debug("%s before_unload" % self.name) 32 | self.strategy_logic.Save() 33 | 34 | def slot_keypress(self, gox, (key)): 35 | 36 | # sell all BTC as market order 37 | if ( key == ord('s') ): 38 | self.strategy_logic.ConvertAllToUSD() 39 | 40 | # spend all USD to buy BTC as market order 41 | if ( key == ord('b') ): 42 | self.strategy_logic.ConvertAllToBTC() 43 | 44 | #dump the state of strategy_logic 45 | if ( key == ord('d') ): 46 | self.strategy_logic.Save() 47 | 48 | # immediately cancel all orders 49 | if ( key == ord('c') ): 50 | for order in gox.orderbook.owns: 51 | gox.cancel(order.oid) 52 | 53 | def slot_trade(self, gox, (date, price, volume, typ, own)): 54 | # a trade message has been received 55 | 56 | # update price indicators 57 | data = {"now": float(time.time()), "value": gox.quote2float(price) } 58 | self.strategy_logic.UpdatePrice(data) 59 | 60 | # Trigger the trading logic 61 | self.strategy_logic.Act() 62 | -------------------------------------------------------------------------------- /strategy_volume_trend_follower.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import goxapi 3 | import time 4 | import datetime 5 | from strategy_logic_volume_trend_follower import StrategyLogicVolumeTrendFollower 6 | from exchange_connection import ExchangeConnection 7 | 8 | class Strategy(goxapi.BaseObject): 9 | # pylint: disable=C0111,W0613,R0201 10 | 11 | def __init__(self, gox): 12 | goxapi.BaseObject.__init__(self) 13 | self.signal_debug.connect(gox.signal_debug) 14 | gox.signal_keypress.connect(self.slot_keypress) 15 | gox.signal_strategy_unload.connect(self.slot_before_unload) 16 | gox.signal_trade.connect(self.slot_trade) 17 | self.gox = gox 18 | self.name = "%s.%s" % \ 19 | (self.__class__.__module__, self.__class__.__name__) 20 | 21 | self.exchangeConnection = ExchangeConnection(self.gox) 22 | self.strategy_logic = StrategyLogicVolumeTrendFollower(self.exchangeConnection) 23 | self.strategy_logic.Load() 24 | 25 | self.debug("%s loaded" % self.name) 26 | 27 | def __del__(self): 28 | self.debug("%s unloaded" % self.name) 29 | 30 | def slot_before_unload(self, _sender, _data): 31 | self.debug("%s before_unload" % self.name) 32 | self.strategy_logic.Save() 33 | 34 | def slot_keypress(self, gox, (key)): 35 | 36 | # sell all BTC as market order 37 | if ( key == ord('s') ): 38 | self.strategy_logic.ConvertAllToUSD() 39 | 40 | # spend all USD to buy BTC as market order 41 | if ( key == ord('b') ): 42 | self.strategy_logic.ConvertAllToBTC() 43 | 44 | #dump the state of strategy_logic 45 | if ( key == ord('d') ): 46 | self.strategy_logic.Save() 47 | 48 | # immediately cancel all orders 49 | if ( key == ord('c') ): 50 | for order in gox.orderbook.owns: 51 | gox.cancel(order.oid) 52 | 53 | def slot_trade(self, gox, (date, price, volume, typ, own)): 54 | # a trade message has been received 55 | 56 | # update the volume indicators 57 | data = {"now": float(time.time()), "value": gox.base2float(volume) } 58 | self.strategy_logic.UpdateVolume(data) 59 | 60 | # update price indicators 61 | data = {"now": float(time.time()), "value": gox.quote2float(price) } 62 | self.strategy_logic.UpdatePrice(data) 63 | 64 | # Trigger the trading logic 65 | self.strategy_logic.Act() 66 | -------------------------------------------------------------------------------- /indicator/candlestick.py: -------------------------------------------------------------------------------- 1 | # Candle sticks 2 | import datetime 3 | import time 4 | from base import Indicator 5 | 6 | # Base class for real indicators (SimpleMovingAverage, ExponentialMovingAverage) 7 | class CandleStick(Indicator): 8 | 9 | # Opening price 10 | Open = 0.0 11 | 12 | # Closing price 13 | Close = 0.0 14 | 15 | # Highest price 16 | High = 0.0 17 | 18 | # Lowest price 19 | Low = 0.0 20 | 21 | # Open timestamp 22 | OpenTime = datetime.datetime.fromtimestamp(time.time()); 23 | 24 | # Close timestamp 25 | CloseTime = OpenTime; 26 | 27 | def __init__(self, openTime, closeTime): 28 | if (isinstance(openTime, datetime.datetime)): 29 | self.OpenTime = openTime 30 | if (isinstance(closeTime, datetime.datetime)): 31 | self.CloseTime = closeTime 32 | 33 | self._is_closed = False 34 | 35 | # Returns true if candle stick accumulated enough data to represent the 36 | # time span between Opening and Closing timestamps 37 | def IsAccurate(self): 38 | return self._is_closed 39 | 40 | def Update(self, data): 41 | 42 | if ( self.CloseTime < self.OpenTime ): 43 | self._resetPrice(0.0) 44 | self._is_closed = False 45 | return 46 | 47 | if (not self._checkData(data)): 48 | return 49 | 50 | _current_timestamp = datetime.datetime.fromtimestamp(data["now"]) 51 | _price = data["value"] 52 | 53 | if (_current_timestamp >= self.CloseTime): 54 | self._is_closed = True 55 | 56 | if (_current_timestamp <= self.CloseTime and _current_timestamp >= self.OpenTime): 57 | self._updateData(_price) 58 | 59 | def _resetPrice(self, price): 60 | self.High = price 61 | self.Low = price 62 | self.Open = price 63 | self.Close = price 64 | 65 | # Update the running timestamps of the data 66 | def _updateData(self, price): 67 | # If this is the first datapoint, initialize the values 68 | if ( self.High == 0.0 and self.Low == 0.0 and self.Open == 0.0 and self.Close == 0.0): 69 | self._resetPrice(price) 70 | self._is_closed = False 71 | return 72 | 73 | # Update the values in case the current datapoint is a current High/Low 74 | self.Close = price 75 | self.High = max(price,self.High) 76 | self.Low = min(price,self.Low) 77 | -------------------------------------------------------------------------------- /indicator/timeminmax.py: -------------------------------------------------------------------------------- 1 | from ma import MovingAverage 2 | 3 | # Indicator to represent a minimum value over a time window 4 | # For example, the minimum price of all trades during a time period 5 | class TimeMin(MovingAverage): 6 | 7 | def __init__(self, time_window): 8 | MovingAverage.__init__(self, time_window) 9 | self.Min = None 10 | 11 | def Update(self, d): 12 | 13 | if (not self._checkData(d)): 14 | return 15 | 16 | # force copy values to prevent storing a reference to the object outside self 17 | data = {"now":d["now"], "value":d["value"]} 18 | 19 | self._updateTimestamps(data) 20 | 21 | # Add the data point to the storage 22 | self._data.append(data) 23 | 24 | # Recalculate min 25 | if (self.Min == None): 26 | self.Min = d["value"] 27 | 28 | if (d["value"] < self.Min): 29 | self.Min = d["value"] 30 | 31 | if ( self._oldest_datapoint_timestamp < self._window_start_timestamp ): 32 | # Remove outdated datapoint from the storage 33 | oldest_value = self._data.pop(0) 34 | # if the oldest was the minimum, do a full linear search for min 35 | if (self.Min == oldest_value["value"]): 36 | minvalue = min(self._data, key=lambda x:x["value"]) 37 | self.Min = minvalue["value"] 38 | 39 | # Indicator to represent a maximum value over a time window 40 | # For example, the maximum price of all trades during a time period 41 | class TimeMax(MovingAverage): 42 | 43 | def __init__(self, time_window): 44 | MovingAverage.__init__(self, time_window) 45 | self.Max = None 46 | 47 | def Update(self, d): 48 | 49 | if (not self._checkData(d)): 50 | return 51 | 52 | # force copy values to prevent storing a reference to the object outside self 53 | data = {"now":d["now"], "value":d["value"]} 54 | 55 | self._updateTimestamps(data) 56 | 57 | # Add the data point to the storage 58 | self._data.append(data) 59 | 60 | # Recalculate max 61 | if (self.Max == None): 62 | self.Max = d["value"] 63 | 64 | if (d["value"] > self.Max): 65 | self.Max = d["value"] 66 | 67 | if ( self._oldest_datapoint_timestamp < self._window_start_timestamp ): 68 | # Remove outdated datapoint from the storage 69 | oldest_value = self._data.pop(0) 70 | # if the oldest was the maximum, do a full linear search for min 71 | if (self.Max == oldest_value["value"]): 72 | maxvalue = max(self._data, key=lambda x:x["value"]) 73 | self.Max = maxvalue["value"] 74 | -------------------------------------------------------------------------------- /mtgoxdata/mtgox-plot-prices-to-file.py: -------------------------------------------------------------------------------- 1 | import httplib 2 | import json 3 | import time 4 | import datetime 5 | import sqlite3 6 | import sys 7 | import calendar 8 | 9 | import matplotlib 10 | matplotlib.use("Agg") 11 | import matplotlib.pyplot 12 | import matplotlib.dates 13 | 14 | 15 | # Plot prices 16 | def plotPrices(timestamps, price, volume, filename = "prices.png"): 17 | fig = matplotlib.pyplot.figure( figsize=(10, 4), dpi=80 ) 18 | ax = fig.add_subplot(111) 19 | line1, = ax.plot(timestamps, price, color="red") 20 | # line2, = ax.plot(timestamps, volume, color="blue") 21 | ax.set_ylabel("USD") 22 | ax.set_xlabel("time") 23 | matplotlib.pyplot.xticks(rotation="25") 24 | # fig.legend([line1, line2], ["price, USD", "volume, BTC"]) 25 | fig.savefig(filename) 26 | 27 | def loadDataFromSqllite(filename, date_from, date_to): 28 | db = sqlite3.connect(filename) 29 | cursor = db.cursor() 30 | data = cursor.execute("select amount,price,date,tid from trades where ( (date>"+str(date_from)+") and (date<"+str(date_to)+") and (currency='USD') )") 31 | 32 | actual_date_from = 9999999999999 33 | actual_date_to = 0 34 | volumes = [] 35 | prices = [] 36 | dates = [] 37 | for row in data: 38 | volume = float(row[0]) 39 | price = float(row[1]) 40 | date = int(row[2]) 41 | if ( actual_date_from > date ): 42 | actual_date_from = date 43 | if ( actual_date_to < date ): 44 | actual_date_to = date 45 | if ( price > 1000 ): 46 | print int(row[3]) 47 | print price 48 | continue 49 | volumes.append( volume ) 50 | prices.append( price ) 51 | dates.append( date ) 52 | 53 | cursor.close() 54 | result = {} 55 | result["prices"]=prices 56 | result["volumes"]=volumes 57 | result["dates"]=dates 58 | result["date_from"]=actual_date_from 59 | result["date_to"]=actual_date_to 60 | return result 61 | 62 | def printData(trade_dates, mtgox_prices, mtgox_volumes): 63 | totalVolume = 0 64 | for vol in mtgox_volumes: 65 | totalVolume+=vol 66 | print "Total volume for period: " + str(totalVolume) 67 | print "Closing price: " + str(mtgox_prices[-1]) 68 | 69 | def Main(): 70 | 71 | if (len(sys.argv)<2): 72 | print "Please give me a sqlite database as a parameter" 73 | print "example: python mtgoxprint.py mtgox.sqlite3" 74 | exit() 75 | 76 | tmp = datetime.datetime.strptime("2013 Oct 19 15:00", "%Y %b %d %H:%M") 77 | # tmp = datetime.datetime.now() - datetime.timedelta(days = 1) 78 | date_from = float(calendar.timegm(tmp.utctimetuple())) 79 | tmp = datetime.datetime.strptime("2013 Oct 19 16:00", "%Y %b %d %H:%M") 80 | date_to = float(calendar.timegm(tmp.utctimetuple())) 81 | # date_to = int(time.time()) 82 | data = loadDataFromSqllite(sys.argv[1],date_from, date_to) 83 | 84 | trade_dates = data["dates"] 85 | mtgox_prices = data["prices"] 86 | mtgox_volumes = data["volumes"] 87 | timestamp_from = data["date_from"] 88 | timestamp_to = data["date_to"] 89 | 90 | printData(trade_dates, mtgox_prices, mtgox_volumes) 91 | plotPrices(trade_dates, mtgox_prices, mtgox_volumes) 92 | 93 | result = "Total records: " + str(len(mtgox_prices)) + " " 94 | result += " from: " + str(datetime.datetime.fromtimestamp(timestamp_from)) + " " 95 | result += " to: " + str(datetime.datetime.fromtimestamp(timestamp_to)) + " " 96 | print result 97 | 98 | Main() 99 | 100 | 101 | -------------------------------------------------------------------------------- /mtgoxdata/download-mtgox-data.py: -------------------------------------------------------------------------------- 1 | import httplib 2 | import json 3 | import time 4 | import threading 5 | import sqlite3 6 | import socket 7 | 8 | print_lock = threading.RLock() 9 | 10 | #currencies = ['USD','EUR','JPY','CAD','GBP','CHF','RUB','AUD','SEK','DKK','HKD','PLN','CNY','SGD','THB','NZD'] 11 | currencies = ['USD'] 12 | 13 | headers = {'User-Agent':'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.20) Gecko/20110921 Gentoo Firefox/3.6.20','Connection':'keep-alive'} 14 | 15 | class MtGoxWorker(threading.Thread): 16 | def run(self): 17 | db = sqlite3.connect('mtgox.sqlite3') 18 | db.execute('PRAGMA checkpoint_fullfsync=false') 19 | db.execute('PRAGMA fullfsync=false') 20 | db.execute('PRAGMA journal_mode=WAL') 21 | db.execute('PRAGMA synchronous=off') 22 | db.execute('PRAGMA temp_store=MEMORY') 23 | 24 | connection = httplib.HTTPSConnection('data.mtgox.com') 25 | 26 | cursor = db.cursor() 27 | cursor.execute("select max(tid) from trades where currency=?",(self.currency,)) 28 | try: 29 | max_tid = int(cursor.fetchone()[0]) 30 | except TypeError: 31 | max_tid = 0 32 | 33 | with print_lock: 34 | print "connected",self.currency 35 | 36 | while True: 37 | try: 38 | print max_tid 39 | connection.request('GET','/api/1/BTC'+self.currency+'/public/trades?since='+str(max_tid),'',headers) 40 | 41 | response = connection.getresponse() 42 | result = json.load(response) 43 | 44 | if result['result'] == 'success': 45 | if len(result['return']) == 0: 46 | time.sleep(120) 47 | else: 48 | cursor = db.cursor() 49 | 50 | for trade in result['return']: 51 | cursor.execute(""" 52 | INSERT OR IGNORE INTO trades(tid,currency,amount,price,date,real) 53 | VALUES (?,?,?,?,?,?) 54 | """,(trade['tid'],trade['price_currency'],trade['amount'],trade['price'],trade['date'],trade['primary']=='Y')) 55 | 56 | cursor.execute("select max(tid) from trades where currency=?",(self.currency,)) 57 | 58 | max_tid = int(cursor.fetchone()[0]) 59 | 60 | db.commit() 61 | cursor.close() 62 | except httplib.HTTPException: 63 | connection = httplib.HTTPSConnection('data.mtgox.com') 64 | except socket.error: 65 | connection = httplib.HTTPSConnection('data.mtgox.com') 66 | except ValueError: 67 | connection = httplib.HTTPSConnection('data.mtgox.com') 68 | 69 | db = sqlite3.connect('mtgox.sqlite3') 70 | db.execute('PRAGMA checkpoint_fullfsync=false') 71 | db.execute('PRAGMA fullfsync=false') 72 | db.execute('PRAGMA journal_mode=WAL') 73 | db.execute('PRAGMA synchronous=off') 74 | db.execute('PRAGMA temp_store=MEMORY') 75 | 76 | db.execute('CREATE TABLE IF NOT EXISTS trades(tid integer,currency text,amount real,price real,date integer,real boolean);') 77 | db.execute('CREATE UNIQUE INDEX IF NOT EXISTS trades_currency_tid_index on trades(currency,tid);') 78 | 79 | db.close() 80 | 81 | workers = [] 82 | 83 | for currency in currencies: 84 | w = MtGoxWorker() 85 | w.currency = currency 86 | w.daemon = True 87 | w.start() 88 | 89 | workers.append(w) 90 | 91 | time.sleep(5000) 92 | 93 | while True: 94 | time.sleep(2) 95 | -------------------------------------------------------------------------------- /strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | trading robot breadboard 3 | """ 4 | 5 | import goxapi 6 | 7 | class Strategy(goxapi.BaseObject): 8 | # pylint: disable=C0111,W0613,R0201 9 | 10 | def __init__(self, gox): 11 | goxapi.BaseObject.__init__(self) 12 | self.signal_debug.connect(gox.signal_debug) 13 | gox.signal_keypress.connect(self.slot_keypress) 14 | gox.signal_strategy_unload.connect(self.slot_before_unload) 15 | gox.signal_ticker.connect(self.slot_tick) 16 | gox.signal_depth.connect(self.slot_depth) 17 | gox.signal_trade.connect(self.slot_trade) 18 | gox.signal_userorder.connect(self.slot_userorder) 19 | gox.orderbook.signal_owns_changed.connect(self.slot_owns_changed) 20 | gox.history.signal_changed.connect(self.slot_history_changed) 21 | gox.signal_wallet.connect(self.slot_wallet_changed) 22 | self.gox = gox 23 | self.name = "%s.%s" % \ 24 | (self.__class__.__module__, self.__class__.__name__) 25 | self.debug("%s loaded" % self.name) 26 | 27 | def __del__(self): 28 | """the strategy object will be garbage collected now, this mainly 29 | only exists to produce the log message, so you can make sure it 30 | really garbage collects and won't stay in memory on reload. If you 31 | don't see this log mesage on reload then you have circular references""" 32 | self.debug("%s unloaded" % self.name) 33 | 34 | def slot_before_unload(self, _sender, _data): 35 | """the strategy is about to be unloaded. Use this signal to persist 36 | any state and also use it to forcefully destroy any circular references 37 | to allow it to be properly garbage collected (you might need to do 38 | this if you instantiated linked lists or similar structures, the 39 | symptom would be that you don't see the 'unloaded' message above.""" 40 | pass 41 | 42 | def slot_keypress(self, gox, (key)): 43 | """a key in has been pressed (only a..z without "q" and "l") 44 | The argument key contains the ascii code. To react to a certain 45 | key use something like if key == ord('a') 46 | """ 47 | pass 48 | 49 | def slot_tick(self, gox, (bid, ask)): 50 | """a tick message has been received from the streaming API""" 51 | pass 52 | 53 | def slot_depth(self, gox, (typ, price, volume, total_volume)): 54 | """a depth message has been received. Use this only if you want to 55 | keep track of the depth and orderbook updates yourself or if you 56 | for example want to log all depth messages to a database. This 57 | signal comes directly from the streaming API and the gox.orderbook 58 | might not yet be updated at this time.""" 59 | pass 60 | 61 | def slot_trade(self, gox, (date, price, volume, typ, own)): 62 | """a trade message has been received. Note that this signal comes 63 | directly from the streaming API, it might come before orderbook.owns 64 | list has been updated, don't rely on the own orders and wallet already 65 | having been updated when this is fired.""" 66 | pass 67 | 68 | def slot_userorder(self, gox, (price, volume, typ, oid, status)): 69 | """this comes directly from the API and owns list might not yet be 70 | updated, if you need the new owns list then use slot_owns_changed""" 71 | pass 72 | 73 | def slot_owns_changed(self, orderbook, _dummy): 74 | """this comes *after* userorder and orderbook.owns is updated already. 75 | Also note that this signal is sent by the orderbook object, not by gox, 76 | so the sender argument is orderbook and not gox. This signal might be 77 | useful if you want to detect whether an order has been filled, you 78 | count open orders, count pending orders and compare with last count""" 79 | pass 80 | 81 | def slot_wallet_changed(self, gox, _dummy): 82 | """this comes after the wallet has been updated. Access the new balances 83 | like so: gox.wallet[gox.curr_base] or gox.wallet[gox.curr_quote] and use 84 | gox.base2float() or gox.quote2float() if you need float values. You can 85 | also access balances from other currenies like gox.wallet["JPY"] but it 86 | is not guaranteed that they exist if you never had a balance in that 87 | particular currency. Always test for their existence first. Note that 88 | there will be multiple wallet signals after every trade. You can look 89 | into gox.msg to inspect the original server message that triggered this 90 | signal to filter the flood a little bit.""" 91 | pass 92 | 93 | def slot_history_changed(self, history, _dummy): 94 | """this is fired whenever a new trade is inserted into the history, 95 | you can also use this to query the close price of the most recent 96 | candle which is effectvely the price of the last trade message. 97 | Contrary to the slot_trade this also fires when streaming API 98 | reconnects and re-downloads the trade history, you can use this 99 | to implement a stoploss or you could also use it for example to detect 100 | when a new candle is opened""" 101 | pass 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #GoxToolBots 2 | 3 | A collection of trading bots implementing simple strategies for goxtool 4 | 5 | Goxtool is a trading client for the MtGox Bitcon currency exchange. 6 | The user manual is here: 7 | [http://prof7bit.github.com/goxtool/](http://prof7bit.github.com/goxtool/) 8 | 9 | 10 | Simple trend following bot: 11 | --------------------------- 12 | strategy_simple_trend_follower.py 13 | 14 | This bot records MtGox trades and estimates the market trend. 15 | When upward trend detected, the bot buys. When downward trend detected, 16 | bot sells. 17 | 18 | Market trend estimation is done by using three moving price averages. 19 | 20 | Features: 21 | * Uses three moving average to estimate the trend of the market 22 | * Trades following the market trend 23 | * Download historic data for MtGox trades 24 | * Backtest your strategy on historic date 25 | * Plot the behavior of your bot 26 | * Save/Restore bot state 27 | 28 | Usage: 29 | ./goxtool.py --strategy strategy_simple_trend_follower.py 30 | 31 | 32 | Volume gated trend following bot: 33 | --------------------------------- 34 | strategy_volume_trend_follower.py 35 | 36 | Very similar to trend follower, but the trades are gated by the shift in 37 | volume. The idea is that significant price shift is always a result of 38 | a significant change in trade volumes. So unless an increased trade volume 39 | is detected, the trend follower is disabled. 40 | 41 | Market trend estimation is done by using three moving price averages. 42 | 43 | Usage: 44 | ./goxtool.py --strategy strategy\_volume\_trend\_follower.py 45 | 46 | 47 | Trailing stop loss bot: 48 | ------------------------------- 49 | strategy_trailing_stoploss.py 50 | 51 | This is an implementation of a trailing stop loss trading bot 52 | for MtGox bitcoin exchange. 53 | 54 | This bot tracks MtGox trades and records the maximum price of BTC in the last three hours. 55 | If the current price drops below 15% of the maximum, the bot sells all BTC assets 56 | as Market Order and stops. It also send a notification email to the owner. 57 | 58 | Features: 59 | * Sell all BTC for USD in case the BTC price dips 15% below maximum value of 3 hours 60 | * Send an email notification when sell BTC 61 | * Download historic data for MtGox trades 62 | * Backtest your strategy on historic date 63 | * Plot the behavior of your bot 64 | * Save/Restore bot state 65 | 66 | Usage: 67 | ./goxtool.py --strategy strategy\_trailing\_stoploss.py 68 | 69 | 70 | Key commands: 71 | ------------ 72 | Each bot supports these key commands: 73 | * "S" - Sell all BTC 74 | * "B" - Buy BTC for all the USD you have 75 | * "C" - Cancell all outstanding orders 76 | * "D" - Dump bot state (for offline plotting) 77 | 78 | Backtesting: 79 | ------------ 80 | python download-mtgox-data.py 81 | in the mtgoxdata folder downloads all the data from mtgox trades 82 | into a sqlite database. Please be careful and do not overwhelm 83 | the exchange with requests. I will try to put up a website to 84 | share this data for direct download. Meanwhile you can ping me, 85 | I can send it to you. 86 | 87 | If you run any of the strategy\_logic\_*.py 88 | python scripts as a standalone porgram, it will default 89 | to backtesting its behavior on downloaded historic data. 90 | It will use matplotlib to show the prices and points of 91 | sells/buys. This is useful for developing your own bots. 92 | 93 | Examples: 94 | python strategy\_logic\_simple\_trend\_follower.py 95 | python strategy\_logic\_trailing\_stoploss.py 96 | 97 | Please not that in the main() function of each strategy logic scripts you can tweak the 98 | simluation start and end dates. 99 | 100 | Bots states: 101 | ---------- 102 | Bot automatically dumps its state into a file when the strategy is unloaded. 103 | You can also manually produce a dump by pressing "D". You can plot the state 104 | of the dump with plot\_pickled.py. 105 | The filename of the dump is strategy\_logic\_*.pickle 106 | 107 | Email: 108 | ------ 109 | Trailing stop loss bot can send you an email notification when it sells. 110 | Adjust your email credentials in goxtool.ini to enable email notifications. 111 | You will need the following: 112 | 113 | [email] 114 | email_to = mtgoxtrader@example.com 115 | email_from = tradingbot@example.com 116 | email_server = smtp.example.com 117 | email_server_port = 25 118 | email_server_password = TradingBotEmailPassword 119 | 120 | TA-Lib: 121 | ------- 122 | Here's how you can plug in TA-Lib if you want more technical analisys indicators 123 | 124 | 1.1 Get build essentials 125 | sudo apt-get install build-essential 126 | sudo apt-get install python2.7-dev 127 | sudo apt-get install python-pip 128 | 129 | 1.2 Install TA-Lib 130 | http://www.zwets.com/ta-lib/ 131 | sudo gedit /etc/apt/sources.list 132 | add: 133 | deb http://www.zwets.com/debs unstable/ 134 | deb-src http://www.zwets.com/debs unstable/ 135 | sudo apt-get update 136 | sudo apt-get install ta-lib-dev 137 | 138 | 1.2.1 If the above does not work (Goobuntu prohibits 3rd party packages) 139 | Download http://sourceforge.net/projects/ta-lib/files/ta-lib/0.4.0/ta-lib-0.4.0-src.tar.gz/download?use_mirror=iweb 140 | cd ~/Downloads 141 | tar zxfv ta-lib-0.4.0-src.tar.gz 142 | cd ta-lib 143 | ./configure --prefix=/usr 144 | make 145 | sudo make install 146 | 147 | 1.3 Install python wrapper 148 | sudo pip install Cython 149 | sudo pip install numpy 150 | sudo pip install TA-Lib 151 | 152 | 153 | Support: 154 | -------- 155 | Please consider donating to: 16csNHCBstmdcLnPg45fxF2PdKoPyPJDhX 156 | 157 | -------------------------------------------------------------------------------- /indicator/ma.py: -------------------------------------------------------------------------------- 1 | # Moving averages indicators 2 | import datetime 3 | import time 4 | from base import Indicator 5 | 6 | # Base class for real indicators (SimpleMovingAverage, ExponentialMovingAverage) 7 | class MovingAverage(Indicator): 8 | 9 | # The current value of moving average 10 | Value = 0.0 11 | 12 | # The window used to calculate the moving average 13 | TimeWindow = datetime.timedelta(hours=1) 14 | 15 | def __init__(self, time_window): 16 | if (isinstance(time_window, datetime.timedelta)): 17 | self.TimeWindow = abs(time_window) 18 | self._data = [] 19 | self._current_timestamp = datetime.datetime.fromtimestamp(time.time()) 20 | self._window_start_timestamp = datetime.datetime.fromtimestamp(time.time()) 21 | self._oldest_datapoint_timestamp = datetime.datetime.fromtimestamp(time.time()) 22 | self._isAccurate = False 23 | 24 | # TimeWindow of the actual data that indicator has received so far 25 | def ActualDataTimeWindow(self): 26 | if (len(self._data)<=1): 27 | return datetime.timedelta(0) 28 | oldest = datetime.datetime.fromtimestamp(self._data[0]["now"]) 29 | newest = datetime.datetime.fromtimestamp(self._data[len(self._data)-1]["now"]) 30 | result = newest - oldest 31 | return result 32 | 33 | # Returns true if the indicator has enough data to satisfy requested time window 34 | def IsAccurate(self): 35 | return self._isAccurate 36 | # return self.TimeWindow > self.ActualDataTimeWindow() 37 | 38 | # Update the running timestamps of the data 39 | def _updateTimestamps(self, data): 40 | self._current_timestamp = datetime.datetime.fromtimestamp(data["now"]) 41 | self._window_start_timestamp = self._current_timestamp - self.TimeWindow 42 | if (len(self._data)==0): 43 | self._oldest_datapoint_timestamp = self._current_timestamp 44 | else: 45 | self._oldest_datapoint_timestamp = datetime.datetime.fromtimestamp(self._data[0]["now"]) 46 | if ( self.TimeWindow < self.ActualDataTimeWindow() ): 47 | self._isAccurate = True 48 | 49 | # Moving average of a price over a period of time 50 | class SimpleMovingAverage(MovingAverage): 51 | 52 | def Update(self, d): 53 | 54 | if (not self._checkData(d)): 55 | return 56 | 57 | # force copy values to prevent storing a reference to the object outside self 58 | data = {"now":d["now"], "value":d["value"]} 59 | 60 | self._updateTimestamps(data) 61 | 62 | if ( self._oldest_datapoint_timestamp < self._window_start_timestamp ): 63 | # Update current moving average, avoiding the loop over all datapoints 64 | self.Value = self.Value + ( data["value"] - self._data[0]["value"] ) / len(self._data) 65 | # Remove outdated datapoint from the storage 66 | self._data.pop(0) 67 | else: 68 | # Not enough data accumulated. Compute cumulative moving average 69 | self.Value = self.Value + (data["value"] - self.Value) / ( len(self._data) + 1.0 ) 70 | 71 | # Add the data point to the storage 72 | self._data.append(data) 73 | 74 | # Moving average with exponential smoothing of a price over a period of time 75 | class ExponentialMovingAverage(MovingAverage): 76 | 77 | def Update(self, d): 78 | 79 | if (not self._checkData(d)): 80 | return 81 | 82 | # force copy values to prevent storing a reference to the object outside self 83 | data = {"now":d["now"], "value":d["value"]} 84 | 85 | self._updateTimestamps(data) 86 | 87 | smoothing = 2.0 / (len(self._data) + 1.0) 88 | 89 | if ( self._oldest_datapoint_timestamp < self._window_start_timestamp ): 90 | # Update current exponential moving average, avoiding the loop over all datapoints 91 | self.Value = self.Value + smoothing * ( data["value"] - self.Value ) 92 | # Remove outdated datapoint from the storage 93 | self._data.pop(0) 94 | else: 95 | # Not enough data accumulated. Compute cumulative moving average 96 | self.Value = self.Value + (data["value"] - self.Value) / ( len(self._data) + 1.0 ) 97 | 98 | # Add the data point to the storage 99 | self._data.append(data) 100 | 101 | # Moving average with exponential smoothing of a volume over a period of time 102 | # If two values arrive on the same timestamp, they are added together 103 | # Imagine to execute an order there was several trades, then the total volume 104 | # of the order would be split up into several volumes. 105 | class SimpleCummulativeMovingAverage(MovingAverage): 106 | 107 | def Update(self, d): 108 | 109 | if (not self._checkData(d)): 110 | return 111 | 112 | # force copy values to prevent storing a reference to the object outside self 113 | data = {"now":d["now"], "value":d["value"]} 114 | 115 | # if there is more values for the same point in time, add them up 116 | if (datetime.datetime.fromtimestamp(data["now"]) == self._current_timestamp): 117 | data["value"] += self._data.pop()["value"] 118 | self.Value = self._lastValue 119 | 120 | self._lastValue = self.Value 121 | 122 | self._updateTimestamps(data) 123 | 124 | if ( self._oldest_datapoint_timestamp < self._window_start_timestamp ): 125 | # Update current moving average, avoiding the loop over all datapoints 126 | self.Value = self.Value + ( data["value"] - self._data[0]["value"] ) / len(self._data) 127 | # Remove outdated datapoint from the storage 128 | self._data.pop(0) 129 | else: 130 | # Not enough data accumulated. Compute cumulative moving average 131 | self.Value = self.Value + (data["value"] - self.Value) / ( len(self._data) + 1.0 ) 132 | 133 | # Add the data point to the storage 134 | self._data.append(data) 135 | 136 | # Moving average with exponential smoothing of a volume over a period of time 137 | # If two values arrive on the same timestamp, they are added together 138 | # Imagine to execute an order there was several trades, then the total volume 139 | # of the order would be split up into several volumes. 140 | class ExponentialCummulativeMovingAverage(MovingAverage): 141 | 142 | def Update(self, d): 143 | 144 | if (not self._checkData(d)): 145 | return 146 | 147 | # force copy values to prevent storing a reference to the object outside self 148 | data = {"now":d["now"], "value":d["value"]} 149 | 150 | # if there is more values for the same point in time, add them up 151 | if (datetime.datetime.fromtimestamp(data["now"]) == self._current_timestamp): 152 | data["value"] += self._data.pop()["value"] 153 | self.Value = self._lastValue 154 | 155 | self._lastValue = self.Value 156 | 157 | self._updateTimestamps(data) 158 | 159 | smoothing = 2.0 / (len(self._data) + 1.0) 160 | 161 | if ( self._oldest_datapoint_timestamp < self._window_start_timestamp ): 162 | # Update current exponential moving average, avoiding the loop over all datapoints 163 | self.Value = self.Value + smoothing * ( data["value"] - self.Value ) 164 | # Remove outdated datapoint from the storage 165 | self._data.pop(0) 166 | else: 167 | # Not enough data accumulated. Compute cumulative moving average 168 | self.Value = self.Value + (data["value"] - self.Value) / ( len(self._data) + 1.0 ) 169 | 170 | # Add the data point to the storage 171 | self._data.append(data) 172 | -------------------------------------------------------------------------------- /strategy_logic_trailing_stoploss.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | import pickle 4 | import sqlite3 5 | import calendar 6 | import math 7 | import smtplib 8 | from email.mime.text import MIMEText 9 | from indicator.ma import ExponentialMovingAverage as ema 10 | from indicator.timeminmax import TimeMax, TimeMin 11 | from exchange_connection import ExchangeConnection, MockExchangeConnection 12 | 13 | """ 14 | Trailing stop loss bot. 15 | When bot detects that the price has droped over 15% 16 | from the maximum price recorded in the last three hours, 17 | it sells all funds and sends an email notification. 18 | """ 19 | 20 | class StrategyLogicTrailingStoploss: 21 | def __init__(self, xcon, filename = "strategy_logic_trailing_stoploss.pickle", debug = False): 22 | 23 | self.filename = filename 24 | self.xcon = xcon 25 | 26 | # Constants 27 | self.Price_Fast_EMA_Time = 1 # 60 seconds, just to smooth out the price 28 | self.Price_Max_Time = 90 # 3 hours 29 | self.Price_Drop_Percent = 15.0 # percent 30 | self.Current_Price = 0.0 31 | self.Enabled = True 32 | 33 | # Fast price moving average 34 | timedelta = datetime.timedelta(minutes = self.Price_Fast_EMA_Time) 35 | self.price_ema_fast = ema(timedelta) 36 | 37 | # Price maximum values over an hour for sell signal 38 | timedelta = datetime.timedelta(minutes = self.Price_Max_Time) 39 | self.price_trailing_max = TimeMax(timedelta) 40 | 41 | self._debugData = {} 42 | 43 | # If we are in debug mode, do not restore previously saved state from file 44 | if (not debug): 45 | self.Load() 46 | 47 | # Make sure we use the currently passed ExchangeConnection, not the restored one 48 | self.xcon = xcon 49 | self.debug = debug 50 | 51 | def __del__(self): 52 | self.Save() 53 | 54 | def UpdatePrice(self, data): 55 | self.price_ema_fast.Update(data) 56 | self.Current_Price = data["value"] 57 | 58 | tmp = {} 59 | tmp["now"] = data["now"] 60 | tmp["value"] = self.price_ema_fast.Value 61 | self.price_trailing_max.Update(tmp) 62 | 63 | self._updatePriceDebugHook(data) 64 | 65 | def IsBigPriceDrop(self): 66 | 67 | if (not self.price_trailing_max.IsAccurate()): 68 | return False 69 | 70 | if ( self.price_trailing_max.Max - self.price_ema_fast.Value > self.price_trailing_max.Max * (self.Price_Drop_Percent/100) ): 71 | timedelta = datetime.timedelta(minutes = self.Price_Max_Time) 72 | self.price_trailing_max = TimeMax(timedelta) 73 | return True 74 | 75 | return False 76 | 77 | def Act(self): 78 | 79 | if ( self.Enabled == False ): 80 | return 81 | 82 | # If we do not have enough data, and are just starting, take no action 83 | if (not self.price_ema_fast.IsAccurate()): 84 | return 85 | 86 | # Currently invested in BTC 87 | if (self.xcon.AvailableBTC() * self.Current_Price > self.xcon.AvailableUSD()): 88 | 89 | if ( self.IsBigPriceDrop() ): 90 | self.ConvertAllToUSD() 91 | if (not self.debug): 92 | self.Enabled = False 93 | self.SendEmail() 94 | 95 | # For testing purposes, convert everything back immediately to give chance to 96 | # Stop loss to sell again. Uncomment the following for backtesting. 97 | # It will result it continuous buys and sells, but you can catch see 98 | # All of the curves where the bot would sell 99 | # if (self.xcon.AvailableUSD() > self.xcon.AvailableBTC() * self.Current_Price): 100 | # 101 | # if (self.debug): 102 | # self.ConvertAllToBTC() 103 | 104 | def Load(self): 105 | try: 106 | f = open(self.filename,'rb') 107 | tmp_dict = pickle.load(f) 108 | self.__dict__.update(tmp_dict) 109 | f.close() 110 | except: 111 | print "StrategyCore: Failed to load previous state, starting from scratch" 112 | 113 | def Save(self): 114 | try: 115 | f = open(self.filename,'wb') 116 | odict = self.__dict__.copy() 117 | del odict['xcon'] #don't pickle this 118 | pickle.dump(odict,f,2) 119 | f.close() 120 | except: 121 | print "StrategyCore: Failed to save my state, all the data will be lost" 122 | 123 | def SendEmail(self): 124 | 125 | try: 126 | email_to = self.xcon.gox.config.get_string("email", "email_to") 127 | email_from = self.xcon.gox.config.get_string("email", "email_from") 128 | email_server = self.xcon.gox.config.get_string("email", "email_server") 129 | email_server_port = self.xcon.gox.config.get_string("email", "email_server_port") 130 | email_server_password = self.xcon.gox.config.get_string("email", "email_server_password") 131 | except: 132 | print "Trailing Stop loss bot failed to send a notification email. Check email settings in goxtool.ini" 133 | print "These need to be filled out:" 134 | print "[email]" 135 | print "email_to=mtgoxtrader@example.com" 136 | print "email_from=tradingbot@example.com" 137 | print "email_server=smtp.example.com" 138 | print "email_server_port=25" 139 | print "email_server_password=TradingBotEmailPassword" 140 | 141 | msg = "Hello!\n\n" 142 | msg += "This is a notification from your mtgox trailing stop loss bot.\n" 143 | msg += "I have just sold "+ str(self.xcon.AvailableBTC()) + " BTC " + "at a price of " + str(self.Current_Price) + " USD\n" 144 | msg += "Date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 145 | msg += "\n\n" 146 | msg += "If this bot was helpful to you, please consider donating to 16csNHCBstmdcLnPg45fxF2PdKoPyPJDhX\n\n" 147 | msg += "Thank you!" 148 | 149 | email = MIMEText(msg) 150 | email['Subject'] = "MtGox trailing stop loss bot sold BTC" 151 | email['From'] = email_from 152 | email['To'] = email_to 153 | s = smtplib.SMTP(email_server,email_server_port) 154 | s.login(email_from, email_server_password) 155 | s.sendmail(email_from, email_to, email.as_string()) 156 | s.quit() 157 | 158 | def ConvertAllToUSD(self): 159 | self._preSellBTCDebugHook() 160 | self.Last_Sell_Price = self.Current_Price 161 | btc_to_sell = self.xcon.AvailableBTC() 162 | self.xcon.SellBTC(btc_to_sell) 163 | self._postSellBTCDebugHook() 164 | 165 | def ConvertAllToBTC(self): 166 | self._preBuyBTCDebugHook() 167 | self.Last_Buy_Price = self.Current_Price 168 | affordable_amount_of_btc = self.xcon.AvailableUSD() / self.Current_Price 169 | self.xcon.BuyBTC(affordable_amount_of_btc) 170 | self._postBuyBTCDebugHook() 171 | 172 | def _updatePriceDebugHook(self, data): 173 | 174 | if ("RawPrice" not in self._debugData): 175 | self._debugData["RawPrice"] = [] 176 | 177 | if ("PriceEmaFast" not in self._debugData): 178 | self._debugData["PriceEmaFast"] = [] 179 | 180 | self._debugData["RawPrice"].append(data) 181 | 182 | tmp = {} 183 | tmp["now"] = data["now"] 184 | tmp["value"] = self.price_ema_fast.Value 185 | self._debugData["PriceEmaFast"].append(tmp) 186 | del tmp 187 | 188 | self._debugData["LastPriceUpdateTime"] = data["now"] 189 | 190 | def _preSellBTCDebugHook(self): 191 | 192 | msg = "Selling " + str(self.xcon.AvailableBTC()) + " BTC" 193 | msg += " current price: " + str(self.Current_Price) 194 | print msg 195 | 196 | def _postSellBTCDebugHook(self): 197 | 198 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 199 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 200 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 201 | print msg 202 | 203 | if ("Trades" not in self._debugData): 204 | self._debugData["Trades"] = [] 205 | 206 | tmp = {} 207 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 208 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 209 | self._debugData["Trades"].append(tmp) 210 | 211 | if ("Sell" not in self._debugData): 212 | self._debugData["Sell"] = [] 213 | 214 | tmp = {} 215 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 216 | tmp["value"] = self.Last_Sell_Price 217 | self._debugData["Sell"].append(tmp) 218 | 219 | def _preBuyBTCDebugHook(self): 220 | 221 | msg = "Buying " + str(self.xcon.AvailableUSD() / self.Current_Price) + " BTC" 222 | msg += " current price: " + str(self.Current_Price) 223 | print msg 224 | 225 | def _postBuyBTCDebugHook(self): 226 | 227 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 228 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 229 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 230 | print msg 231 | 232 | if ("Trades" not in self._debugData): 233 | self._debugData["Trades"] = [] 234 | 235 | tmp = {} 236 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 237 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 238 | self._debugData["Trades"].append(tmp) 239 | 240 | if ("Buy" not in self._debugData): 241 | self._debugData["Buy"] = [] 242 | 243 | tmp = {} 244 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 245 | tmp["value"] = self.Last_Buy_Price 246 | self._debugData["Buy"].append(tmp) 247 | 248 | def feedRecordedData(score, sqliteDataFile, date_from, date_to): 249 | 250 | db = sqlite3.connect(sqliteDataFile) 251 | cursor = db.cursor() 252 | data = cursor.execute("select amount,price,date from trades where ( (date>"+str(date_from)+") and (date<"+str(date_to)+") and (currency='USD') )") 253 | actual_date_from = 9999999999999 254 | actual_date_to = 0 255 | 256 | price_volume_data = [] 257 | 258 | for row in data: 259 | volume = float(row[0]) 260 | price = float(row[1]) 261 | date = float(row[2]) 262 | 263 | if ( actual_date_from > date ): 264 | actual_date_from = date 265 | if ( actual_date_to < date ): 266 | actual_date_to = date 267 | 268 | update_data = {} 269 | update_data["now"] = date 270 | update_data["price"] = price 271 | update_data["volume"] = volume 272 | price_volume_data.append(update_data) 273 | 274 | from operator import itemgetter 275 | price_volume_data = sorted(price_volume_data, key=itemgetter("now")) 276 | 277 | for item in price_volume_data: 278 | score.xcon.SetBTCPrice(item["price"]) 279 | tmp = {"now":item["now"], "value": item["price"]} 280 | score.UpdatePrice(tmp) 281 | score.Act() 282 | 283 | cursor.close() 284 | 285 | return (actual_date_from, actual_date_to) 286 | 287 | def plotStrategyCorePerformance(debugData): 288 | import strategy_plot 289 | 290 | subplots = 1 291 | splot = strategy_plot.StrategyPlot(debugData, subplots) 292 | splot.Plot("RawPrice",1, "y-") 293 | splot.Plot("Sell", 1, "go") 294 | splot.Plot("Buy", 1, "y^") 295 | splot.Plot("PriceEmaFast", 1, "b-") 296 | 297 | splot.Show() 298 | 299 | def main(): 300 | # Test this strategy core by mocking ExchangeConnection 301 | # And by feeding it the prerecorded data 302 | xcon = MockExchangeConnection() 303 | score = StrategyLogicTrailingStoploss(xcon, debug = True) 304 | 305 | tmp = datetime.datetime.strptime("2013 Oct 1 22:00", "%Y %b %d %H:%M") 306 | date_from = float(calendar.timegm(tmp.utctimetuple())) 307 | tmp = datetime.datetime.strptime("2013 Oct 3 12:00", "%Y %b %d %H:%M") 308 | date_to = float(calendar.timegm(tmp.utctimetuple())) 309 | 310 | (actual_date_from, actual_date_to) = feedRecordedData(score, "mtgoxdata/mtgox.sqlite3", date_from, date_to) 311 | print "Simulation from: " + str(datetime.datetime.fromtimestamp(date_from)) + " to " + str(datetime.datetime.fromtimestamp(date_to)) 312 | print "Total funds. BTC: " + str(xcon.AvailableBTC()) + " USD: " + str(xcon.AvailableUSD()) + " current price: " + str(xcon.currentPrice) + " Convert to USD:" + str(xcon.AvailableBTC() * score.Current_Price + xcon.AvailableUSD()) 313 | plotStrategyCorePerformance(score._debugData) 314 | 315 | if __name__ == "__main__": 316 | main() -------------------------------------------------------------------------------- /strategy_logic_simple_trend_follower.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | import pickle 4 | import sqlite3 5 | import calendar 6 | import math 7 | from indicator.ma import ExponentialMovingAverage as ema 8 | from indicator.ma import SimpleMovingAverage as ema 9 | from exchange_connection import ExchangeConnection, MockExchangeConnection 10 | 11 | """ 12 | Simple trend following bot. It relies on three moving averages. 13 | It buys when the market is trending up, sells when the market trends down. 14 | The market is believed to be bullish when the fastest moving average 15 | is above the medium moving average, and the medium moving average is above 16 | the slow moving average. Same true in reverse for bullish market. 17 | """ 18 | 19 | class StrategyLogicSimpleTrendFollower: 20 | def __init__(self, xcon, filename = "strategy_logic_simple_trend_follower.pickle", debug = False): 21 | 22 | self.filename = filename 23 | self.xcon = xcon 24 | 25 | # Constants 26 | self.Price_Fast_EMA_Time = 7 * 60 # minutes 27 | self.Price_Slow_EMA_Time = 14 * 60 # minutes 28 | self.Price_LongTerm_EMA_Time = 21 * 60 # minutes 29 | 30 | self.Last_Buy_Price = 0.0 31 | self.Last_Sell_Price = 0.0 32 | self.Current_Price = 0.0 33 | 34 | self.MinimumSpread = 0.0012 35 | 36 | # Price indicators 37 | 38 | # Slow moving price average 39 | timedelta = datetime.timedelta(minutes = self.Price_Slow_EMA_Time) 40 | self.price_ema_slow = ema(timedelta) 41 | 42 | # Fast five minutes moving averages 43 | timedelta = datetime.timedelta(minutes = self.Price_Fast_EMA_Time) 44 | self.price_ema_fast = ema(timedelta) 45 | 46 | # Long Term moving average 47 | timedelta = datetime.timedelta(minutes = self.Price_LongTerm_EMA_Time) 48 | self.price_ema_longterm = ema(timedelta) 49 | 50 | self._debugData = {} 51 | 52 | # Restore state from disk if possible 53 | if (not debug): 54 | self.Load() 55 | 56 | # Make sure we use the currently passed ExchangeConnection, not the restored one 57 | self.xcon = xcon 58 | self.debug = debug 59 | 60 | def UpdatePrice(self, data): 61 | 62 | self.price_ema_slow.Update(data) 63 | self.price_ema_fast.Update(data) 64 | self.price_ema_longterm.Update(data) 65 | self.Current_Price = data["value"] 66 | self._updatePriceDebugHook(data) 67 | 68 | def IsDownTrend(self): 69 | 70 | if ( self.price_ema_fast.Value > self.price_ema_slow.Value ): 71 | return False 72 | 73 | if ( self.price_ema_fast.Value > self.price_ema_longterm.Value): 74 | return False 75 | 76 | if (self.price_ema_slow.Value > self.price_ema_longterm.Value): 77 | return False 78 | 79 | return True 80 | 81 | def IsUpTrend(self): 82 | 83 | ma_diff_min = self.price_ema_fast.Value * self.MinimumSpread 84 | 85 | ma_diff = self.price_ema_fast.Value - self.price_ema_slow.Value 86 | if ( ma_diff < ma_diff_min ): 87 | return False 88 | 89 | ma_diff = self.price_ema_fast.Value - self.price_ema_longterm.Value 90 | if ( ma_diff < ma_diff_min ): 91 | return False 92 | 93 | ma_diff = self.price_ema_slow.Value - self.price_ema_longterm.Value 94 | if ( ma_diff < ma_diff_min ): 95 | return False 96 | 97 | return True 98 | 99 | def Act(self): 100 | 101 | # If we do not have enough data, and are just starting, take no action 102 | if (not self.price_ema_slow.IsAccurate()): 103 | return 104 | 105 | if (not self.price_ema_longterm.IsAccurate()): 106 | return 107 | 108 | # Debug breakpoint for particular datapoints in time 109 | if (self._debugIsNowPassedTimeStamp("2013 Oct 2 02:00", "%Y %b %d %H:%M")): 110 | set_breakpoint_here = True 111 | 112 | # Currently invested in BTC 113 | if (self.xcon.AvailableBTC() * self.Current_Price > self.xcon.AvailableUSD()): 114 | 115 | if (not self.IsDownTrend()): 116 | return 117 | 118 | self.ConvertAllToUSD() 119 | return 120 | 121 | # Currently invested in USD 122 | if (self.xcon.AvailableBTC() * self.Current_Price < self.xcon.AvailableUSD()): 123 | 124 | if (not self.IsUpTrend()): 125 | return 126 | 127 | self.ConvertAllToBTC() 128 | 129 | def Load(self): 130 | try: 131 | f = open(self.filename,'rb') 132 | tmp_dict = pickle.load(f) 133 | self.__dict__.update(tmp_dict) 134 | print "LoadSuccess!" 135 | f.close() 136 | except: 137 | print "StrategyLogicSimpleTrendFollower: Failed to load previous state, starting from scratch" 138 | 139 | def Save(self): 140 | try: 141 | f = open(self.filename,'wb') 142 | odict = self.__dict__.copy() 143 | del odict['xcon'] #don't pickle this 144 | pickle.dump(odict,f,2) 145 | f.close() 146 | except: 147 | print "StrategyLogicSimpleTrendFollower: Failed to save my state, all the data will be lost" 148 | 149 | def ConvertAllToUSD(self): 150 | self._preSellBTCDebugHook() 151 | self.Last_Sell_Price = self.Current_Price 152 | btc_to_sell = self.xcon.AvailableBTC() 153 | self.xcon.SellBTC(btc_to_sell) 154 | self._postSellBTCDebugHook() 155 | 156 | def ConvertAllToBTC(self): 157 | self._preBuyBTCDebugHook() 158 | self.Last_Buy_Price = self.Current_Price 159 | affordable_amount_of_btc = self.xcon.AvailableUSD() / self.Current_Price 160 | self.xcon.BuyBTC(affordable_amount_of_btc) 161 | self._postBuyBTCDebugHook() 162 | 163 | def _debugIsNowPassedTimeStamp(self, timestring, timeformat): 164 | if (not self.debug): 165 | return False 166 | 167 | now = datetime.datetime.fromtimestamp(self._debugData["PriceEmaFast"][-1]["now"]) 168 | time_of_interest = datetime.datetime.strptime(timestring,timeformat) 169 | if (now > time_of_interest): 170 | return True 171 | 172 | return False 173 | 174 | def _updatePriceDebugHook(self, data): 175 | if (not self.debug): 176 | return 177 | 178 | if ("RawPrice" not in self._debugData): 179 | self._debugData["RawPrice"] = [] 180 | 181 | if ("PriceEmaSlow" not in self._debugData): 182 | self._debugData["PriceEmaSlow"] = [] 183 | 184 | if ("PriceEmaFast" not in self._debugData): 185 | self._debugData["PriceEmaFast"] = [] 186 | 187 | if ("PriceEmaLongTerm" not in self._debugData): 188 | self._debugData["PriceEmaLongTerm"] = [] 189 | 190 | self._debugData["RawPrice"].append(data) 191 | 192 | tmp = {} 193 | tmp["now"] = data["now"] 194 | tmp["value"] = self.price_ema_slow.Value 195 | self._debugData["PriceEmaSlow"].append(tmp) 196 | del tmp 197 | 198 | tmp = {} 199 | tmp["now"] = data["now"] 200 | tmp["value"] = self.price_ema_fast.Value 201 | self._debugData["PriceEmaFast"].append(tmp) 202 | del tmp 203 | 204 | tmp = {} 205 | tmp["now"] = data["now"] 206 | tmp["value"] = self.price_ema_longterm.Value 207 | self._debugData["PriceEmaLongTerm"].append(tmp) 208 | del tmp 209 | 210 | self._debugData["LastPriceUpdateTime"] = data["now"] 211 | 212 | def _preSellBTCDebugHook(self): 213 | if (not self.debug): 214 | return 215 | msg = "Selling " + str(self.xcon.AvailableBTC()) + " BTC" 216 | msg += " current price: " + str(self.Current_Price) 217 | print msg 218 | 219 | def _postSellBTCDebugHook(self): 220 | if (not self.debug): 221 | return 222 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 223 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 224 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 225 | print msg 226 | 227 | if ("Trades" not in self._debugData): 228 | self._debugData["Trades"] = [] 229 | 230 | tmp = {} 231 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 232 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 233 | self._debugData["Trades"].append(tmp) 234 | 235 | if ("Sell" not in self._debugData): 236 | self._debugData["Sell"] = [] 237 | 238 | tmp = {} 239 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 240 | tmp["value"] = self.Last_Sell_Price 241 | self._debugData["Sell"].append(tmp) 242 | 243 | def _preBuyBTCDebugHook(self): 244 | if (not self.debug): 245 | return 246 | msg = "Buying " + str(self.xcon.AvailableUSD() / self.Current_Price) + " BTC" 247 | msg += " current price: " + str(self.Current_Price) 248 | print msg 249 | 250 | def _postBuyBTCDebugHook(self): 251 | if (not self.debug): 252 | return 253 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 254 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 255 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 256 | print msg 257 | 258 | if ("Trades" not in self._debugData): 259 | self._debugData["Trades"] = [] 260 | 261 | tmp = {} 262 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 263 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 264 | self._debugData["Trades"].append(tmp) 265 | 266 | if ("Buy" not in self._debugData): 267 | self._debugData["Buy"] = [] 268 | 269 | tmp = {} 270 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 271 | tmp["value"] = self.Last_Buy_Price 272 | self._debugData["Buy"].append(tmp) 273 | 274 | def feedRecordedData(score, sqliteDataFile, date_from, date_to): 275 | 276 | db = sqlite3.connect(sqliteDataFile) 277 | cursor = db.cursor() 278 | data = cursor.execute("select amount,price,date from trades where ( (date>"+str(date_from)+") and (date<"+str(date_to)+") and (currency='USD') )") 279 | actual_date_from = 9999999999999 280 | actual_date_to = 0 281 | 282 | price_volume_data = [] 283 | 284 | for row in data: 285 | volume = float(row[0]) 286 | price = float(row[1]) 287 | date = float(row[2]) 288 | 289 | if ( actual_date_from > date ): 290 | actual_date_from = date 291 | if ( actual_date_to < date ): 292 | actual_date_to = date 293 | 294 | update_data = {} 295 | update_data["now"] = date 296 | update_data["price"] = price 297 | update_data["volume"] = volume 298 | price_volume_data.append(update_data) 299 | 300 | from operator import itemgetter 301 | price_volume_data = sorted(price_volume_data, key=itemgetter("now")) 302 | 303 | for item in price_volume_data: 304 | score.xcon.SetBTCPrice(item["price"]) 305 | tmp = {"now":item["now"], "value": item["price"]} 306 | score.UpdatePrice(tmp) 307 | score.Act() 308 | 309 | cursor.close() 310 | 311 | return (actual_date_from, actual_date_to) 312 | 313 | def plotStrategyCorePerformance(debugData): 314 | import strategy_plot 315 | 316 | subplots = 1 317 | splot = strategy_plot.StrategyPlot(debugData, subplots) 318 | splot.Plot("RawPrice",1, "y-") 319 | splot.Plot("Sell", 1, "ro") 320 | splot.Plot("Buy", 1, "g^") 321 | splot.Plot("PriceEmaSlow", 1, "g-") 322 | splot.Plot("PriceEmaFast", 1, "b-") 323 | splot.Plot("PriceEmaLongTerm", 1, "r-") 324 | 325 | splot.Show() 326 | 327 | def main(): 328 | # Test this strategy core by mocking ExchangeConnection 329 | # And by feeding it the prerecorded data 330 | xcon = MockExchangeConnection() 331 | score = StrategyLogicSimpleTrendFollower(xcon, debug = True) 332 | 333 | tmp = datetime.datetime.strptime("2013 Nov 1 00:00", "%Y %b %d %H:%M") 334 | date_from = float(calendar.timegm(tmp.utctimetuple())) 335 | tmp = datetime.datetime.strptime("2013 Nov 30 00:00", "%Y %b %d %H:%M") 336 | date_to = float(calendar.timegm(tmp.utctimetuple())) 337 | 338 | (actual_date_from, actual_date_to) = feedRecordedData(score, "mtgoxdata/mtgox.sqlite3", date_from, date_to) 339 | print "Simulation from: " + str(datetime.datetime.fromtimestamp(date_from)) + " to " + str(datetime.datetime.fromtimestamp(date_to)) 340 | print "Total funds. BTC: " + str(xcon.AvailableBTC()) + " USD: " + str(xcon.AvailableUSD()) + " current price: " + str(xcon.currentPrice) + " Convert to USD:" + str(xcon.AvailableBTC() * score.Current_Price + xcon.AvailableUSD()) 341 | plotStrategyCorePerformance(score._debugData) 342 | 343 | if __name__ == "__main__": 344 | main() -------------------------------------------------------------------------------- /strategy_logic_simple_mean_reversion.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | import pickle 4 | import sqlite3 5 | import calendar 6 | import math 7 | from indicator.ma import ExponentialMovingAverage as ema 8 | from exchange_connection import ExchangeConnection, MockExchangeConnection 9 | 10 | """ 11 | Simple mean reversion bot. It relies on three moving averages. 12 | It believes that if faster moving averages are significantly 13 | larger slow moving ones, that there will be a reversal movement 14 | in the market, and price will drop. Same for the opposite scenario. 15 | If fast moving average is significantly lower the slower moving one, 16 | it is treated as a signal to buy in anticipation of market mode 17 | switch to bull. 18 | """ 19 | 20 | class StrategyLogicSimpleMeanReversion: 21 | def __init__(self, xcon, filename = "strategy_logic_simple_mean_reversion.pickle", debug = False): 22 | 23 | self.filename = filename 24 | self.xcon = xcon 25 | 26 | # Constants 27 | self.Price_Fast_EMA_Time = 7 * 60 # minutes 28 | self.Price_Slow_EMA_Time = 14 * 60 # minutes 29 | self.Price_LongTerm_EMA_Time = 21 * 60 # minutes 30 | 31 | self.Last_Buy_Price = 0.0 32 | self.Last_Sell_Price = 0.0 33 | self.Current_Price = 0.0 34 | 35 | self.MinimumSpreadBuy = 0.0385 36 | self.MinimumSpreadSell = 0.00832 37 | 38 | # Price indicators 39 | 40 | # Slow moving price average 41 | timedelta = datetime.timedelta(minutes = self.Price_Slow_EMA_Time) 42 | self.price_ema_slow = ema(timedelta) 43 | 44 | # Fast five minutes moving averages 45 | timedelta = datetime.timedelta(minutes = self.Price_Fast_EMA_Time) 46 | self.price_ema_fast = ema(timedelta) 47 | 48 | # Long Term moving average 49 | timedelta = datetime.timedelta(minutes = self.Price_LongTerm_EMA_Time) 50 | self.price_ema_longterm = ema(timedelta) 51 | 52 | self._debugData = {} 53 | 54 | # Restore state from disk if possible 55 | if (not debug): 56 | self.Load() 57 | 58 | # Make sure we use the currently passed ExchangeConnection, not the restored one 59 | self.xcon = xcon 60 | self.debug = debug 61 | 62 | def UpdatePrice(self, data): 63 | 64 | self.price_ema_slow.Update(data) 65 | self.price_ema_fast.Update(data) 66 | self.price_ema_longterm.Update(data) 67 | self.Current_Price = data["value"] 68 | self._updatePriceDebugHook(data) 69 | 70 | def ShouldBuy(self): 71 | # Returns true if faster moving averages are significantly 72 | # lower slower moving ones. 73 | # Treated as a signal to buy. 74 | 75 | # ma_diff_min = self.price_ema_slow.Value * self.MinimumSpreadBuy 76 | ma_diff_min = self.price_ema_longterm.Value * self.MinimumSpreadBuy 77 | ma_diff = self.price_ema_slow.Value - self.price_ema_fast.Value 78 | if ( ma_diff < ma_diff_min ): 79 | return False 80 | 81 | # ma_diff_min = self.price_ema_longterm.Value * self.MinimumSpreadBuy 82 | ma_diff = self.price_ema_longterm.Value - self.price_ema_slow.Value 83 | if ( ma_diff < ma_diff_min ): 84 | return False 85 | 86 | return True 87 | 88 | def ShouldSell(self): 89 | # Returns true if faster moving averages are significantly 90 | # larger slower moving ones. 91 | # Treated as a signal to sell. 92 | 93 | ma_diff_min = self.price_ema_fast.Value * self.MinimumSpreadSell 94 | ma_diff = self.price_ema_fast.Value - self.price_ema_slow.Value 95 | if ( ma_diff < ma_diff_min ): 96 | return False 97 | 98 | # ma_diff_min = self.price_ema_slow.Value * self.MinimumSpreadSell 99 | ma_diff = self.price_ema_slow.Value - self.price_ema_longterm.Value 100 | if ( ma_diff < ma_diff_min ): 101 | return False 102 | 103 | return True 104 | 105 | def Act(self): 106 | 107 | # If we do not have enough data, and are just starting, take no action 108 | if (not self.price_ema_slow.IsAccurate()): 109 | return 110 | 111 | if (not self.price_ema_longterm.IsAccurate()): 112 | return 113 | 114 | # Debug breakpoint for particular datapoints in time 115 | if (self._debugIsNowPassedTimeStamp("2013 Aug 17 05:00", "%Y %b %d %H:%M")): 116 | set_breakpoint_here = True 117 | 118 | # Currently invested in BTC 119 | if (self.xcon.AvailableBTC() * self.Current_Price > self.xcon.AvailableUSD()): 120 | 121 | if (not self.ShouldSell()): 122 | return 123 | 124 | self.ConvertAllToUSD() 125 | return 126 | 127 | # Currently invested in USD 128 | if (self.xcon.AvailableBTC() * self.Current_Price < self.xcon.AvailableUSD()): 129 | 130 | if (not self.ShouldBuy()): 131 | return 132 | 133 | self.ConvertAllToBTC() 134 | 135 | def Load(self): 136 | try: 137 | f = open(self.filename,'rb') 138 | tmp_dict = pickle.load(f) 139 | self.__dict__.update(tmp_dict) 140 | print "LoadSuccess!" 141 | f.close() 142 | except: 143 | print "StrategyLogicSimpleMeanReversion: Failed to load previous state, starting from scratch" 144 | 145 | def Save(self): 146 | try: 147 | f = open(self.filename,'wb') 148 | odict = self.__dict__.copy() 149 | del odict['xcon'] #don't pickle this 150 | pickle.dump(odict,f,2) 151 | f.close() 152 | except: 153 | print "StrategyLogicSimpleMeanReversion: Failed to save my state, all the data will be lost" 154 | 155 | def CancelAllOutstandingOrders(self): 156 | if (not self.debug): 157 | for order in gox.orderbook.owns: 158 | gox.cancel(order.oid) 159 | 160 | def ConvertAllToUSD(self): 161 | self.CancelAllOutstandingOrders() 162 | self._preSellBTCDebugHook() 163 | self.Last_Sell_Price = self.Current_Price 164 | btc_to_sell = self.xcon.AvailableBTC() 165 | self.xcon.SellBTC(btc_to_sell) 166 | self._postSellBTCDebugHook() 167 | 168 | def ConvertAllToBTC(self): 169 | self.CancelAllOutstandingOrders() 170 | self._preBuyBTCDebugHook() 171 | self.Last_Buy_Price = self.Current_Price 172 | affordable_amount_of_btc = self.xcon.AvailableUSD() / self.Current_Price 173 | self.xcon.BuyBTC(affordable_amount_of_btc) 174 | self._postBuyBTCDebugHook() 175 | 176 | def _debugIsNowPassedTimeStamp(self, timestring, timeformat): 177 | if (not self.debug): 178 | return False 179 | 180 | now = datetime.datetime.fromtimestamp(self._debugData["PriceEmaFast"][-1]["now"]) 181 | time_of_interest = datetime.datetime.strptime(timestring,timeformat) 182 | if (now > time_of_interest): 183 | return True 184 | 185 | return False 186 | 187 | def _updatePriceDebugHook(self, data): 188 | if (not self.debug): 189 | return 190 | 191 | if ("RawPrice" not in self._debugData): 192 | self._debugData["RawPrice"] = [] 193 | 194 | if ("PriceEmaSlow" not in self._debugData): 195 | self._debugData["PriceEmaSlow"] = [] 196 | 197 | if ("PriceEmaFast" not in self._debugData): 198 | self._debugData["PriceEmaFast"] = [] 199 | 200 | if ("PriceEmaLongTerm" not in self._debugData): 201 | self._debugData["PriceEmaLongTerm"] = [] 202 | 203 | self._debugData["RawPrice"].append(data) 204 | 205 | tmp = {} 206 | tmp["now"] = data["now"] 207 | tmp["value"] = self.price_ema_slow.Value 208 | self._debugData["PriceEmaSlow"].append(tmp) 209 | del tmp 210 | 211 | tmp = {} 212 | tmp["now"] = data["now"] 213 | tmp["value"] = self.price_ema_fast.Value 214 | self._debugData["PriceEmaFast"].append(tmp) 215 | del tmp 216 | 217 | tmp = {} 218 | tmp["now"] = data["now"] 219 | tmp["value"] = self.price_ema_longterm.Value 220 | self._debugData["PriceEmaLongTerm"].append(tmp) 221 | del tmp 222 | 223 | self._debugData["LastPriceUpdateTime"] = data["now"] 224 | 225 | def _preSellBTCDebugHook(self): 226 | if (not self.debug): 227 | return 228 | msg = "Selling " + str(self.xcon.AvailableBTC()) + " BTC" 229 | msg += " current price: " + str(self.Current_Price) 230 | print msg 231 | 232 | def _postSellBTCDebugHook(self): 233 | if (not self.debug): 234 | return 235 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 236 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 237 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 238 | print msg 239 | 240 | if ("Trades" not in self._debugData): 241 | self._debugData["Trades"] = [] 242 | 243 | tmp = {} 244 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 245 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 246 | self._debugData["Trades"].append(tmp) 247 | 248 | if ("Sell" not in self._debugData): 249 | self._debugData["Sell"] = [] 250 | 251 | tmp = {} 252 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 253 | tmp["value"] = self.Last_Sell_Price 254 | self._debugData["Sell"].append(tmp) 255 | 256 | def _preBuyBTCDebugHook(self): 257 | if (not self.debug): 258 | return 259 | msg = "Buying " + str(self.xcon.AvailableUSD() / self.Current_Price) + " BTC" 260 | msg += " current price: " + str(self.Current_Price) 261 | print msg 262 | 263 | def _postBuyBTCDebugHook(self): 264 | if (not self.debug): 265 | return 266 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 267 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 268 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 269 | print msg 270 | 271 | if ("Trades" not in self._debugData): 272 | self._debugData["Trades"] = [] 273 | 274 | tmp = {} 275 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 276 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 277 | self._debugData["Trades"].append(tmp) 278 | 279 | if ("Buy" not in self._debugData): 280 | self._debugData["Buy"] = [] 281 | 282 | tmp = {} 283 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 284 | tmp["value"] = self.Last_Buy_Price 285 | self._debugData["Buy"].append(tmp) 286 | 287 | def feedRecordedData(score, sqliteDataFile, date_from, date_to): 288 | 289 | db = sqlite3.connect(sqliteDataFile) 290 | cursor = db.cursor() 291 | data = cursor.execute("select amount,price,date from trades where ( (date>"+str(date_from)+") and (date<"+str(date_to)+") and (currency='USD') )") 292 | actual_date_from = 9999999999999 293 | actual_date_to = 0 294 | 295 | price_volume_data = [] 296 | 297 | for row in data: 298 | volume = float(row[0]) 299 | price = float(row[1]) 300 | date = float(row[2]) 301 | 302 | if ( actual_date_from > date ): 303 | actual_date_from = date 304 | if ( actual_date_to < date ): 305 | actual_date_to = date 306 | 307 | update_data = {} 308 | update_data["now"] = date 309 | update_data["price"] = price 310 | update_data["volume"] = volume 311 | price_volume_data.append(update_data) 312 | 313 | from operator import itemgetter 314 | price_volume_data = sorted(price_volume_data, key=itemgetter("now")) 315 | 316 | for item in price_volume_data: 317 | score.xcon.SetBTCPrice(item["price"]) 318 | tmp = {"now":item["now"], "value": item["price"]} 319 | score.UpdatePrice(tmp) 320 | score.Act() 321 | 322 | cursor.close() 323 | 324 | return (actual_date_from, actual_date_to) 325 | 326 | def plotStrategyCorePerformance(debugData): 327 | import strategy_plot 328 | 329 | subplots = 1 330 | splot = strategy_plot.StrategyPlot(debugData, subplots) 331 | splot.Plot("RawPrice",1, "y-") 332 | splot.Plot("Sell", 1, "ro") 333 | splot.Plot("Buy", 1, "g^") 334 | splot.Plot("PriceEmaSlow", 1, "g-") 335 | splot.Plot("PriceEmaFast", 1, "b-") 336 | splot.Plot("PriceEmaLongTerm", 1, "r-") 337 | 338 | splot.Show() 339 | 340 | def minimizeFunction(minSpread): 341 | xcon = MockExchangeConnection() 342 | score = StrategyLogicSimpleMeanReversion(xcon, debug = True) 343 | minSpreadBuy = minSpread[0] 344 | minSpreadSell = minSpread[1] 345 | score.MinimumSpreadBuy = minSpreadBuy 346 | score.MinimumSpreadSell = minSpreadSell 347 | 348 | tmp = datetime.datetime.strptime("2013 Dec 1 00:00", "%Y %b %d %H:%M") 349 | date_from = float(calendar.timegm(tmp.utctimetuple())) 350 | tmp = datetime.datetime.strptime("2014 Jan 1 00:00", "%Y %b %d %H:%M") 351 | date_to = float(calendar.timegm(tmp.utctimetuple())) 352 | 353 | feedRecordedData(score, "mtgoxdata/mtgox.sqlite3", date_from, date_to) 354 | totalFunds = xcon.AvailableBTC() * score.Current_Price + xcon.AvailableUSD() 355 | print "MinimumSpreadBuy="+ str(minSpreadBuy) + " MinimumSpreadSell=" + str(minSpreadSell) 356 | print "Total funds = " + str(totalFunds) 357 | print "*********************************************" 358 | return 1/totalFunds 359 | 360 | def optimizeMagicNumbers(): 361 | from scipy.optimize import fmin 362 | from scipy.optimize import minimize 363 | from scipy.optimize import fmin_tnc 364 | from scipy.optimize import anneal 365 | 366 | xopt = anneal(minimizeFunction, [0.0385, 0.00832], maxeval=50, disp=True, lower=[0.0, 0.0], upper=[0.1, 0.1] ) 367 | print "MonteCarlo optimzation results: " + str(xopt) 368 | print "Starting gradient descent with the following starting point: MinimumSpreadBuy=" + str(xopt[0][0]) + "MinimumSpreadSell=" +str(xopt[0][1]) 369 | xopt = fmin(minimizeFunction, [xopt[0][0], xopt[0][1]], maxiter=100, maxfun=4000) 370 | print "Gradient descent optimization results: " + str(xopt) 371 | print "Result: MinimumSpreadBuy=" + str(xopt[0]) + " MinimumSpreadSell=" + str(xopt[1]) 372 | 373 | def main(): 374 | # Test this strategy core by mocking ExchangeConnection 375 | # And by feeding it the prerecorded data 376 | xcon = MockExchangeConnection() 377 | score = StrategyLogicSimpleMeanReversion(xcon, debug = True) 378 | 379 | tmp = datetime.datetime.strptime("2013 Dec 1 00:00", "%Y %b %d %H:%M") 380 | date_from = float(calendar.timegm(tmp.utctimetuple())) 381 | tmp = datetime.datetime.strptime("2014 Jan 1 00:00", "%Y %b %d %H:%M") 382 | date_to = float(calendar.timegm(tmp.utctimetuple())) 383 | 384 | (actual_date_from, actual_date_to) = feedRecordedData(score, "mtgoxdata/mtgox.sqlite3", date_from, date_to) 385 | print "Simulation from: " + str(datetime.datetime.fromtimestamp(date_from)) + " to " + str(datetime.datetime.fromtimestamp(date_to)) 386 | print "Total funds. BTC: " + str(xcon.AvailableBTC()) + " USD: " + str(xcon.AvailableUSD()) + " current price: " + str(xcon.currentPrice) + " Convert to USD:" + str(xcon.AvailableBTC() * score.Current_Price + xcon.AvailableUSD()) 387 | plotStrategyCorePerformance(score._debugData) 388 | 389 | if __name__ == "__main__": 390 | import argparse 391 | parser = argparse.ArgumentParser() 392 | parser.add_argument('-o', '--optimize_magic_humbers', action="store_true") 393 | args = parser.parse_args() 394 | if (args.optimize_magic_humbers): 395 | optimizeMagicNumbers() 396 | exit() 397 | main() 398 | -------------------------------------------------------------------------------- /strategy_logic_volume_trend_follower.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | import pickle 4 | import sqlite3 5 | import calendar 6 | import math 7 | from indicator.ma import ExponentialMovingAverage as ema 8 | from indicator.ma import SimpleMovingAverage as sma 9 | from indicator.candlestick import CandleStick 10 | from indicator.timesum import TimeSum 11 | from indicator.timeminmax import TimeMax, TimeMin 12 | from exchange_connection import ExchangeConnection, MockExchangeConnection 13 | 14 | """ 15 | Volume Trend Following bot 16 | Similar to moving average based trading bot. 17 | The difference is that it also pays attention to volume of trades. 18 | Big price moves are usually associated with big trading volumes. 19 | So this bot tracks the amount of trades made, and uses it as a 20 | gating signal for trend following trades. 21 | """ 22 | 23 | class StrategyLogicVolumeTrendFollower: 24 | def __init__(self, xcon, filename = "strategy_logic_volume_trend_follower.pickle", debug = False): 25 | 26 | self.filename = filename 27 | self.xcon = xcon 28 | 29 | # Constants 30 | self.Price_Fast_EMA_Time = 0.5 # 5 minutes 31 | self.Price_Slow_SMA_Time = 14 # 30 minutes 32 | self.Price_LongTerm_EMA_Time = 60*4 # 4 hours 33 | 34 | self.Volume_TimeSum_Time = 1 # minutes 35 | self.Volume_Fast_EMA_Time = 3 # minutes 36 | self.Volume_Slow_SMA_Time = 20 # minutes 37 | 38 | self.Volume_MA_Spike_Diff_Coef = 3.5 # ema/sma 39 | self.Volume_MA_Spike_Diff_Value = 570 # ema - sma 40 | 41 | self.Last_Buy_Price = 0.0 42 | self.Last_Sell_Price = 0.0 43 | self.Current_Price = 0.0 44 | 45 | # Volume indicators 46 | 47 | # Volume TimeSum - a sum of all trades per timeframe 48 | self.current_volume_timesum = None 49 | 50 | # Volume slow moving average 51 | timedelta = datetime.timedelta(minutes = self.Volume_Slow_SMA_Time) 52 | self.volume_sma_slow = sma(timedelta) 53 | 54 | # Volume fast moving average 55 | timedelta = datetime.timedelta(minutes = self.Volume_Fast_EMA_Time) 56 | self.volume_ema_fast = ema(timedelta) 57 | 58 | self.volume_spike = False 59 | 60 | # Price indicators 61 | 62 | # Slow moving price average 63 | timedelta = datetime.timedelta(minutes = self.Price_Slow_SMA_Time) 64 | self.price_sma_slow = sma(timedelta) 65 | 66 | # Fast five minutes moving averages 67 | timedelta = datetime.timedelta(minutes = self.Price_Fast_EMA_Time) 68 | self.price_ema_fast = ema(timedelta) 69 | 70 | # Long Term moving average 71 | timedelta = datetime.timedelta(minutes = self.Price_LongTerm_EMA_Time) 72 | self.price_ema_longterm = ema(timedelta) 73 | 74 | self._debugData = {} 75 | 76 | # Restore state from disk if possible 77 | if (not debug): 78 | self.Load() 79 | 80 | # Make sure we use the currently passed ExchangeConnection, not the restored one 81 | self.xcon = xcon 82 | self.debug = debug 83 | 84 | def UpdateVolume(self, data): 85 | 86 | if (self.current_volume_timesum == None): 87 | timesum_time_from = datetime.datetime.fromtimestamp(int(data["now"])) 88 | timesum_time_to = timesum_time_from + datetime.timedelta(minutes = self.Volume_TimeSum_Time) 89 | self.current_volume_timesum = TimeSum(timesum_time_from, timesum_time_to) 90 | 91 | self.current_volume_timesum.Update(data) 92 | 93 | self._volumeUpdateDebugHook(data) 94 | 95 | if ( not self.current_volume_timesum.IsAccurate() ): 96 | return 97 | 98 | data["value"] = self.current_volume_timesum.Value 99 | self.volume_sma_slow.Update(data) 100 | self.volume_ema_fast.Update(data) 101 | 102 | if (self.volume_ema_fast.Value / self.volume_sma_slow.Value > self.Volume_MA_Spike_Diff_Coef): 103 | if (self.volume_ema_fast.Value - self.volume_sma_slow.Value > self.Volume_MA_Spike_Diff_Value): 104 | self.volume_spike = True 105 | 106 | if (self.volume_ema_fast.Value < self.volume_sma_slow.Value ): 107 | self.volume_spike = False 108 | 109 | self.current_volume_timesum = None 110 | 111 | def UpdatePrice(self, data): 112 | 113 | self.price_sma_slow.Update(data) 114 | self.price_ema_fast.Update(data) 115 | self.price_ema_longterm.Update(data) 116 | self.Current_Price = data["value"] 117 | 118 | self._updatePriceDebugHook(data) 119 | 120 | def IsDownTrend(self): 121 | 122 | if ( self.price_ema_fast.Value > self.price_sma_slow.Value ): 123 | return False 124 | 125 | if ( self.price_ema_fast.Value > self.price_ema_longterm.Value): 126 | return False 127 | 128 | if (self.price_sma_slow.Value > self.price_ema_longterm.Value): 129 | return False 130 | 131 | return True 132 | 133 | def IsUpTrend(self): 134 | ma_diff = self.price_ema_fast.Value - self.price_sma_slow.Value 135 | ma_diff_min = self.price_ema_fast.Value * 0.0012 136 | # ma_diff_min = 0 137 | 138 | if ( ma_diff < ma_diff_min ): 139 | return False 140 | 141 | ma_diff = self.price_ema_fast.Value - self.price_ema_longterm.Value 142 | ma_diff_min = self.price_ema_fast.Value * 0.0012 143 | if ( ma_diff < ma_diff_min ): 144 | return False 145 | 146 | ma_diff = self.price_sma_slow.Value - self.price_ema_longterm.Value 147 | ma_diff_min = self.price_ema_fast.Value * 0.0012 148 | if ( ma_diff < ma_diff_min ): 149 | return False 150 | 151 | return True 152 | 153 | def Act(self): 154 | 155 | # If we do not have enough data, and are just starting, take no action 156 | if (not self.price_sma_slow.IsAccurate()): 157 | return 158 | 159 | if (not self.price_ema_longterm.IsAccurate()): 160 | return 161 | 162 | if (not self.volume_sma_slow.IsAccurate()): 163 | return 164 | 165 | # Currently invested in BTC 166 | if (self.xcon.AvailableBTC() * self.Current_Price > self.xcon.AvailableUSD()): 167 | 168 | if (self.volume_spike == False): 169 | return 170 | 171 | if (not self.IsDownTrend()): 172 | return 173 | 174 | self.ConvertAllToUSD() 175 | return 176 | 177 | # Currently invested in USD 178 | if (self.xcon.AvailableBTC() * self.Current_Price < self.xcon.AvailableUSD()): 179 | 180 | if (self.volume_spike == False): 181 | return 182 | 183 | if (not self.IsUpTrend()): 184 | return 185 | 186 | self.ConvertAllToBTC() 187 | 188 | def Load(self): 189 | try: 190 | f = open(self.filename,'rb') 191 | tmp_dict = pickle.load(f) 192 | self.__dict__.update(tmp_dict) 193 | print "LoadSuccess!" 194 | f.close() 195 | except: 196 | print "SimpleTrendFollowerStrategyCore: Failed to load previous state, starting from scratch" 197 | 198 | def Save(self): 199 | try: 200 | f = open(self.filename,'wb') 201 | odict = self.__dict__.copy() 202 | del odict['xcon'] #don't pickle this 203 | pickle.dump(odict,f,2) 204 | f.close() 205 | except: 206 | print "SimpleTrendFollowerStrategyCore: Failed to save my state, all the data will be lost" 207 | 208 | def ConvertAllToUSD(self): 209 | self._preSellBTCDebugHook() 210 | self.Last_Sell_Price = self.Current_Price 211 | btc_to_sell = self.xcon.AvailableBTC() 212 | self.xcon.SellBTC(btc_to_sell) 213 | self._postSellBTCDebugHook() 214 | 215 | def ConvertAllToBTC(self): 216 | self._preBuyBTCDebugHook() 217 | self.Last_Buy_Price = self.Current_Price 218 | affordable_amount_of_btc = self.xcon.AvailableUSD() / self.Current_Price 219 | self.xcon.BuyBTC(affordable_amount_of_btc) 220 | self._postBuyBTCDebugHook() 221 | 222 | 223 | def _updatePriceDebugHook(self, data): 224 | if (not self.debug): 225 | return 226 | 227 | if ("RawPrice" not in self._debugData): 228 | self._debugData["RawPrice"] = [] 229 | 230 | if ("PriceSmaSlow" not in self._debugData): 231 | self._debugData["PriceSmaSlow"] = [] 232 | 233 | if ("PriceEmaFast" not in self._debugData): 234 | self._debugData["PriceEmaFast"] = [] 235 | 236 | if ("PriceEmaLongTerm" not in self._debugData): 237 | self._debugData["PriceEmaLongTerm"] = [] 238 | 239 | if ("PriceEmaDiff" not in self._debugData): 240 | self._debugData["PriceEmaDiff"] = [] 241 | 242 | self._debugData["RawPrice"].append(data) 243 | 244 | tmp = {} 245 | tmp["now"] = data["now"] 246 | tmp["value"] = self.price_sma_slow.Value 247 | self._debugData["PriceSmaSlow"].append(tmp) 248 | del tmp 249 | 250 | tmp = {} 251 | tmp["now"] = data["now"] 252 | tmp["value"] = self.price_ema_fast.Value 253 | self._debugData["PriceEmaFast"].append(tmp) 254 | del tmp 255 | 256 | tmp = {} 257 | tmp["now"] = data["now"] 258 | tmp["value"] = self.price_ema_longterm.Value 259 | self._debugData["PriceEmaLongTerm"].append(tmp) 260 | del tmp 261 | 262 | self._debugData["LastPriceUpdateTime"] = data["now"] 263 | 264 | def _volumeUpdateDebugHook(self, data): 265 | 266 | if (not self.debug): 267 | return 268 | 269 | if ("RawVolume" not in self._debugData): 270 | self._debugData["RawVolume"] = [] 271 | 272 | if ("VolumeTimeSums" not in self._debugData): 273 | self._debugData["VolumeTimeSums"] = [] 274 | 275 | if ("VolumeSmaSlow" not in self._debugData): 276 | self._debugData["VolumeSmaSlow"] = [] 277 | 278 | if ("VolumeEmaFast" not in self._debugData): 279 | self._debugData["VolumeEmaFast"] = [] 280 | 281 | if ("VolumeSpike" not in self._debugData): 282 | self._debugData["VolumeSpike"] = [] 283 | 284 | self._debugData["RawVolume"].append(data) 285 | 286 | tmp = {} 287 | tmp["now"] = data["now"] 288 | tmp["value"] = self.volume_sma_slow.Value 289 | self._debugData["VolumeSmaSlow"].append(tmp) 290 | del tmp 291 | 292 | tmp = {} 293 | tmp["now"] = data["now"] 294 | tmp["value"] = self.volume_ema_fast.Value 295 | self._debugData["VolumeEmaFast"].append(tmp) 296 | 297 | tmp = {} 298 | tmp["now"] = data["now"] 299 | tmp["value"] = self.current_volume_timesum.Value 300 | if (self.current_volume_timesum.IsAccurate()): 301 | self._debugData["VolumeTimeSums"].append(tmp) 302 | 303 | tmp = {} 304 | tmp["now"] = data["now"] 305 | tmp["value"] = self.volume_spike 306 | self._debugData["VolumeSpike"].append(tmp) 307 | 308 | self._debugData["LastVolumeUpdateTime"] = data["now"] 309 | 310 | def _preSellBTCDebugHook(self): 311 | if (not self.debug): 312 | return 313 | msg = "Selling " + str(self.xcon.AvailableBTC()) + " BTC" 314 | msg += " current price: " + str(self.Current_Price) 315 | print msg 316 | 317 | def _postSellBTCDebugHook(self): 318 | if (not self.debug): 319 | return 320 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 321 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 322 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 323 | print msg 324 | 325 | if ("Trades" not in self._debugData): 326 | self._debugData["Trades"] = [] 327 | 328 | tmp = {} 329 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 330 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 331 | self._debugData["Trades"].append(tmp) 332 | 333 | if ("Sell" not in self._debugData): 334 | self._debugData["Sell"] = [] 335 | 336 | tmp = {} 337 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 338 | tmp["value"] = self.Last_Sell_Price 339 | self._debugData["Sell"].append(tmp) 340 | 341 | def _preBuyBTCDebugHook(self): 342 | if (not self.debug): 343 | return 344 | msg = "Buying " + str(self.xcon.AvailableUSD() / self.Current_Price) + " BTC" 345 | msg += " current price: " + str(self.Current_Price) 346 | print msg 347 | 348 | def _postBuyBTCDebugHook(self): 349 | if (not self.debug): 350 | return 351 | msg = " Wallet: " + str(self.xcon.AvailableBTC()) + " BTC " + str(self.xcon.AvailableUSD()) + " USD" 352 | msg += " totalUSD: " + str(self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price) 353 | msg += " date: " + str(datetime.datetime.fromtimestamp(self._debugData["LastPriceUpdateTime"])) 354 | print msg 355 | 356 | if ("Trades" not in self._debugData): 357 | self._debugData["Trades"] = [] 358 | 359 | tmp = {} 360 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 361 | tmp["value"] = self.xcon.AvailableUSD() + self.xcon.AvailableBTC() * self.Current_Price 362 | self._debugData["Trades"].append(tmp) 363 | 364 | if ("Buy" not in self._debugData): 365 | self._debugData["Buy"] = [] 366 | 367 | tmp = {} 368 | tmp["now"] = self._debugData["LastPriceUpdateTime"] 369 | tmp["value"] = self.Last_Buy_Price 370 | self._debugData["Buy"].append(tmp) 371 | 372 | def feedRecordedData(score, sqliteDataFile, date_from, date_to): 373 | 374 | db = sqlite3.connect(sqliteDataFile) 375 | cursor = db.cursor() 376 | data = cursor.execute("select amount,price,date from trades where ( (date>"+str(date_from)+") and (date<"+str(date_to)+") and (currency='USD') )") 377 | actual_date_from = 9999999999999 378 | actual_date_to = 0 379 | 380 | price_volume_data = [] 381 | 382 | for row in data: 383 | volume = float(row[0]) 384 | price = float(row[1]) 385 | date = float(row[2]) 386 | 387 | if ( actual_date_from > date ): 388 | actual_date_from = date 389 | if ( actual_date_to < date ): 390 | actual_date_to = date 391 | 392 | update_data = {} 393 | update_data["now"] = date 394 | update_data["price"] = price 395 | update_data["volume"] = volume 396 | price_volume_data.append(update_data) 397 | 398 | from operator import itemgetter 399 | price_volume_data = sorted(price_volume_data, key=itemgetter("now")) 400 | 401 | for item in price_volume_data: 402 | score.xcon.SetBTCPrice(item["price"]) 403 | tmp = {"now":item["now"], "value": item["price"]} 404 | score.UpdatePrice(tmp) 405 | tmp = {"now":item["now"], "value": item["volume"]} 406 | score.UpdateVolume(tmp) 407 | score.Act() 408 | 409 | cursor.close() 410 | 411 | return (actual_date_from, actual_date_to) 412 | 413 | def plotStrategyCorePerformance(debugData): 414 | import strategy_plot 415 | 416 | subplots = 1 417 | splot = strategy_plot.StrategyPlot(debugData, subplots) 418 | splot.Plot("RawPrice",1, "y-") 419 | splot.Plot("Sell", 1, "ro") 420 | splot.Plot("Buy", 1, "g^") 421 | # splot.Plot("VolumeTimeSums", 3) 422 | splot.Plot("PriceSmaSlow", 1, "r-") 423 | splot.Plot("PriceEmaFast", 1, "b-") 424 | splot.Plot("PriceEmaLongTerm", 1, "g-") 425 | # splot.Plot("Trades", 3, "g*") 426 | # splot.Plot("VolumeSpike", 4, "y-") 427 | # splot.Plot("VolumeSmaSlow", 2, "r-") 428 | # splot.Plot("VolumeEmaFast", 2, "b-") 429 | # splot.Plot("PriceEmaDiff", 3, "y-") 430 | 431 | splot.Show() 432 | 433 | def main(): 434 | # Test this strategy core by mocking ExchangeConnection 435 | # And by feeding it the prerecorded data 436 | xcon = MockExchangeConnection() 437 | score = StrategyLogicVolumeTrendFollower(xcon, debug = True) 438 | 439 | tmp = datetime.datetime.strptime("2013 Sep 1 00:00", "%Y %b %d %H:%M") 440 | date_from = float(calendar.timegm(tmp.utctimetuple())) 441 | tmp = datetime.datetime.strptime("2013 Sep 30 00:00", "%Y %b %d %H:%M") 442 | date_to = float(calendar.timegm(tmp.utctimetuple())) 443 | 444 | (actual_date_from, actual_date_to) = feedRecordedData(score, "mtgoxdata/mtgox.sqlite3", date_from, date_to) 445 | print "Simulation from: " + str(datetime.datetime.fromtimestamp(date_from)) + " to " + str(datetime.datetime.fromtimestamp(date_to)) 446 | print "Total funds. BTC: " + str(xcon.AvailableBTC()) + " USD: " + str(xcon.AvailableUSD()) + " current price: " + str(xcon.currentPrice) + " Convert to USD:" + str(xcon.AvailableBTC() * score.Current_Price + xcon.AvailableUSD()) 447 | plotStrategyCorePerformance(score._debugData) 448 | 449 | if __name__ == "__main__": 450 | main() -------------------------------------------------------------------------------- /websocket.py: -------------------------------------------------------------------------------- 1 | """ 2 | websocket - WebSocket client library for Python 3 | 4 | Copyright (C) 2010 Hiroki Ohtani(liris) 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | 20 | """ 21 | 22 | 23 | import socket 24 | from urlparse import urlparse 25 | import os 26 | import array 27 | import struct 28 | import uuid 29 | import hashlib 30 | import base64 31 | import logging 32 | 33 | """ 34 | websocket python client. 35 | ========================= 36 | 37 | This version support only hybi-13. 38 | Please see http://tools.ietf.org/html/rfc6455 for protocol. 39 | """ 40 | 41 | 42 | # websocket supported version. 43 | VERSION = 13 44 | 45 | # closing frame status codes. 46 | STATUS_NORMAL = 1000 47 | STATUS_GOING_AWAY = 1001 48 | STATUS_PROTOCOL_ERROR = 1002 49 | STATUS_UNSUPPORTED_DATA_TYPE = 1003 50 | STATUS_STATUS_NOT_AVAILABLE = 1005 51 | STATUS_ABNORMAL_CLOSED = 1006 52 | STATUS_INVALID_PAYLOAD = 1007 53 | STATUS_POLICY_VIOLATION = 1008 54 | STATUS_MESSAGE_TOO_BIG = 1009 55 | STATUS_INVALID_EXTENSION = 1010 56 | STATUS_UNEXPECTED_CONDITION = 1011 57 | STATUS_TLS_HANDSHAKE_ERROR = 1015 58 | 59 | logger = logging.getLogger() 60 | 61 | 62 | class WebSocketException(Exception): 63 | """ 64 | websocket exeception class. 65 | """ 66 | pass 67 | 68 | 69 | class WebSocketConnectionClosedException(WebSocketException): 70 | """ 71 | If remote host closed the connection or some network error happened, 72 | this exception will be raised. 73 | """ 74 | pass 75 | 76 | default_timeout = None 77 | traceEnabled = False 78 | 79 | 80 | def enableTrace(tracable): 81 | """ 82 | turn on/off the tracability. 83 | 84 | tracable: boolean value. if set True, tracability is enabled. 85 | """ 86 | global traceEnabled 87 | traceEnabled = tracable 88 | if tracable: 89 | if not logger.handlers: 90 | logger.addHandler(logging.StreamHandler()) 91 | logger.setLevel(logging.DEBUG) 92 | 93 | 94 | def setdefaulttimeout(timeout): 95 | """ 96 | Set the global timeout setting to connect. 97 | 98 | timeout: default socket timeout time. This value is second. 99 | """ 100 | global default_timeout 101 | default_timeout = timeout 102 | 103 | 104 | def getdefaulttimeout(): 105 | """ 106 | Return the global timeout setting(second) to connect. 107 | """ 108 | return default_timeout 109 | 110 | 111 | def _parse_url(url): 112 | """ 113 | parse url and the result is tuple of 114 | (hostname, port, resource path and the flag of secure mode) 115 | 116 | url: url string. 117 | """ 118 | if ":" not in url: 119 | raise ValueError("url is invalid") 120 | 121 | scheme, url = url.split(":", 1) 122 | 123 | parsed = urlparse(url, scheme="http") 124 | if parsed.hostname: 125 | hostname = parsed.hostname 126 | else: 127 | raise ValueError("hostname is invalid") 128 | port = 0 129 | if parsed.port: 130 | port = parsed.port 131 | 132 | is_secure = False 133 | if scheme == "ws": 134 | if not port: 135 | port = 80 136 | elif scheme == "wss": 137 | is_secure = True 138 | if not port: 139 | port = 443 140 | else: 141 | raise ValueError("scheme %s is invalid" % scheme) 142 | 143 | if parsed.path: 144 | resource = parsed.path 145 | else: 146 | resource = "/" 147 | 148 | if parsed.query: 149 | resource += "?" + parsed.query 150 | 151 | return (hostname, port, resource, is_secure) 152 | 153 | 154 | def create_connection(url, timeout=None, **options): 155 | """ 156 | connect to url and return websocket object. 157 | 158 | Connect to url and return the WebSocket object. 159 | Passing optional timeout parameter will set the timeout on the socket. 160 | If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used. 161 | You can customize using 'options'. 162 | If you set "header" dict object, you can set your own custom header. 163 | 164 | >>> conn = create_connection("ws://echo.websocket.org/", 165 | ... header={"User-Agent: MyProgram", 166 | ... "x-custom: header"}) 167 | 168 | 169 | timeout: socket timeout time. This value is integer. 170 | if you set None for this value, it means "use default_timeout value" 171 | 172 | options: current support option is only "header". 173 | if you set header as dict value, the custom HTTP headers are added. 174 | """ 175 | websock = WebSocket() 176 | websock.settimeout(timeout != None and timeout or default_timeout) 177 | websock.connect(url, **options) 178 | return websock 179 | 180 | _MAX_INTEGER = (1 << 32) -1 181 | _AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1) 182 | _MAX_CHAR_BYTE = (1<<8) -1 183 | 184 | # ref. Websocket gets an update, and it breaks stuff. 185 | # http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html 186 | 187 | 188 | def _create_sec_websocket_key(): 189 | uid = uuid.uuid4() 190 | return base64.encodestring(uid.bytes).strip() 191 | 192 | _HEADERS_TO_CHECK = { 193 | "upgrade": "websocket", 194 | "connection": "upgrade", 195 | } 196 | 197 | 198 | class _SSLSocketWrapper(object): 199 | def __init__(self, sock): 200 | self.ssl = socket.ssl(sock) 201 | 202 | def recv(self, bufsize): 203 | return self.ssl.read(bufsize) 204 | 205 | def send(self, payload): 206 | return self.ssl.write(payload) 207 | 208 | _BOOL_VALUES = (0, 1) 209 | 210 | 211 | def _is_bool(*values): 212 | for v in values: 213 | if v not in _BOOL_VALUES: 214 | return False 215 | 216 | return True 217 | 218 | 219 | class ABNF(object): 220 | """ 221 | ABNF frame class. 222 | see http://tools.ietf.org/html/rfc5234 223 | and http://tools.ietf.org/html/rfc6455#section-5.2 224 | """ 225 | 226 | # operation code values. 227 | OPCODE_TEXT = 0x1 228 | OPCODE_BINARY = 0x2 229 | OPCODE_CLOSE = 0x8 230 | OPCODE_PING = 0x9 231 | OPCODE_PONG = 0xa 232 | 233 | # available operation code value tuple 234 | OPCODES = (OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, 235 | OPCODE_PING, OPCODE_PONG) 236 | 237 | # opcode human readable string 238 | OPCODE_MAP = { 239 | OPCODE_TEXT: "text", 240 | OPCODE_BINARY: "binary", 241 | OPCODE_CLOSE: "close", 242 | OPCODE_PING: "ping", 243 | OPCODE_PONG: "pong" 244 | } 245 | 246 | # data length threashold. 247 | LENGTH_7 = 0x7d 248 | LENGTH_16 = 1 << 16 249 | LENGTH_63 = 1 << 63 250 | 251 | def __init__(self, fin = 0, rsv1 = 0, rsv2 = 0, rsv3 = 0, 252 | opcode = OPCODE_TEXT, mask = 1, data = ""): 253 | """ 254 | Constructor for ABNF. 255 | please check RFC for arguments. 256 | """ 257 | self.fin = fin 258 | self.rsv1 = rsv1 259 | self.rsv2 = rsv2 260 | self.rsv3 = rsv3 261 | self.opcode = opcode 262 | self.mask = mask 263 | self.data = data 264 | self.get_mask_key = os.urandom 265 | 266 | @staticmethod 267 | def create_frame(data, opcode): 268 | """ 269 | create frame to send text, binary and other data. 270 | 271 | data: data to send. This is string value(byte array). 272 | if opcode is OPCODE_TEXT and this value is uniocde, 273 | data value is conveted into unicode string, automatically. 274 | 275 | opcode: operation code. please see OPCODE_XXX. 276 | """ 277 | if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode): 278 | data = data.encode("utf-8") 279 | # mask must be set if send data from client 280 | return ABNF(1, 0, 0, 0, opcode, 1, data) 281 | 282 | def format(self): 283 | """ 284 | format this object to string(byte array) to send data to server. 285 | """ 286 | if not _is_bool(self.fin, self.rsv1, self.rsv2, self.rsv3): 287 | raise ValueError("not 0 or 1") 288 | if self.opcode not in ABNF.OPCODES: 289 | raise ValueError("Invalid OPCODE") 290 | length = len(self.data) 291 | if length >= ABNF.LENGTH_63: 292 | raise ValueError("data is too long") 293 | 294 | frame_header = chr(self.fin << 7 295 | | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 296 | | self.opcode) 297 | if length < ABNF.LENGTH_7: 298 | frame_header += chr(self.mask << 7 | length) 299 | elif length < ABNF.LENGTH_16: 300 | frame_header += chr(self.mask << 7 | 0x7e) 301 | frame_header += struct.pack("!H", length) 302 | else: 303 | frame_header += chr(self.mask << 7 | 0x7f) 304 | frame_header += struct.pack("!Q", length) 305 | 306 | if not self.mask: 307 | return frame_header + self.data 308 | else: 309 | mask_key = self.get_mask_key(4) 310 | return frame_header + self._get_masked(mask_key) 311 | 312 | def _get_masked(self, mask_key): 313 | s = ABNF.mask(mask_key, self.data) 314 | return mask_key + "".join(s) 315 | 316 | @staticmethod 317 | def mask(mask_key, data): 318 | """ 319 | mask or unmask data. Just do xor for each byte 320 | 321 | mask_key: 4 byte string(byte). 322 | 323 | data: data to mask/unmask. 324 | """ 325 | _m = array.array("B", mask_key) 326 | _d = array.array("B", data) 327 | for i in xrange(len(_d)): 328 | _d[i] ^= _m[i % 4] 329 | return _d.tostring() 330 | 331 | 332 | class WebSocket(object): 333 | """ 334 | Low level WebSocket interface. 335 | This class is based on 336 | The WebSocket protocol draft-hixie-thewebsocketprotocol-76 337 | http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 338 | 339 | We can connect to the websocket server and send/recieve data. 340 | The following example is a echo client. 341 | 342 | >>> import websocket 343 | >>> ws = websocket.WebSocket() 344 | >>> ws.connect("ws://echo.websocket.org") 345 | >>> ws.send("Hello, Server") 346 | >>> ws.recv() 347 | 'Hello, Server' 348 | >>> ws.close() 349 | 350 | get_mask_key: a callable to produce new mask keys, see the set_mask_key 351 | function's docstring for more details 352 | """ 353 | 354 | def __init__(self, get_mask_key = None): 355 | """ 356 | Initalize WebSocket object. 357 | """ 358 | self.connected = False 359 | self.io_sock = self.sock = socket.socket() 360 | self.get_mask_key = get_mask_key 361 | 362 | def set_mask_key(self, func): 363 | """ 364 | set function to create musk key. You can custumize mask key generator. 365 | Mainly, this is for testing purpose. 366 | 367 | func: callable object. the fuct must 1 argument as integer. 368 | The argument means length of mask key. 369 | This func must be return string(byte array), 370 | which length is argument specified. 371 | """ 372 | self.get_mask_key = func 373 | 374 | def settimeout(self, timeout): 375 | """ 376 | Set the timeout to the websocket. 377 | 378 | timeout: timeout time(second). 379 | """ 380 | self.sock.settimeout(timeout) 381 | 382 | def gettimeout(self): 383 | """ 384 | Get the websocket timeout(second). 385 | """ 386 | return self.sock.gettimeout() 387 | 388 | def connect(self, url, **options): 389 | """ 390 | Connect to url. url is websocket url scheme. ie. ws://host:port/resource 391 | You can customize using 'options'. 392 | If you set "header" dict object, you can set your own custom header. 393 | 394 | >>> ws = WebSocket() 395 | >>> ws.connect("ws://echo.websocket.org/", 396 | ... header={"User-Agent: MyProgram", 397 | ... "x-custom: header"}) 398 | 399 | timeout: socket timeout time. This value is integer. 400 | if you set None for this value, 401 | it means "use default_timeout value" 402 | 403 | options: current support option is only "header". 404 | if you set header as dict value, 405 | the custom HTTP headers are added. 406 | 407 | """ 408 | hostname, port, resource, is_secure = _parse_url(url) 409 | # TODO: we need to support proxy 410 | self.sock.connect((hostname, port)) 411 | if is_secure: 412 | self.io_sock = _SSLSocketWrapper(self.sock) 413 | self._handshake(hostname, port, resource, **options) 414 | 415 | def _handshake(self, host, port, resource, **options): 416 | sock = self.io_sock 417 | headers = [] 418 | headers.append("GET %s HTTP/1.1" % resource) 419 | headers.append("Upgrade: websocket") 420 | headers.append("Connection: Upgrade") 421 | if port == 80: 422 | hostport = host 423 | else: 424 | hostport = "%s:%d" % (host, port) 425 | headers.append("Host: %s" % hostport) 426 | 427 | if "origin" in options: 428 | headers.append("Origin: %s" % options["origin"]) 429 | else: 430 | headers.append("Origin: %s" % hostport) 431 | 432 | key = _create_sec_websocket_key() 433 | headers.append("Sec-WebSocket-Key: %s" % key) 434 | headers.append("Sec-WebSocket-Version: %s" % VERSION) 435 | if "header" in options: 436 | headers.extend(options["header"]) 437 | 438 | headers.append("") 439 | headers.append("") 440 | 441 | header_str = "\r\n".join(headers) 442 | sock.send(header_str) 443 | if traceEnabled: 444 | logger.debug("--- request header ---") 445 | logger.debug(header_str) 446 | logger.debug("-----------------------") 447 | 448 | status, resp_headers = self._read_headers() 449 | if status != 101: 450 | self.close() 451 | raise WebSocketException("Handshake Status %d" % status) 452 | 453 | success = self._validate_header(resp_headers, key) 454 | if not success: 455 | self.close() 456 | raise WebSocketException("Invalid WebSocket Header") 457 | 458 | self.connected = True 459 | 460 | def _validate_header(self, headers, key): 461 | for k, v in _HEADERS_TO_CHECK.iteritems(): 462 | r = headers.get(k, None) 463 | if not r: 464 | return False 465 | r = r.lower() 466 | if v != r: 467 | return False 468 | 469 | result = headers.get("sec-websocket-accept", None) 470 | if not result: 471 | return False 472 | result = result.lower() 473 | 474 | value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 475 | hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower() 476 | return hashed == result 477 | 478 | def _read_headers(self): 479 | status = None 480 | headers = {} 481 | if traceEnabled: 482 | logger.debug("--- response header ---") 483 | 484 | while True: 485 | line = self._recv_line() 486 | if line == "\r\n": 487 | break 488 | line = line.strip() 489 | if traceEnabled: 490 | logger.debug(line) 491 | if not status: 492 | status_info = line.split(" ", 2) 493 | status = int(status_info[1]) 494 | else: 495 | kv = line.split(":", 1) 496 | if len(kv) == 2: 497 | key, value = kv 498 | headers[key.lower()] = value.strip().lower() 499 | else: 500 | raise WebSocketException("Invalid header") 501 | 502 | if traceEnabled: 503 | logger.debug("-----------------------") 504 | 505 | return status, headers 506 | 507 | def send(self, payload, opcode = ABNF.OPCODE_TEXT): 508 | """ 509 | Send the data as string. 510 | 511 | payload: Payload must be utf-8 string or unicoce, 512 | if the opcode is OPCODE_TEXT. 513 | Otherwise, it must be string(byte array) 514 | 515 | opcode: operation code to send. Please see OPCODE_XXX. 516 | """ 517 | frame = ABNF.create_frame(payload, opcode) 518 | if self.get_mask_key: 519 | frame.get_mask_key = self.get_mask_key 520 | data = frame.format() 521 | self.io_sock.send(data) 522 | if traceEnabled: 523 | logger.debug("send: " + repr(data)) 524 | 525 | def ping(self, payload = ""): 526 | """ 527 | send ping data. 528 | 529 | payload: data payload to send server. 530 | """ 531 | self.send(payload, ABNF.OPCODE_PING) 532 | 533 | def pong(self, payload): 534 | """ 535 | send pong data. 536 | 537 | payload: data payload to send server. 538 | """ 539 | self.send(payload, ABNF.OPCODE_PONG) 540 | 541 | def recv(self): 542 | """ 543 | Receive string data(byte array) from the server. 544 | 545 | return value: string(byte array) value. 546 | """ 547 | opcode, data = self.recv_data() 548 | return data 549 | 550 | def recv_data(self): 551 | """ 552 | Recieve data with operation code. 553 | 554 | return value: tuple of operation code and string(byte array) value. 555 | """ 556 | while True: 557 | frame = self.recv_frame() 558 | if not frame: 559 | # handle error: 560 | # 'NoneType' object has no attribute 'opcode' 561 | raise WebSocketException("Not a valid frame %s" % frame) 562 | elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): 563 | return (frame.opcode, frame.data) 564 | elif frame.opcode == ABNF.OPCODE_CLOSE: 565 | self.send_close() 566 | return (frame.opcode, None) 567 | elif frame.opcode == ABNF.OPCODE_PING: 568 | self.pong(frame.data) 569 | 570 | def recv_frame(self): 571 | """ 572 | recieve data as frame from server. 573 | 574 | return value: ABNF frame object. 575 | """ 576 | header_bytes = self._recv_strict(2) 577 | if not header_bytes: 578 | return None 579 | b1 = ord(header_bytes[0]) 580 | fin = b1 >> 7 & 1 581 | rsv1 = b1 >> 6 & 1 582 | rsv2 = b1 >> 5 & 1 583 | rsv3 = b1 >> 4 & 1 584 | opcode = b1 & 0xf 585 | b2 = ord(header_bytes[1]) 586 | mask = b2 >> 7 & 1 587 | length = b2 & 0x7f 588 | 589 | length_data = "" 590 | if length == 0x7e: 591 | length_data = self._recv_strict(2) 592 | length = struct.unpack("!H", length_data)[0] 593 | elif length == 0x7f: 594 | length_data = self._recv_strict(8) 595 | length = struct.unpack("!Q", length_data)[0] 596 | 597 | mask_key = "" 598 | if mask: 599 | mask_key = self._recv_strict(4) 600 | data = self._recv_strict(length) 601 | if traceEnabled: 602 | recieved = header_bytes + length_data + mask_key + data 603 | logger.debug("recv: " + repr(recieved)) 604 | 605 | if mask: 606 | data = ABNF.mask(mask_key, data) 607 | 608 | frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, mask, data) 609 | return frame 610 | 611 | def send_close(self, status = STATUS_NORMAL, reason = ""): 612 | """ 613 | send close data to the server. 614 | 615 | status: status code to send. see STATUS_XXX. 616 | 617 | reason: the reason to close. This must be string. 618 | """ 619 | if status < 0 or status >= ABNF.LENGTH_16: 620 | raise ValueError("code is invalid range") 621 | self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) 622 | 623 | def close(self, status = STATUS_NORMAL, reason = ""): 624 | """ 625 | Close Websocket object 626 | 627 | status: status code to send. see STATUS_XXX. 628 | 629 | reason: the reason to close. This must be string. 630 | """ 631 | if self.connected: 632 | if status < 0 or status >= ABNF.LENGTH_16: 633 | raise ValueError("code is invalid range") 634 | 635 | try: 636 | self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) 637 | timeout = self.sock.gettimeout() 638 | self.sock.settimeout(3) 639 | try: 640 | frame = self.recv_frame() 641 | if logger.isEnabledFor(logging.DEBUG): 642 | logger.error("close status: " + repr(frame.data)) 643 | except: 644 | pass 645 | self.sock.settimeout(timeout) 646 | self.sock.shutdown(socket.SHUT_RDWR) 647 | except: 648 | pass 649 | self._closeInternal() 650 | 651 | def _closeInternal(self): 652 | self.connected = False 653 | self.sock.close() 654 | self.io_sock = self.sock 655 | 656 | def _recv(self, bufsize): 657 | bytes = self.io_sock.recv(bufsize) 658 | if not bytes: 659 | raise WebSocketConnectionClosedException() 660 | return bytes 661 | 662 | def _recv_strict(self, bufsize): 663 | remaining = bufsize 664 | bytes = "" 665 | while remaining: 666 | bytes += self._recv(remaining) 667 | remaining = bufsize - len(bytes) 668 | 669 | return bytes 670 | 671 | def _recv_line(self): 672 | line = [] 673 | while True: 674 | c = self._recv(1) 675 | line.append(c) 676 | if c == "\n": 677 | break 678 | return "".join(line) 679 | 680 | 681 | class WebSocketApp(object): 682 | """ 683 | Higher level of APIs are provided. 684 | The interface is like JavaScript WebSocket object. 685 | """ 686 | def __init__(self, url, 687 | on_open = None, on_message = None, on_error = None, 688 | on_close = None, keep_running = True, get_mask_key = None): 689 | """ 690 | url: websocket url. 691 | on_open: callable object which is called at opening websocket. 692 | this function has one argument. The arugment is this class object. 693 | on_message: callbale object which is called when recieved data. 694 | on_message has 2 arguments. 695 | The 1st arugment is this class object. 696 | The passing 2nd arugment is utf-8 string which we get from the server. 697 | on_error: callable object which is called when we get error. 698 | on_error has 2 arguments. 699 | The 1st arugment is this class object. 700 | The passing 2nd arugment is exception object. 701 | on_close: callable object which is called when closed the connection. 702 | this function has one argument. The arugment is this class object. 703 | keep_running: a boolean flag indicating whether the app's main loop should 704 | keep running, defaults to True 705 | get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's 706 | docstring for more information 707 | """ 708 | self.url = url 709 | self.on_open = on_open 710 | self.on_message = on_message 711 | self.on_error = on_error 712 | self.on_close = on_close 713 | self.keep_running = keep_running 714 | self.get_mask_key = get_mask_key 715 | self.sock = None 716 | 717 | def send(self, data, opcode = ABNF.OPCODE_TEXT): 718 | """ 719 | send message. 720 | data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode. 721 | opcode: operation code of data. default is OPCODE_TEXT. 722 | """ 723 | if self.sock.send(data, opcode) == 0: 724 | raise WebSocketConnectionClosedException() 725 | 726 | def close(self): 727 | """ 728 | close websocket connection. 729 | """ 730 | self.keep_running = False 731 | self.sock.close() 732 | 733 | def run_forever(self): 734 | """ 735 | run event loop for WebSocket framework. 736 | This loop is infinite loop and is alive during websocket is available. 737 | """ 738 | if self.sock: 739 | raise WebSocketException("socket is already opened") 740 | try: 741 | self.sock = WebSocket(self.get_mask_key) 742 | self.sock.connect(self.url) 743 | self._run_with_no_err(self.on_open) 744 | while self.keep_running: 745 | data = self.sock.recv() 746 | if data is None: 747 | break 748 | self._run_with_no_err(self.on_message, data) 749 | except Exception, e: 750 | self._run_with_no_err(self.on_error, e) 751 | finally: 752 | self.sock.close() 753 | self._run_with_no_err(self.on_close) 754 | self.sock = None 755 | 756 | def _run_with_no_err(self, callback, *args): 757 | if callback: 758 | try: 759 | callback(self, *args) 760 | except Exception, e: 761 | if logger.isEnabledFor(logging.DEBUG): 762 | logger.error(e) 763 | 764 | 765 | if __name__ == "__main__": 766 | enableTrace(True) 767 | ws = create_connection("ws://echo.websocket.org/") 768 | print "Sending 'Hello, World'..." 769 | ws.send("Hello, World") 770 | print "Sent" 771 | print "Receiving..." 772 | result = ws.recv() 773 | print "Received '%s'" % result 774 | ws.close() 775 | -------------------------------------------------------------------------------- /goxtool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | """ 4 | Tool to display live MtGox market info and 5 | framework for experimenting with trading bots 6 | """ 7 | # Copyright (c) 2013 Bernd Kreuss 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 22 | # MA 02110-1301, USA. 23 | 24 | # pylint: disable=C0301,C0302,R0902,R0903,R0912,R0913,R0914,R0915,R0922,W0703 25 | 26 | import argparse 27 | import curses 28 | import curses.panel 29 | import curses.textpad 30 | import goxapi 31 | import logging 32 | import locale 33 | import math 34 | import os 35 | import sys 36 | import time 37 | import traceback 38 | import threading 39 | 40 | sys_out = sys.stdout #pylint: disable=C0103 41 | 42 | # 43 | # 44 | # curses user interface 45 | # 46 | 47 | HEIGHT_STATUS = 2 48 | HEIGHT_CON = 7 49 | WIDTH_ORDERBOOK = 45 50 | 51 | COLORS = [["con_text", curses.COLOR_BLUE, curses.COLOR_CYAN] 52 | ,["con_text_buy", curses.COLOR_BLUE, curses.COLOR_GREEN] 53 | ,["con_text_sell", curses.COLOR_BLUE, curses.COLOR_RED] 54 | ,["status_text", curses.COLOR_BLUE, curses.COLOR_CYAN] 55 | 56 | ,["book_text", curses.COLOR_BLACK, curses.COLOR_CYAN] 57 | ,["book_bid", curses.COLOR_BLACK, curses.COLOR_GREEN] 58 | ,["book_ask", curses.COLOR_BLACK, curses.COLOR_RED] 59 | ,["book_own", curses.COLOR_BLACK, curses.COLOR_YELLOW] 60 | ,["book_vol", curses.COLOR_BLACK, curses.COLOR_CYAN] 61 | 62 | ,["chart_text", curses.COLOR_BLACK, curses.COLOR_WHITE] 63 | ,["chart_up", curses.COLOR_BLACK, curses.COLOR_GREEN] 64 | ,["chart_down", curses.COLOR_BLACK, curses.COLOR_RED] 65 | ,["order_pending", curses.COLOR_BLACK, curses.COLOR_RED] 66 | 67 | ,["dialog_text", curses.COLOR_BLUE, curses.COLOR_CYAN] 68 | ,["dialog_sel", curses.COLOR_CYAN, curses.COLOR_BLUE] 69 | ,["dialog_sel_text", curses.COLOR_BLUE, curses.COLOR_YELLOW] 70 | ,["dialog_sel_sel", curses.COLOR_YELLOW, curses.COLOR_BLUE] 71 | ,["dialog_bid_text", curses.COLOR_GREEN, curses.COLOR_BLACK] 72 | ,["dialog_ask_text", curses.COLOR_RED, curses.COLOR_WHITE] 73 | ] 74 | 75 | INI_DEFAULTS = [["goxtool", "set_xterm_title", "True"] 76 | ,["goxtool", "dont_truncate_logfile", "False"] 77 | ,["goxtool", "show_orderbook_stats", "True"] 78 | ,["goxtool", "highlight_changes", "True"] 79 | ,["goxtool", "orderbook_group", "0"] 80 | ,["goxtool", "orderbook_sum_total", "False"] 81 | ,["goxtool", "display_right", "history_chart"] 82 | ,["goxtool", "depth_chart_group", "1"] 83 | ,["goxtool", "depth_chart_sum_total", "True"] 84 | ,["goxtool", "show_ticker", "True"] 85 | ,["goxtool", "show_depth", "True"] 86 | ,["goxtool", "show_trade", "True"] 87 | ,["goxtool", "show_trade_own", "True"] 88 | ] 89 | 90 | COLOR_PAIR = {} 91 | 92 | def init_colors(): 93 | """initialize curses color pairs and give them names. The color pair 94 | can then later quickly be retrieved from the COLOR_PAIR[] dict""" 95 | index = 1 96 | for (name, back, fore) in COLORS: 97 | if curses.has_colors(): 98 | curses.init_pair(index, fore, back) 99 | COLOR_PAIR[name] = curses.color_pair(index) 100 | else: 101 | COLOR_PAIR[name] = 0 102 | index += 1 103 | 104 | def dump_all_stacks(): 105 | """dump a stack trace for all running threads for debugging purpose""" 106 | 107 | def get_name(thread_id): 108 | """return the human readable name that was assigned to a thread""" 109 | for thread in threading.enumerate(): 110 | if thread.ident == thread_id: 111 | return thread.name 112 | 113 | ret = "\n# Full stack trace of all running threads:\n" 114 | #pylint: disable=W0212 115 | for thread_id, stack in sys._current_frames().items(): 116 | ret += "\n# %s (%s)\n" % (get_name(thread_id), thread_id) 117 | for filename, lineno, name, line in traceback.extract_stack(stack): 118 | ret += 'File: "%s", line %d, in %s\n' % (filename, lineno, name) 119 | if line: 120 | ret += " %s\n" % (line.strip()) 121 | return ret 122 | 123 | def try_get_lock_or_break_open(): 124 | """this is an ugly hack to workaround possible deadlock problems. 125 | It is used during shutdown to make sure we can properly exit even when 126 | some slot is stuck (due to a programming error) and won't release the lock. 127 | If we can't acquire it within 2 seconds we just break it open forcefully.""" 128 | #pylint: disable=W0212 129 | time_end = time.time() + 2 130 | while time.time() < time_end: 131 | if goxapi.Signal._lock.acquire(False): 132 | return 133 | time.sleep(0.001) 134 | 135 | # something keeps holding the lock, apparently some slot is stuck 136 | # in an infinite loop. In order to be able to shut down anyways 137 | # we just throw away that lock and replace it with a new one 138 | lock = threading.RLock() 139 | lock.acquire() 140 | goxapi.Signal._lock = lock 141 | print "### could not acquire signal lock, frozen slot somewhere?" 142 | print "### please see the stacktrace log to determine the cause." 143 | 144 | class Win: 145 | """represents a curses window""" 146 | # pylint: disable=R0902 147 | 148 | def __init__(self, stdscr): 149 | """create and initialize the window. This will also subsequently 150 | call the paint() method.""" 151 | self.stdscr = stdscr 152 | self.posx = 0 153 | self.posy = 0 154 | self.width = 10 155 | self.height = 10 156 | self.termwidth = 10 157 | self.termheight = 10 158 | self.win = None 159 | self.panel = None 160 | self.__create_win() 161 | 162 | def __del__(self): 163 | del self.panel 164 | del self.win 165 | curses.panel.update_panels() 166 | curses.doupdate() 167 | 168 | def calc_size(self): 169 | """override this method to change posx, posy, width, height. 170 | It will be called before window creation and on resize.""" 171 | pass 172 | 173 | def do_paint(self): 174 | """call this if you want the window to repaint itself""" 175 | curses.curs_set(0) 176 | if self.win: 177 | self.paint() 178 | self.done_paint() 179 | 180 | # method could be a function - pylint: disable=R0201 181 | def done_paint(self): 182 | """update the sreen after paint operations, this will invoke all 183 | necessary stuff to refresh all (possibly overlapping) windows in 184 | the right order and then push it to the screen""" 185 | curses.panel.update_panels() 186 | curses.doupdate() 187 | 188 | def paint(self): 189 | """paint the window. Override this with your own implementation. 190 | This method must paint the entire window contents from scratch. 191 | It is automatically called after the window has been initially 192 | created and also after every resize. Call it explicitly when 193 | your data has changed and must be displayed""" 194 | pass 195 | 196 | def resize(self): 197 | """You must call this method from your main loop when the 198 | terminal has been resized. It will subsequently make it 199 | recalculate its own new size and then call its paint() method""" 200 | del self.win 201 | self.__create_win() 202 | 203 | def addstr(self, *args): 204 | """drop-in replacement for addstr that will never raie exceptions 205 | and that will cut off at end of line instead of wrapping""" 206 | if len(args) > 0: 207 | line, col = self.win.getyx() 208 | string = args[0] 209 | attr = 0 210 | if len(args) > 1: 211 | attr = args[1] 212 | if len(args) > 2: 213 | line, col, string = args[:3] 214 | attr = 0 215 | if len(args) > 3: 216 | attr = args[3] 217 | if line >= self.height: 218 | return 219 | space_left = self.width - col - 1 #always omit last column, avoids problems. 220 | if space_left <= 0: 221 | return 222 | self.win.addstr(line, col, string[:space_left], attr) 223 | 224 | def addch(self, posy, posx, character, color_pair): 225 | """place a character but don't throw error in lower right corner""" 226 | if posy < 0 or posy > self.height - 1: 227 | return 228 | if posx < 0 or posx > self.width - 1: 229 | return 230 | if posx == self.width - 1 and posy == self.height - 1: 231 | return 232 | self.win.addch(posy, posx, character, color_pair) 233 | 234 | def __create_win(self): 235 | """create the window. This will also be called on every resize, 236 | windows won't be moved, they will be deleted and recreated.""" 237 | self.__calc_size() 238 | try: 239 | self.win = curses.newwin(self.height, self.width, self.posy, self.posx) 240 | self.panel = curses.panel.new_panel(self.win) 241 | self.win.scrollok(True) 242 | self.win.keypad(1) 243 | self.do_paint() 244 | except Exception: 245 | self.win = None 246 | self.panel = None 247 | 248 | def __calc_size(self): 249 | """calculate the default values for positionand size. By default 250 | this will result in a window covering the entire terminal. 251 | Implement the calc_size() method (which will be called afterwards) 252 | to change (some of) these values according to your needs.""" 253 | maxyx = self.stdscr.getmaxyx() 254 | self.termwidth = maxyx[1] 255 | self.termheight = maxyx[0] 256 | self.posx = 0 257 | self.posy = 0 258 | self.width = self.termwidth 259 | self.height = self.termheight 260 | self.calc_size() 261 | 262 | 263 | class WinConsole(Win): 264 | """The console window at the bottom""" 265 | def __init__(self, stdscr, gox): 266 | """create the console window and connect it to the Gox debug 267 | callback function""" 268 | self.gox = gox 269 | gox.signal_debug.connect(self.slot_debug) 270 | Win.__init__(self, stdscr) 271 | 272 | def paint(self): 273 | """just empty the window after resize (I am lazy)""" 274 | self.win.bkgd(" ", COLOR_PAIR["con_text"]) 275 | 276 | def resize(self): 277 | """resize and print a log message. Old messages will have been 278 | lost after resize because of my dumb paint() implementation, so 279 | at least print a message indicating that fact into the 280 | otherwise now empty console window""" 281 | Win.resize(self) 282 | self.write("### console has been resized") 283 | 284 | def calc_size(self): 285 | """put it at the bottom of the screen""" 286 | self.height = HEIGHT_CON 287 | self.posy = self.termheight - self.height 288 | 289 | def slot_debug(self, dummy_gox, (txt)): 290 | """this slot will be connected to all debug signals.""" 291 | self.write(txt) 292 | 293 | def write(self, txt): 294 | """write a line of text, scroll if needed""" 295 | if not self.win: 296 | return 297 | 298 | # This code would break if the format of 299 | # the log messages would ever change! 300 | if " tick:" in txt: 301 | if not self.gox.config.get_bool("goxtool", "show_ticker"): 302 | return 303 | if "depth:" in txt: 304 | if not self.gox.config.get_bool("goxtool", "show_depth"): 305 | return 306 | if "trade:" in txt: 307 | if "own order" in txt: 308 | if not self.gox.config.get_bool("goxtool", "show_trade_own"): 309 | return 310 | else: 311 | if not self.gox.config.get_bool("goxtool", "show_trade"): 312 | return 313 | 314 | col = COLOR_PAIR["con_text"] 315 | if "trade: bid:" in txt: 316 | col = COLOR_PAIR["con_text_buy"] + curses.A_BOLD 317 | if "trade: ask:" in txt: 318 | col = COLOR_PAIR["con_text_sell"] + curses.A_BOLD 319 | self.win.addstr("\n" + txt, col) 320 | self.done_paint() 321 | 322 | 323 | class WinOrderBook(Win): 324 | """the orderbook window""" 325 | 326 | def __init__(self, stdscr, gox): 327 | """create the orderbook window and connect it to the 328 | onChanged callback of the gox.orderbook instance""" 329 | self.gox = gox 330 | gox.orderbook.signal_changed.connect(self.slot_changed) 331 | Win.__init__(self, stdscr) 332 | 333 | def calc_size(self): 334 | """put it into the middle left side""" 335 | self.height = self.termheight - HEIGHT_CON - HEIGHT_STATUS 336 | self.posy = HEIGHT_STATUS 337 | self.width = WIDTH_ORDERBOOK 338 | 339 | def paint(self): 340 | """paint the visible portion of the orderbook""" 341 | 342 | def paint_row(pos, price, vol, ownvol, color, changevol): 343 | """paint a row in the orderbook (bid or ask)""" 344 | if changevol > 0: 345 | col2 = col_bid + curses.A_BOLD 346 | elif changevol < 0: 347 | col2 = col_ask + curses.A_BOLD 348 | else: 349 | col2 = col_vol 350 | self.addstr(pos, 0, book.gox.quote2str(price), color) 351 | self.addstr(pos, 12, book.gox.base2str(vol), col2) 352 | if ownvol: 353 | self.addstr(pos, 28, book.gox.base2str(ownvol), col_own) 354 | 355 | self.win.bkgd(" ", COLOR_PAIR["book_text"]) 356 | self.win.erase() 357 | 358 | gox = self.gox 359 | book = gox.orderbook 360 | 361 | mid = self.height / 2 362 | col_bid = COLOR_PAIR["book_bid"] 363 | col_ask = COLOR_PAIR["book_ask"] 364 | col_vol = COLOR_PAIR["book_vol"] 365 | col_own = COLOR_PAIR["book_own"] 366 | 367 | sum_total = gox.config.get_bool("goxtool", "orderbook_sum_total") 368 | group = gox.config.get_float("goxtool", "orderbook_group") 369 | group = gox.quote2int(group) 370 | if group == 0: 371 | group = 1 372 | 373 | # 374 | # 375 | # paint the asks (first we put them into bins[] then we paint them) 376 | # 377 | if len(book.asks): 378 | i = 0 379 | bins = [] 380 | pos = mid - 1 381 | vol = 0 382 | prev_vol = 0 383 | 384 | # no grouping, bins can be created in one simple and fast loop 385 | if group == 1: 386 | cnt = len(book.asks) 387 | while pos >= 0 and i < cnt: 388 | level = book.asks[i] 389 | price = level.price 390 | if sum_total: 391 | vol += level.volume 392 | else: 393 | vol = level.volume 394 | ownvol = level.own_volume 395 | bins.append([pos, price, vol, ownvol, 0]) 396 | pos -= 1 397 | i += 1 398 | 399 | # with gouping its a bit more complicated 400 | else: 401 | # first bin is exact lowest ask price 402 | price = book.asks[0].price 403 | vol = book.asks[0].volume 404 | bins.append([pos, price, vol, 0, 0]) 405 | prev_vol = vol 406 | pos -= 1 407 | 408 | # now all following bins 409 | bin_price = int(math.ceil(float(price) / group) * group) 410 | if bin_price == price: 411 | # first level was exact bin price already, skip to next bin 412 | bin_price += group 413 | while pos >= 0 and bin_price < book.asks[-1].price + group: 414 | vol, _vol_quote = book.get_total_up_to(bin_price, True) ## 01 freeze 415 | if vol > prev_vol: 416 | # append only non-empty bins 417 | if sum_total: 418 | bins.append([pos, bin_price, vol, 0, 0]) 419 | else: 420 | bins.append([pos, bin_price, vol - prev_vol, 0, 0]) 421 | prev_vol = vol 422 | pos -= 1 423 | bin_price += group 424 | 425 | # now add the own volumes to their bins 426 | for order in book.owns: 427 | if order.typ == "ask" and order.price > 0: 428 | order_bin_price = int(math.ceil(float(order.price) / group) * group) 429 | for abin in bins: 430 | if abin[1] == order.price: 431 | abin[3] += order.volume 432 | break 433 | if abin[1] == order_bin_price: 434 | abin[3] += order.volume 435 | break 436 | 437 | # mark the level where change took place (optional) 438 | if gox.config.get_bool("goxtool", "highlight_changes"): 439 | if book.last_change_type == "ask": 440 | change_bin_price = int(math.ceil(float(book.last_change_price) / group) * group) 441 | for abin in bins: 442 | if abin[1] == book.last_change_price: 443 | abin[4] = book.last_change_volume 444 | break 445 | if abin[1] == change_bin_price: 446 | abin[4] = book.last_change_volume 447 | break 448 | 449 | # now finally paint the asks 450 | for pos, price, vol, ownvol, changevol in bins: 451 | paint_row(pos, price, vol, ownvol, col_ask, changevol) 452 | 453 | # 454 | # 455 | # paint the bids (first we put them into bins[] then we paint them) 456 | # 457 | if len(book.bids): 458 | i = 0 459 | bins = [] 460 | pos = mid + 1 461 | vol = 0 462 | prev_vol = 0 463 | 464 | # no grouping, bins can be created in one simple and fast loop 465 | if group == 1: 466 | cnt = len(book.bids) 467 | while pos < self.height and i < cnt: 468 | level = book.bids[i] 469 | price = level.price 470 | if sum_total: 471 | vol += level.volume 472 | else: 473 | vol = level.volume 474 | ownvol = level.own_volume 475 | bins.append([pos, price, vol, ownvol, 0]) 476 | prev_vol = vol 477 | pos += 1 478 | i += 1 479 | 480 | # with gouping its a bit more complicated 481 | else: 482 | # first bin is exact lowest ask price 483 | price = book.bids[0].price 484 | vol = book.bids[0].volume 485 | bins.append([pos, price, vol, 0, 0]) 486 | prev_vol = vol 487 | pos += 1 488 | 489 | # now all following bins 490 | bin_price = int(math.floor(float(price) / group) * group) 491 | if bin_price == price: 492 | # first level was exact bin price already, skip to next bin 493 | bin_price -= group 494 | while pos < self.height and bin_price >= 0: 495 | vol, _vol_quote = book.get_total_up_to(bin_price, False) 496 | if vol > prev_vol: 497 | # append only non-empty bins 498 | if sum_total: 499 | bins.append([pos, bin_price, vol, 0, 0]) 500 | else: 501 | bins.append([pos, bin_price, vol - prev_vol, 0, 0]) 502 | prev_vol = vol 503 | pos += 1 504 | bin_price -= group 505 | 506 | # now add the own volumes to their bins 507 | for order in book.owns: 508 | if order.typ == "bid" and order.price > 0: 509 | order_bin_price = int(math.floor(float(order.price) / group) * group) 510 | for abin in bins: 511 | if abin[1] == order.price: 512 | abin[3] += order.volume 513 | break 514 | if abin[1] == order_bin_price: 515 | abin[3] += order.volume 516 | break 517 | 518 | # mark the level where change took place (optional) 519 | if gox.config.get_bool("goxtool", "highlight_changes"): 520 | if book.last_change_type == "bid": 521 | change_bin_price = int(math.floor(float(book.last_change_price) / group) * group) 522 | for abin in bins: 523 | if abin[1] == book.last_change_price: 524 | abin[4] = book.last_change_volume 525 | break 526 | if abin[1] == change_bin_price: 527 | abin[4] = book.last_change_volume 528 | break 529 | 530 | # now finally paint the bids 531 | for pos, price, vol, ownvol, changevol in bins: 532 | paint_row(pos, price, vol, ownvol, col_bid, changevol) 533 | 534 | # update the xterm title bar 535 | if self.gox.config.get_bool("goxtool", "set_xterm_title"): 536 | last_candle = self.gox.history.last_candle() 537 | if last_candle: 538 | title = self.gox.quote2str(last_candle.cls).strip() 539 | title += " - goxtool -" 540 | title += " bid:" + self.gox.quote2str(book.bid).strip() 541 | title += " ask:" + self.gox.quote2str(book.ask).strip() 542 | 543 | term = os.environ["TERM"] 544 | # the following is incomplete but better safe than sorry 545 | # if you know more terminals then please provide a patch 546 | if "xterm" in term or "rxvt" in term: 547 | sys_out.write("\x1b]0;%s\x07" % title) 548 | sys_out.flush() 549 | 550 | def slot_changed(self, _book, _dummy): 551 | """Slot for orderbook.signal_changed""" 552 | self.do_paint() 553 | 554 | 555 | TYPE_HISTORY = 1 556 | TYPE_ORDERBOOK = 2 557 | 558 | class WinChart(Win): 559 | """the chart window""" 560 | 561 | def __init__(self, stdscr, gox): 562 | self.gox = gox 563 | self.pmin = 0 564 | self.pmax = 0 565 | self.change_type = None 566 | gox.history.signal_changed.connect(self.slot_history_changed) 567 | gox.orderbook.signal_changed.connect(self.slot_orderbook_changed) 568 | 569 | # some terminals do not support reverse video 570 | # so we cannot use reverse space for candle bodies 571 | if curses.A_REVERSE & curses.termattrs(): 572 | self.body_char = " " 573 | self.body_attr = curses.A_REVERSE 574 | else: 575 | self.body_char = curses.ACS_CKBOARD # pylint: disable=E1101 576 | self.body_attr = 0 577 | 578 | Win.__init__(self, stdscr) 579 | 580 | def calc_size(self): 581 | """position in the middle, right to the orderbook""" 582 | self.posx = WIDTH_ORDERBOOK 583 | self.posy = HEIGHT_STATUS 584 | self.width = self.termwidth - WIDTH_ORDERBOOK 585 | self.height = self.termheight - HEIGHT_CON - HEIGHT_STATUS 586 | 587 | def is_in_range(self, price): 588 | """is this price in the currently visible range?""" 589 | return price <= self.pmax and price >= self.pmin 590 | 591 | def get_optimal_step(self, num_min): 592 | """return optimal step size for painting y-axis labels so that the 593 | range will be divided into at least num_min steps""" 594 | if self.pmax <= self.pmin: 595 | return None 596 | stepex = float(self.pmax - self.pmin) / num_min 597 | step1 = math.pow(10, math.floor(math.log(stepex, 10))) 598 | step2 = step1 * 2 599 | step5 = step1 * 5 600 | if step5 <= stepex: 601 | return step5 602 | if step2 <= stepex: 603 | return step2 604 | return step1 605 | 606 | def price_to_screen(self, price): 607 | """convert price into screen coordinates (y=0 is at the top!)""" 608 | relative_from_bottom = \ 609 | float(price - self.pmin) / float(self.pmax - self.pmin) 610 | screen_from_bottom = relative_from_bottom * self.height 611 | return int(self.height - screen_from_bottom) 612 | 613 | def paint_y_label(self, posy, posx, price): 614 | """paint the y label of the history chart, formats the number 615 | so that it needs not more room than necessary but it also uses 616 | pmax to determine how many digits are needed so that all numbers 617 | will be nicely aligned at the decimal point""" 618 | 619 | fprice = self.gox.quote2float(price) 620 | labelstr = ("%f" % fprice).rstrip("0").rstrip(".") 621 | 622 | # look at pmax to determine the max number of digits before the decimal 623 | # and then pad all smaller prices with spaces to make them align nicely. 624 | need_digits = int(math.log10(self.gox.quote2float(self.pmax))) + 1 625 | have_digits = len(str(int(fprice))) 626 | if have_digits < need_digits: 627 | padding = " " * (need_digits - have_digits) 628 | labelstr = padding + labelstr 629 | 630 | self.addstr( 631 | posy, posx, 632 | labelstr, 633 | COLOR_PAIR["chart_text"] 634 | ) 635 | 636 | def paint_candle(self, posx, candle): 637 | """paint a single candle""" 638 | 639 | sopen = self.price_to_screen(candle.opn) 640 | shigh = self.price_to_screen(candle.hig) 641 | slow = self.price_to_screen(candle.low) 642 | sclose = self.price_to_screen(candle.cls) 643 | 644 | for posy in range(self.height): 645 | if posy >= shigh and posy < sopen and posy < sclose: 646 | # upper wick 647 | # pylint: disable=E1101 648 | self.addch(posy, posx, curses.ACS_VLINE, COLOR_PAIR["chart_text"]) 649 | if posy >= sopen and posy < sclose: 650 | # red body 651 | self.addch(posy, posx, self.body_char, self.body_attr + COLOR_PAIR["chart_down"]) 652 | if posy >= sclose and posy < sopen: 653 | # green body 654 | self.addch(posy, posx, self.body_char, self.body_attr + COLOR_PAIR["chart_up"]) 655 | if posy >= sopen and posy >= sclose and posy < slow: 656 | # lower wick 657 | # pylint: disable=E1101 658 | self.addch(posy, posx, curses.ACS_VLINE, COLOR_PAIR["chart_text"]) 659 | 660 | def paint(self): 661 | typ = self.gox.config.get_string("goxtool", "display_right") 662 | if typ == "history_chart": 663 | self.paint_history_chart() 664 | elif typ == "depth_chart": 665 | self.paint_depth_chart() 666 | else: 667 | self.paint_history_chart() 668 | 669 | def paint_depth_chart(self): 670 | """paint a depth chart""" 671 | 672 | # pylint: disable=C0103 673 | if self.gox.curr_quote in "JPY SEK": 674 | BAR_LEFT_EDGE = 7 675 | FORMAT_STRING = "%6.0f" 676 | else: 677 | BAR_LEFT_EDGE = 8 678 | FORMAT_STRING = "%7.2f" 679 | 680 | def paint_depth(pos, price, vol, own, col_price, change): 681 | """paint one row of the depth chart""" 682 | if change > 0: 683 | col = col_bid + curses.A_BOLD 684 | elif change < 0: 685 | col = col_ask + curses.A_BOLD 686 | else: 687 | col = col_bar 688 | pricestr = FORMAT_STRING % self.gox.quote2float(price) 689 | self.addstr(pos, 0, pricestr, col_price) 690 | length = int(vol * mult_x) 691 | # pylint: disable=E1101 692 | self.win.hline(pos, BAR_LEFT_EDGE, curses.ACS_CKBOARD, length, col) 693 | if own: 694 | self.addstr(pos, length + BAR_LEFT_EDGE, "o", col_own) 695 | 696 | self.win.bkgd(" ", COLOR_PAIR["chart_text"]) 697 | self.win.erase() 698 | 699 | book = self.gox.orderbook 700 | if not (book.bid and book.ask and len(book.bids) and len(book.asks)): 701 | # orderbook is not initialized yet, paint nothing 702 | return 703 | 704 | col_bar = COLOR_PAIR["book_vol"] 705 | col_bid = COLOR_PAIR["book_bid"] 706 | col_ask = COLOR_PAIR["book_ask"] 707 | col_own = COLOR_PAIR["book_own"] 708 | 709 | group = self.gox.config.get_float("goxtool", "depth_chart_group") 710 | if group == 0: 711 | group = 1 712 | group = self.gox.quote2int(group) 713 | 714 | max_vol_ask = 0 715 | max_vol_bid = 0 716 | bin_asks = [] 717 | bin_bids = [] 718 | mid = self.height / 2 719 | sum_total = self.gox.config.get_bool("goxtool", "depth_chart_sum_total") 720 | 721 | # 722 | # 723 | # bin the asks 724 | # 725 | pos = mid - 1 726 | prev_vol = 0 727 | bin_price = int(math.ceil(float(book.asks[0].price) / group) * group) 728 | while pos >= 0 and bin_price < book.asks[-1].price + group: 729 | bin_vol, _bin_vol_quote = book.get_total_up_to(bin_price, True) 730 | if bin_vol > prev_vol: 731 | # add only non-empty bins 732 | if sum_total: 733 | bin_asks.append([pos, bin_price, bin_vol, 0, 0]) 734 | max_vol_ask = max(bin_vol, max_vol_ask) 735 | else: 736 | bin_asks.append([pos, bin_price, bin_vol - prev_vol, 0, 0]) 737 | max_vol_ask = max(bin_vol - prev_vol, max_vol_ask) 738 | prev_vol = bin_vol 739 | pos -= 1 740 | bin_price += group 741 | 742 | # 743 | # 744 | # bin the bids 745 | # 746 | pos = mid + 1 747 | prev_vol = 0 748 | bin_price = int(math.floor(float(book.bids[0].price) / group) * group) 749 | while pos < self.height and bin_price >= 0: 750 | _bin_vol_base, bin_vol_quote = book.get_total_up_to(bin_price, False) 751 | bin_vol = self.gox.base2int(bin_vol_quote / book.bid) 752 | if bin_vol > prev_vol: 753 | # add only non-empty bins 754 | if sum_total: 755 | bin_bids.append([pos, bin_price, bin_vol, 0, 0]) 756 | max_vol_bid = max(bin_vol, max_vol_bid) 757 | else: 758 | bin_bids.append([pos, bin_price, bin_vol - prev_vol, 0, 0]) 759 | max_vol_bid = max(bin_vol - prev_vol, max_vol_bid) 760 | prev_vol = bin_vol 761 | pos += 1 762 | bin_price -= group 763 | 764 | max_vol_tot = max(max_vol_ask, max_vol_bid) 765 | if not max_vol_tot: 766 | return 767 | mult_x = float(self.width - BAR_LEFT_EDGE - 2) / max_vol_tot 768 | 769 | # add the own volume to the bins 770 | for order in book.owns: 771 | if order.price > 0: 772 | if order.typ == "ask": 773 | bin_price = int(math.ceil(float(order.price) / group) * group) 774 | for abin in bin_asks: 775 | if abin[1] == bin_price: 776 | abin[3] += order.volume 777 | break 778 | else: 779 | bin_price = int(math.floor(float(order.price) / group) * group) 780 | for abin in bin_bids: 781 | if abin[1] == bin_price: 782 | abin[3] += order.volume 783 | break 784 | 785 | # highlight the relative change (optional) 786 | if self.gox.config.get_bool("goxtool", "highlight_changes"): 787 | price = book.last_change_price 788 | if book.last_change_type == "ask": 789 | bin_price = int(math.ceil(float(price) / group) * group) 790 | for abin in bin_asks: 791 | if abin[1] == bin_price: 792 | abin[4] = book.last_change_volume 793 | break 794 | if book.last_change_type == "bid": 795 | bin_price = int(math.floor(float(price) / group) * group) 796 | for abin in bin_bids: 797 | if abin[1] == bin_price: 798 | abin[4] = book.last_change_volume 799 | break 800 | 801 | # paint the asks 802 | for pos, price, vol, own, change in bin_asks: 803 | paint_depth(pos, price, vol, own, col_ask, change) 804 | 805 | # paint the bids 806 | for pos, price, vol, own, change in bin_bids: 807 | paint_depth(pos, price, vol, own, col_bid, change) 808 | 809 | def paint_history_chart(self): 810 | """paint a history candlestick chart""" 811 | 812 | if self.change_type == TYPE_ORDERBOOK: 813 | # erase only the rightmost column to redraw bid/ask and orders 814 | # beause we won't redraw the chart, its only an orderbook change 815 | self.win.vline(0, self.width - 1, " ", self.height, COLOR_PAIR["chart_text"]) 816 | else: 817 | self.win.bkgd(" ", COLOR_PAIR["chart_text"]) 818 | self.win.erase() 819 | 820 | hist = self.gox.history 821 | book = self.gox.orderbook 822 | 823 | self.pmax = 0 824 | self.pmin = 9999999999 825 | 826 | # determine y range 827 | posx = self.width - 2 828 | index = 0 829 | while index < hist.length() and posx >= 0: 830 | candle = hist.candles[index] 831 | if self.pmax < candle.hig: 832 | self.pmax = candle.hig 833 | if self.pmin > candle.low: 834 | self.pmin = candle.low 835 | index += 1 836 | posx -= 1 837 | 838 | if self.pmax == self.pmin: 839 | return 840 | 841 | # paint the candlestick chart. 842 | # We won't paint it if it was triggered from an orderbook change 843 | # signal because that would be redundant and only waste CPU. 844 | # In that case we only repaint the bid/ask markers (see below) 845 | if self.change_type != TYPE_ORDERBOOK: 846 | # paint the candles 847 | posx = self.width - 2 848 | index = 0 849 | while index < hist.length() and posx >= 0: 850 | candle = hist.candles[index] 851 | self.paint_candle(posx, candle) 852 | index += 1 853 | posx -= 1 854 | 855 | # paint the y-axis labels 856 | posx = 0 857 | step = self.get_optimal_step(4) 858 | if step: 859 | labelprice = int(self.pmin / step) * step 860 | while not labelprice > self.pmax: 861 | posy = self.price_to_screen(labelprice) 862 | if posy < self.height - 1: 863 | self.paint_y_label(posy, posx, labelprice) 864 | labelprice += step 865 | 866 | # paint bid, ask, own orders 867 | posx = self.width - 1 868 | for order in book.owns: 869 | if self.is_in_range(order.price): 870 | posy = self.price_to_screen(order.price) 871 | if order.status == "pending": 872 | self.addch(posy, posx, 873 | ord("p"), COLOR_PAIR["order_pending"]) 874 | else: 875 | self.addch(posy, posx, 876 | ord("o"), COLOR_PAIR["book_own"]) 877 | 878 | if self.is_in_range(book.bid): 879 | posy = self.price_to_screen(book.bid) 880 | # pylint: disable=E1101 881 | self.addch(posy, posx, 882 | curses.ACS_HLINE, COLOR_PAIR["chart_up"]) 883 | 884 | if self.is_in_range(book.ask): 885 | posy = self.price_to_screen(book.ask) 886 | # pylint: disable=E1101 887 | self.addch(posy, posx, 888 | curses.ACS_HLINE, COLOR_PAIR["chart_down"]) 889 | 890 | 891 | def slot_history_changed(self, _sender, _data): 892 | """Slot for history changed""" 893 | self.change_type = TYPE_HISTORY 894 | self.do_paint() 895 | self.change_type = None 896 | 897 | def slot_orderbook_changed(self, _sender, _data): 898 | """Slot for orderbook changed""" 899 | self.change_type = TYPE_ORDERBOOK 900 | self.do_paint() 901 | self.change_type = None 902 | 903 | 904 | class WinStatus(Win): 905 | """the status window at the top""" 906 | 907 | def __init__(self, stdscr, gox): 908 | """create the status window and connect the needed callbacks""" 909 | self.gox = gox 910 | self.order_lag = 0 911 | self.order_lag_txt = "" 912 | self.sorted_currency_list = [] 913 | gox.signal_orderlag.connect(self.slot_orderlag) 914 | gox.signal_wallet.connect(self.slot_changed) 915 | gox.orderbook.signal_changed.connect(self.slot_changed) 916 | Win.__init__(self, stdscr) 917 | 918 | def calc_size(self): 919 | """place it at the top of the terminal""" 920 | self.height = HEIGHT_STATUS 921 | 922 | def sort_currency_list_if_changed(self): 923 | """sort the currency list in the wallet for better display, 924 | sort it only if it has changed, otherwise leave it as it is""" 925 | currency_list = self.gox.wallet.keys() 926 | if len(currency_list) == len(self.sorted_currency_list): 927 | return 928 | 929 | # now we will bring base and quote currency to the front and sort the 930 | # the rest of the list of names by acount balance in descending order 931 | if self.gox.curr_base in currency_list: 932 | currency_list.remove(self.gox.curr_base) 933 | if self.gox.curr_quote in currency_list: 934 | currency_list.remove(self.gox.curr_quote) 935 | currency_list.sort(key=lambda name: -self.gox.wallet[name]) 936 | currency_list.insert(0, self.gox.curr_quote) 937 | currency_list.insert(0, self.gox.curr_base) 938 | self.sorted_currency_list = currency_list 939 | 940 | def paint(self): 941 | """paint the complete status""" 942 | cbase = self.gox.curr_base 943 | cquote = self.gox.curr_quote 944 | self.sort_currency_list_if_changed() 945 | self.win.bkgd(" ", COLOR_PAIR["status_text"]) 946 | self.win.erase() 947 | 948 | # 949 | # first line 950 | # 951 | line1 = "Market: %s%s | " % (cbase, cquote) 952 | line1 += "Account: " 953 | if len(self.sorted_currency_list): 954 | for currency in self.sorted_currency_list: 955 | if currency in self.gox.wallet: 956 | line1 += currency + " " \ 957 | + goxapi.int2str(self.gox.wallet[currency], currency).strip() \ 958 | + " + " 959 | line1 = line1.strip(" +") 960 | else: 961 | line1 += "No info (yet)" 962 | 963 | # 964 | # second line 965 | # 966 | line2 = "" 967 | if self.gox.config.get_bool("goxtool", "show_orderbook_stats"): 968 | str_btc = locale.format('%d', self.gox.orderbook.total_ask, 1) 969 | str_fiat = locale.format('%d', self.gox.orderbook.total_bid, 1) 970 | if self.gox.orderbook.total_ask: 971 | str_ratio = locale.format('%1.2f', 972 | self.gox.orderbook.total_bid / self.gox.orderbook.total_ask, 1) 973 | else: 974 | str_ratio = "-" 975 | 976 | line2 += "sum_bid: %s %s | " % (str_fiat, cquote) 977 | line2 += "sum_ask: %s %s | " % (str_btc, cbase) 978 | line2 += "ratio: %s %s/%s | " % (str_ratio, cquote, cbase) 979 | 980 | line2 += "o_lag: %s | " % self.order_lag_txt 981 | line2 += "s_lag: %.3f s" % (self.gox.socket_lag / 1e6) 982 | self.addstr(0, 0, line1, COLOR_PAIR["status_text"]) 983 | self.addstr(1, 0, line2, COLOR_PAIR["status_text"]) 984 | 985 | 986 | def slot_changed(self, dummy_sender, dummy_data): 987 | """the callback funtion called by the Gox() instance""" 988 | self.do_paint() 989 | 990 | def slot_orderlag(self, dummy_sender, (usec, text)): 991 | """slot for order_lag mesages""" 992 | self.order_lag = usec 993 | self.order_lag_txt = text 994 | self.do_paint() 995 | 996 | 997 | class DlgListItems(Win): 998 | """dialog with a scrollable list of items""" 999 | def __init__(self, stdscr, width, title, hlp, keys): 1000 | self.items = [] 1001 | self.selected = [] 1002 | self.item_top = 0 1003 | self.item_sel = 0 1004 | self.dlg_width = width 1005 | self.dlg_title = title 1006 | self.dlg_hlp = hlp 1007 | self.dlg_keys = keys 1008 | self.reserved_lines = 5 # how many lines NOT used for order list 1009 | self.init_items() 1010 | Win.__init__(self, stdscr) 1011 | 1012 | def init_items(self): 1013 | """initialize the items list, must override and implement this""" 1014 | raise NotImplementedError() 1015 | 1016 | def calc_size(self): 1017 | maxh = self.termheight - 4 1018 | self.height = len(self.items) + self.reserved_lines 1019 | if self.height > maxh: 1020 | self.height = maxh 1021 | self.posy = (self.termheight - self.height) / 2 1022 | 1023 | self.width = self.dlg_width 1024 | self.posx = (self.termwidth - self.width) / 2 1025 | 1026 | def paint_item(self, posx, index): 1027 | """paint the item. Must override and implement this""" 1028 | raise NotImplementedError() 1029 | 1030 | def paint(self): 1031 | self.win.bkgd(" ", COLOR_PAIR["dialog_text"]) 1032 | self.win.erase() 1033 | self.win.border() 1034 | self.addstr(0, 1, " %s " % self.dlg_title, COLOR_PAIR["dialog_text"]) 1035 | index = self.item_top 1036 | posy = 2 1037 | while posy < self.height - 3 and index < len(self.items): 1038 | self.paint_item(posy, index) 1039 | index += 1 1040 | posy += 1 1041 | 1042 | self.win.move(self.height - 2, 2) 1043 | for key, desc in self.dlg_hlp: 1044 | self.addstr(key + " ", COLOR_PAIR["dialog_sel"]) 1045 | self.addstr(desc + " ", COLOR_PAIR["dialog_text"]) 1046 | 1047 | def down(self, num): 1048 | """move the cursor down (or up)""" 1049 | if not len(self.items): 1050 | return 1051 | self.item_sel += num 1052 | if self.item_sel < 0: 1053 | self.item_sel = 0 1054 | if self.item_sel > len(self.items) - 1: 1055 | self.item_sel = len(self.items) - 1 1056 | 1057 | last_line = self.height - 1 - self.reserved_lines 1058 | if self.item_sel < self.item_top: 1059 | self.item_top = self.item_sel 1060 | if self.item_sel - self.item_top > last_line: 1061 | self.item_top = self.item_sel - last_line 1062 | 1063 | self.do_paint() 1064 | 1065 | def toggle_select(self): 1066 | """toggle selection under cursor""" 1067 | if not len(self.items): 1068 | return 1069 | item = self.items[self.item_sel] 1070 | if item in self.selected: 1071 | self.selected.remove(item) 1072 | else: 1073 | self.selected.append(item) 1074 | self.do_paint() 1075 | 1076 | def modal(self): 1077 | """run the modal getch-loop for this dialog""" 1078 | if self.win: 1079 | done = False 1080 | while not done: 1081 | key_pressed = self.win.getch() 1082 | if key_pressed in [27, ord("q"), curses.KEY_F10]: 1083 | done = True 1084 | if key_pressed == curses.KEY_DOWN: 1085 | self.down(1) 1086 | if key_pressed == curses.KEY_UP: 1087 | self.down(-1) 1088 | if key_pressed == curses.KEY_IC: 1089 | self.toggle_select() 1090 | self.down(1) 1091 | 1092 | for key, func in self.dlg_keys: 1093 | if key == key_pressed: 1094 | func() 1095 | done = True 1096 | 1097 | # help the garbage collector clean up circular references 1098 | # to make sure __del__() will be called to close the dialog 1099 | del self.dlg_keys 1100 | 1101 | 1102 | class DlgCancelOrders(DlgListItems): 1103 | """modal dialog to cancel orders""" 1104 | def __init__(self, stdscr, gox): 1105 | self.gox = gox 1106 | hlp = [("INS", "select"), ("F8", "cancel selected"), ("F10", "exit")] 1107 | keys = [(curses.KEY_F8, self._do_cancel)] 1108 | DlgListItems.__init__(self, stdscr, 45, "Cancel order(s)", hlp, keys) 1109 | 1110 | def init_items(self): 1111 | for order in self.gox.orderbook.owns: 1112 | self.items.append(order) 1113 | self.items.sort(key = lambda o: -o.price) 1114 | 1115 | def paint_item(self, posy, index): 1116 | """paint one single order""" 1117 | order = self.items[index] 1118 | if order in self.selected: 1119 | marker = "*" 1120 | if index == self.item_sel: 1121 | attr = COLOR_PAIR["dialog_sel_sel"] 1122 | else: 1123 | attr = COLOR_PAIR["dialog_sel_text"] + curses.A_BOLD 1124 | else: 1125 | marker = "" 1126 | if index == self.item_sel: 1127 | attr = COLOR_PAIR["dialog_sel"] 1128 | else: 1129 | attr = COLOR_PAIR["dialog_text"] 1130 | 1131 | self.addstr(posy, 2, marker, attr) 1132 | self.addstr(posy, 5, order.typ, attr) 1133 | self.addstr(posy, 9, self.gox.quote2str(order.price), attr) 1134 | self.addstr(posy, 22, self.gox.base2str(order.volume), attr) 1135 | 1136 | def _do_cancel(self): 1137 | """cancel all selected orders (or the order under cursor if empty)""" 1138 | 1139 | def do_cancel(order): 1140 | """cancel a single order""" 1141 | self.gox.cancel(order.oid) 1142 | 1143 | if not len(self.items): 1144 | return 1145 | if not len(self.selected): 1146 | order = self.items[self.item_sel] 1147 | do_cancel(order) 1148 | else: 1149 | for order in self.selected: 1150 | do_cancel(order) 1151 | 1152 | 1153 | class TextBox(): 1154 | """wrapper for curses.textpad.Textbox""" 1155 | 1156 | def __init__(self, dlg, posy, posx, length): 1157 | self.dlg = dlg 1158 | self.win = dlg.win.derwin(1, length, posy, posx) 1159 | self.win.keypad(1) 1160 | self.box = curses.textpad.Textbox(self.win, insert_mode=True) 1161 | self.value = "" 1162 | self.result = None 1163 | self.editing = False 1164 | 1165 | def __del__(self): 1166 | self.box = None 1167 | self.win = None 1168 | 1169 | def modal(self): 1170 | """enter te edit box modal loop""" 1171 | self.win.move(0, 0) 1172 | self.editing = True 1173 | goxapi.start_thread(self.cursor_placement_thread, "TextBox cursor placement") 1174 | self.value = self.box.edit(self.validator) 1175 | self.editing = False 1176 | return self.result 1177 | 1178 | def validator(self, char): 1179 | """here we tweak the behavior slightly, especially we want to 1180 | end modal editing mode immediately on arrow up/down and on enter 1181 | and we also want to catch ESC and F10, to abort the entire dialog""" 1182 | if curses.ascii.isprint(char): 1183 | return char 1184 | if char == curses.ascii.TAB: 1185 | char = curses.KEY_DOWN 1186 | if char in [curses.KEY_DOWN, curses.KEY_UP]: 1187 | self.result = char 1188 | return curses.ascii.BEL 1189 | if char in [10, 13, curses.KEY_ENTER, curses.ascii.BEL]: 1190 | self.result = 10 1191 | return curses.ascii.BEL 1192 | if char in [27, curses.KEY_F10]: 1193 | self.result = -1 1194 | return curses.ascii.BEL 1195 | return char 1196 | 1197 | def cursor_placement_thread(self): 1198 | """this is the most ugly hack of the entire program. During the 1199 | signals hat are fired while we are editing there will be many repaints 1200 | of other other panels below this dialog and when curses is done 1201 | repainting everything the blinking cursor is not in the correct 1202 | position. This is only a cosmetic problem but very annnoying. Try to 1203 | force it into the edit field by repainting it very often.""" 1204 | while self.editing: 1205 | # pylint: disable=W0212 1206 | with goxapi.Signal._lock: 1207 | curses.curs_set(2) 1208 | self.win.touchwin() 1209 | self.win.refresh() 1210 | time.sleep(0.1) 1211 | curses.curs_set(0) 1212 | 1213 | 1214 | class NumberBox(TextBox): 1215 | """TextBox that only accepts numbers""" 1216 | def __init__(self, dlg, posy, posx, length): 1217 | TextBox.__init__(self, dlg, posy, posx, length) 1218 | 1219 | def validator(self, char): 1220 | """allow only numbers to be entered""" 1221 | if char == ord("q"): 1222 | char = curses.KEY_F10 1223 | if curses.ascii.isprint(char): 1224 | if chr(char) not in "0123456789.": 1225 | char = 0 1226 | return TextBox.validator(self, char) 1227 | 1228 | 1229 | class DlgNewOrder(Win): 1230 | """abtract base class for entering new orders""" 1231 | def __init__(self, stdscr, gox, color, title): 1232 | self.gox = gox 1233 | self.color = color 1234 | self.title = title 1235 | self.edit_price = None 1236 | self.edit_volume = None 1237 | Win.__init__(self, stdscr) 1238 | 1239 | def calc_size(self): 1240 | Win.calc_size(self) 1241 | self.width = 35 1242 | self.height = 8 1243 | self.posx = (self.termwidth - self.width) / 2 1244 | self.posy = (self.termheight - self.height) / 2 1245 | 1246 | def paint(self): 1247 | self.win.bkgd(" ", self.color) 1248 | self.win.border() 1249 | self.addstr(0, 1, " %s " % self.title, self.color) 1250 | self.addstr(2, 2, " price", self.color) 1251 | self.addstr(2, 30, self.gox.curr_quote) 1252 | self.addstr(4, 2, "volume", self.color) 1253 | self.addstr(4, 30, self.gox.curr_base) 1254 | self.addstr(6, 2, "F10 ", self.color + curses.A_REVERSE) 1255 | self.addstr("cancel ", self.color) 1256 | self.addstr("Enter ", self.color + curses.A_REVERSE) 1257 | self.addstr("submit ", self.color) 1258 | self.edit_price = NumberBox(self, 2, 10, 20) 1259 | self.edit_volume = NumberBox(self, 4, 10, 20) 1260 | 1261 | def do_submit(self, price_float, volume_float): 1262 | """sumit the order. implementating class will do eiter buy or sell""" 1263 | raise NotImplementedError() 1264 | 1265 | def modal(self): 1266 | """enter the modal getch() loop of this dialog""" 1267 | if self.win: 1268 | focus = 1 1269 | # next time I am going to use some higher level 1270 | # wrapper on top of curses, i promise... 1271 | while True: 1272 | if focus == 1: 1273 | res = self.edit_price.modal() 1274 | if res == -1: 1275 | break # cancel entire dialog 1276 | if res in [10, curses.KEY_DOWN, curses.KEY_UP]: 1277 | try: 1278 | price_float = float(self.edit_price.value) 1279 | focus = 2 1280 | except ValueError: 1281 | pass # can't move down until this is a valid number 1282 | 1283 | if focus == 2: 1284 | res = self.edit_volume.modal() 1285 | if res == -1: 1286 | break # cancel entire dialog 1287 | if res in [curses.KEY_UP, curses.KEY_DOWN]: 1288 | focus = 1 1289 | if res == 10: 1290 | try: 1291 | volume_float = float(self.edit_volume.value) 1292 | break # have both values now, can submit order 1293 | except ValueError: 1294 | pass # no float number, stay in this edit field 1295 | 1296 | if res == -1: 1297 | #user has hit f10. just end here, do nothing 1298 | pass 1299 | if res == 10: 1300 | self.do_submit(price_float, volume_float) 1301 | 1302 | # make sure all cyclic references are garbage collected or 1303 | # otherwise the curses window won't disappear 1304 | self.edit_price = None 1305 | self.edit_volume = None 1306 | 1307 | 1308 | class DlgNewOrderBid(DlgNewOrder): 1309 | """Modal dialog for new buy order""" 1310 | def __init__(self, stdscr, gox): 1311 | DlgNewOrder.__init__(self, stdscr, gox, 1312 | COLOR_PAIR["dialog_bid_text"], 1313 | "New buy order") 1314 | 1315 | def do_submit(self, price, volume): 1316 | price = self.gox.quote2int(price) 1317 | volume = self.gox.base2int(volume) 1318 | self.gox.buy(price, volume) 1319 | 1320 | 1321 | class DlgNewOrderAsk(DlgNewOrder): 1322 | """Modal dialog for new sell order""" 1323 | def __init__(self, stdscr, gox): 1324 | DlgNewOrder.__init__(self, stdscr, gox, 1325 | COLOR_PAIR["dialog_ask_text"], 1326 | "New sell order") 1327 | 1328 | def do_submit(self, price, volume): 1329 | price = self.gox.quote2int(price) 1330 | volume = self.gox.base2int(volume) 1331 | self.gox.sell(price, volume) 1332 | 1333 | 1334 | 1335 | # 1336 | # 1337 | # logging, printing, etc... 1338 | # 1339 | 1340 | class LogWriter(): 1341 | """connects to gox.signal_debug and logs it all to the logfile""" 1342 | def __init__(self, gox): 1343 | self.gox = gox 1344 | if self.gox.config.get_bool("goxtool", "dont_truncate_logfile"): 1345 | logfilemode = 'a' 1346 | else: 1347 | logfilemode = 'w' 1348 | 1349 | logging.basicConfig(filename='goxtool.log' 1350 | ,filemode=logfilemode 1351 | ,format='%(asctime)s:%(levelname)s:%(message)s' 1352 | ,level=logging.DEBUG 1353 | ) 1354 | self.gox.signal_debug.connect(self.slot_debug) 1355 | 1356 | def close(self): 1357 | """stop logging""" 1358 | #not needed 1359 | pass 1360 | 1361 | # pylint: disable=R0201 1362 | def slot_debug(self, sender, (msg)): 1363 | """handler for signal_debug signals""" 1364 | name = "%s.%s" % (sender.__class__.__module__, sender.__class__.__name__) 1365 | logging.debug("%s:%s", name, msg) 1366 | 1367 | 1368 | class PrintHook(): 1369 | """intercept stdout/stderr and send it all to gox.signal_debug instead""" 1370 | def __init__(self, gox): 1371 | self.gox = gox 1372 | self.stdout = sys.stdout 1373 | self.stderr = sys.stderr 1374 | sys.stdout = self 1375 | sys.stderr = self 1376 | 1377 | def close(self): 1378 | """restore normal stdio""" 1379 | sys.stdout = self.stdout 1380 | sys.stderr = self.stderr 1381 | 1382 | def write(self, string): 1383 | """called when someone uses print(), send it to gox""" 1384 | string = string.strip() 1385 | if string != "": 1386 | self.gox.signal_debug(self, string) 1387 | 1388 | 1389 | 1390 | # 1391 | # 1392 | # dynamically (re)loadable strategy module 1393 | # 1394 | 1395 | class StrategyManager(): 1396 | """load the strategy module""" 1397 | 1398 | def __init__(self, gox, strategy_name_list): 1399 | self.strategy_object_list = [] 1400 | self.strategy_name_list = strategy_name_list 1401 | self.gox = gox 1402 | self.reload() 1403 | 1404 | def unload(self): 1405 | """unload the strategy, will trigger its the __del__ method""" 1406 | self.gox.signal_strategy_unload(self, None) 1407 | self.strategy_object_list = [] 1408 | 1409 | def reload(self): 1410 | """reload and re-initialize the strategy module""" 1411 | self.unload() 1412 | for name in self.strategy_name_list: 1413 | name = name.replace(".py", "").strip() 1414 | 1415 | try: 1416 | strategy_module = __import__(name) 1417 | try: 1418 | reload(strategy_module) 1419 | strategy_object = strategy_module.Strategy(self.gox) 1420 | self.strategy_object_list.append(strategy_object) 1421 | if hasattr(strategy_object, "name"): 1422 | self.gox.strategies[strategy_object.name] = strategy_object 1423 | 1424 | except Exception: 1425 | self.gox.debug("### error while loading strategy %s.py, traceback follows:" % name) 1426 | self.gox.debug(traceback.format_exc()) 1427 | 1428 | except ImportError: 1429 | self.gox.debug("### could not import %s.py, traceback follows:" % name) 1430 | self.gox.debug(traceback.format_exc()) 1431 | 1432 | 1433 | def toggle_setting(gox, alternatives, option_name, direction): 1434 | """toggle a setting in the ini file""" 1435 | # pylint: disable=W0212 1436 | with goxapi.Signal._lock: 1437 | setting = gox.config.get_string("goxtool", option_name) 1438 | try: 1439 | newindex = (alternatives.index(setting) + direction) % len(alternatives) 1440 | except ValueError: 1441 | newindex = 0 1442 | gox.config.set("goxtool", option_name, alternatives[newindex]) 1443 | gox.config.save() 1444 | 1445 | def toggle_depth_group(gox, direction): 1446 | """toggle the step width of the depth chart""" 1447 | if gox.curr_quote in "JPY SEK": 1448 | alt = ["5", "10", "25", "50", "100", "200", "500", "1000", "2000", "5000", "10000"] 1449 | else: 1450 | alt = ["0.05", "0.1", "0.25", "0.5", "1", "2", "5", "10", "20", "50", "100"] 1451 | toggle_setting(gox, alt, "depth_chart_group", direction) 1452 | gox.orderbook.signal_changed(gox.orderbook, None) 1453 | 1454 | def toggle_orderbook_group(gox, direction): 1455 | """toggle the group width of the orderbook""" 1456 | if gox.curr_quote in "JPY SEK": 1457 | alt = ["0", "5", "10", "25", "50", "100", "200", "500", "1000", "2000", "5000", "10000"] 1458 | else: 1459 | alt = ["0", "0.05", "0.1", "0.25", "0.5", "1", "2", "5", "10", "20", "50", "100"] 1460 | toggle_setting(gox, alt, "orderbook_group", direction) 1461 | gox.orderbook.signal_changed(gox.orderbook, None) 1462 | 1463 | def toggle_orderbook_sum(gox): 1464 | """toggle the summing in the orderbook on and off""" 1465 | alt = ["False", "True"] 1466 | toggle_setting(gox, alt, "orderbook_sum_total", 1) 1467 | gox.orderbook.signal_changed(gox.orderbook, None) 1468 | 1469 | def toggle_depth_sum(gox): 1470 | """toggle the summing in the depth chart on and off""" 1471 | alt = ["False", "True"] 1472 | toggle_setting(gox, alt, "depth_chart_sum_total", 1) 1473 | gox.orderbook.signal_changed(gox.orderbook, None) 1474 | 1475 | def set_ini(gox, setting, value, signal, signal_sender, signal_params): 1476 | """set the ini value and then send a signal""" 1477 | # pylint: disable=W0212 1478 | with goxapi.Signal._lock: 1479 | gox.config.set("goxtool", setting, value) 1480 | gox.config.save() 1481 | signal(signal_sender, signal_params) 1482 | 1483 | 1484 | 1485 | # 1486 | # 1487 | # main program 1488 | # 1489 | 1490 | def main(): 1491 | """main funtion, called at the start of the program""" 1492 | 1493 | debug_tb = [] 1494 | def curses_loop(stdscr): 1495 | """Only the code inside this function runs within the curses wrapper""" 1496 | 1497 | # this function may under no circumstancs raise an exception, so I'm 1498 | # wrapping everything into try/except (should actually never happen 1499 | # anyways but when it happens during coding or debugging it would 1500 | # leave the terminal in an unusable state and this must be avoded). 1501 | # We have a list debug_tb[] where we can append tracebacks and 1502 | # after curses uninitialized properly and the terminal is restored 1503 | # we can print them. 1504 | try: 1505 | init_colors() 1506 | gox = goxapi.Gox(secret, config) 1507 | 1508 | logwriter = LogWriter(gox) 1509 | printhook = PrintHook(gox) 1510 | 1511 | conwin = WinConsole(stdscr, gox) 1512 | bookwin = WinOrderBook(stdscr, gox) 1513 | statuswin = WinStatus(stdscr, gox) 1514 | chartwin = WinChart(stdscr, gox) 1515 | 1516 | strategy_manager = StrategyManager(gox, strat_mod_list) 1517 | 1518 | gox.start() 1519 | while True: 1520 | key = stdscr.getch() 1521 | if key == ord("q"): 1522 | break 1523 | elif key == curses.KEY_F4: 1524 | DlgNewOrderBid(stdscr, gox).modal() 1525 | elif key == curses.KEY_F5: 1526 | DlgNewOrderAsk(stdscr, gox).modal() 1527 | elif key == curses.KEY_F6: 1528 | DlgCancelOrders(stdscr, gox).modal() 1529 | elif key == curses.KEY_RESIZE: 1530 | # pylint: disable=W0212 1531 | with goxapi.Signal._lock: 1532 | stdscr.erase() 1533 | stdscr.refresh() 1534 | conwin.resize() 1535 | bookwin.resize() 1536 | chartwin.resize() 1537 | statuswin.resize() 1538 | elif key == ord("l"): 1539 | strategy_manager.reload() 1540 | 1541 | # which chart to show on the right side 1542 | elif key == ord("H"): 1543 | set_ini(gox, "display_right", "history_chart", 1544 | gox.history.signal_changed, gox.history, None) 1545 | elif key == ord("D"): 1546 | set_ini(gox, "display_right", "depth_chart", 1547 | gox.orderbook.signal_changed, gox.orderbook, None) 1548 | 1549 | # depth chart step 1550 | elif key == ord(","): # zoom out 1551 | toggle_depth_group(gox, +1) 1552 | elif key == ord("."): # zoom in 1553 | toggle_depth_group(gox, -1) 1554 | 1555 | # orderbook grouping step 1556 | elif key == ord("-"): # zoom out (larger step) 1557 | toggle_orderbook_group(gox, +1) 1558 | elif key == ord("+"): # zoom in (smaller step) 1559 | toggle_orderbook_group(gox, -1) 1560 | 1561 | elif key == ord("S"): 1562 | toggle_orderbook_sum(gox) 1563 | 1564 | elif key == ord("T"): 1565 | toggle_depth_sum(gox) 1566 | 1567 | # lowercase keys go to the strategy module 1568 | elif key >= ord("a") and key <= ord("z"): 1569 | gox.signal_keypress(gox, (key)) 1570 | else: 1571 | gox.debug("key pressed: key=%i" % key) 1572 | 1573 | except KeyboardInterrupt: 1574 | # Ctrl+C has been pressed 1575 | pass 1576 | 1577 | except Exception: 1578 | debug_tb.append(traceback.format_exc()) 1579 | 1580 | # we are here because shutdown was requested. 1581 | # 1582 | # Before we do anything we dump stacktraces of all currently running 1583 | # threads to a separate logfile because this helps debugging freezes 1584 | # and deadlocks that might occur if things went totally wrong. 1585 | with open("goxtool.stacktrace.log", "w") as stacklog: 1586 | stacklog.write(dump_all_stacks()) 1587 | 1588 | # we need the signal lock to be able to shut down. And we cannot 1589 | # wait for any frozen slot to return, so try really hard to get 1590 | # the lock and if that fails then unlock it forcefully. 1591 | try_get_lock_or_break_open() 1592 | 1593 | # Now trying to shutdown everything in an orderly manner.it in the 1594 | # Since we are still inside curses but we don't know whether 1595 | # the printhook or the logwriter was initialized properly already 1596 | # or whether it crashed earlier we cannot print here and we also 1597 | # cannot log, so we put all tracebacks into the debug_tb list to 1598 | # print them later once the terminal is properly restored again. 1599 | try: 1600 | strategy_manager.unload() 1601 | except Exception: 1602 | debug_tb.append(traceback.format_exc()) 1603 | 1604 | try: 1605 | gox.stop() 1606 | except Exception: 1607 | debug_tb.append(traceback.format_exc()) 1608 | 1609 | try: 1610 | printhook.close() 1611 | except Exception: 1612 | debug_tb.append(traceback.format_exc()) 1613 | 1614 | try: 1615 | logwriter.close() 1616 | except Exception: 1617 | debug_tb.append(traceback.format_exc()) 1618 | 1619 | # curses_loop() ends here, we must reach this point under all circumstances. 1620 | # Now curses will restore the terminal back to cooked (normal) mode. 1621 | 1622 | 1623 | # Here it begins. The very first thing is to always set US or GB locale 1624 | # to have always the same well defined behavior for number formatting. 1625 | for loc in ["en_US.UTF8", "en_GB.UTF8", "en_EN", "en_GB", "C"]: 1626 | try: 1627 | locale.setlocale(locale.LC_NUMERIC, loc) 1628 | break 1629 | except locale.Error: 1630 | continue 1631 | 1632 | # before we can finally start the curses UI we might need to do some user 1633 | # interaction on the command line, regarding the encrypted secret 1634 | argp = argparse.ArgumentParser(description='MtGox live market data monitor' 1635 | + ' and trading bot experimentation framework') 1636 | argp.add_argument('--add-secret', action="store_true", 1637 | help="prompt for API secret, encrypt it and then exit") 1638 | argp.add_argument('--strategy', action="store", default="strategy.py", 1639 | help="name of strategy module files, comma separated list, default=strategy.py") 1640 | argp.add_argument('--protocol', action="store", default="", 1641 | help="force protocol (socketio or websocket), ignore setting in .ini") 1642 | argp.add_argument('--no-fulldepth', action="store_true", default=False, 1643 | help="do not download full depth (useful for debugging)") 1644 | argp.add_argument('--no-depth', action="store_true", default=False, 1645 | help="do not request depth messages (implies no-fulldeph), useful for low traffic") 1646 | argp.add_argument('--no-lag', action="store_true", default=False, 1647 | help="do not request order-lag updates, useful for low traffic") 1648 | argp.add_argument('--no-history', action="store_true", default=False, 1649 | help="do not download full history (useful for debugging)") 1650 | argp.add_argument('--use-http', action="store_true", default=False, 1651 | help="use http api for trading (more reliable, recommended") 1652 | argp.add_argument('--no-http', action="store_true", default=False, 1653 | help="use streaming api for trading (problematic when streaming api disconnects often)") 1654 | argp.add_argument('--password', action="store", default=None, 1655 | help="password for decryption of stored key. This is a dangerous option " 1656 | +"because the password might end up being stored in the history file " 1657 | +"of your shell, for example in ~/.bash_history. Use this only when " 1658 | +"starting it from within a script and then of course you need to " 1659 | +"keep this start script in a secure place!") 1660 | args = argp.parse_args() 1661 | 1662 | config = goxapi.GoxConfig("goxtool.ini") 1663 | config.init_defaults(INI_DEFAULTS) 1664 | secret = goxapi.Secret(config) 1665 | secret.password_from_commandline_option = args.password 1666 | if args.add_secret: 1667 | # prompt for secret, encrypt, write to .ini and then exit the program 1668 | secret.prompt_encrypt() 1669 | else: 1670 | strat_mod_list = args.strategy.split(",") 1671 | goxapi.FORCE_PROTOCOL = args.protocol 1672 | goxapi.FORCE_NO_FULLDEPTH = args.no_fulldepth 1673 | goxapi.FORCE_NO_DEPTH = args.no_depth 1674 | goxapi.FORCE_NO_LAG = args.no_lag 1675 | goxapi.FORCE_NO_HISTORY = args.no_history 1676 | goxapi.FORCE_HTTP_API = args.use_http 1677 | goxapi.FORCE_NO_HTTP_API = args.no_http 1678 | if goxapi.FORCE_NO_DEPTH: 1679 | goxapi.FORCE_NO_FULLDEPTH = True 1680 | 1681 | # if its ok then we can finally enter the curses main loop 1682 | if secret.prompt_decrypt() != secret.S_FAIL_FATAL: 1683 | 1684 | ### 1685 | # 1686 | # now going to enter cbreak mode and start the curses loop... 1687 | curses.wrapper(curses_loop) 1688 | # curses ended, terminal is back in normal (cooked) mode 1689 | # 1690 | ### 1691 | 1692 | if len(debug_tb): 1693 | print "\n\n*** error(s) in curses_loop() that caused unclean shutdown:\n" 1694 | for trb in debug_tb: 1695 | print trb 1696 | else: 1697 | print 1698 | print "*******************************************************" 1699 | print "* Please donate: 1C8aDabADaYvTKvCAG1htqYcEgpAhkeYoW *" 1700 | print "*******************************************************" 1701 | 1702 | if __name__ == "__main__": 1703 | main() 1704 | 1705 | --------------------------------------------------------------------------------