├── 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 |
--------------------------------------------------------------------------------