├── .gitignore ├── requirements.txt ├── README.md ├── test.py ├── lib ├── util.py └── prediction.py ├── faig.py ├── apps └── market_watcher.py ├── default.conf ├── igclient.py ├── ig.py └── igstream.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.conf 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.26.3 2 | Requests>=2.31.0 3 | scikit_learn>=1.4.0 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FAIG - Fully Automated IG Index 2 | 3 | 1. Requires a valid IG Index Account - https://www.ig.com/uk/welcome-page 4 | 2. Requires a valid IG Index API Key. (Works for DEMO and LIVE accounts) 5 | 3. Cross Platform (Linux and Windows) 6 | 4. Python3 7 | 8 | Copy default.conf to config.conf and add all settings there 9 | 10 | 11 | Donate with PayPal 12 | 13 | 14 | ###### Bitcoin Cash (BCH) tipjar - qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 15 | 16 | ###### Ether (ETH) tipjar - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 17 | 18 | ###### Litecoin (LTC) tipjar - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 19 | 20 | ###### Bitcoin (BTC) tipjar - 14Dm7L3ABPtumPDcj3REAvs4L6w9YFRnHK 21 | 22 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | """This is to test modules.""" 36 | import json 37 | import configparser 38 | 39 | from igclient import IGClient 40 | from apps.market_watcher import MarketWatcher 41 | 42 | config = configparser.ConfigParser() 43 | config.read("config.conf") 44 | 45 | client = IGClient() 46 | client.session() 47 | 48 | 49 | def main(): 50 | epics = [i for i in json.loads(config["Epics"]["EPIC_IDS"])] 51 | watcher = MarketWatcher(client=client, epics=epics) 52 | watcher.watch() 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /lib/util.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | # Licensed under the Apache License, Version 2.0 (the "License"); 36 | # you may not use this file except in compliance with the License. 37 | # You may obtain a copy of the License at 38 | # 39 | # http://www.apache.org/licenses/LICENSE-2.0 40 | # 41 | # Unless required by applicable law or agreed to in writing, software 42 | # distributed under the License is distributed on an "AS IS" BASIS, 43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | # See the License for the specific language governing permissions and 45 | # limitations under the License. 46 | 47 | 48 | def humanize_time(secs): 49 | mins, secs = divmod(secs, 60) 50 | hours, mins = divmod(mins, 60) 51 | return "%02d:%02d:%02d" % (hours, mins, secs) 52 | -------------------------------------------------------------------------------- /faig.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | # IF YOU FOUND THIS USEFUL, Please Donate some Bitcoin .... 36 | # 1FWt366i5PdrxCC6ydyhD8iywUHQ2C7BWC 37 | 38 | #!/usr/bin/env python3 39 | # -*- coding: utf-8 -*- 40 | import sys 41 | from ig import API 42 | from lib.prediction import Prediction 43 | 44 | api = API() 45 | 46 | while True: 47 | d = api.find_next_trade() 48 | 49 | epic_id = d["values"]["EPIC"] 50 | 51 | prediction = Prediction(api.config) 52 | prediction.epic_id = epic_id 53 | prediction.current_price = float(d["values"]["BID"]) 54 | prediction.set_marketdata(api.clientsentiment(epic_id)) 55 | if ( 56 | prediction.quick_check() is None 57 | ): # no point pulling in market data (right now), we'll reject this later on anyway 58 | continue # find a different trade 59 | 60 | if api.config["Trade"]["algorithm"] == "LinearRegression": 61 | (x, y) = api.fetch_lg_prices(epic_id) 62 | (high_price, low_price) = api.fetch_lg_highlow(epic_id) 63 | 64 | prediction.linear_regression(x=x, 65 | y=y, 66 | high_price=high_price, 67 | low_price=low_price) 68 | else: 69 | sys.exit("Trading Algorithm: {} not found".format( 70 | api.config["Trade"]["algorithm"])) 71 | 72 | if prediction.direction_to_trade is None: 73 | print( 74 | "!!DEBUG!! Literally NO decent trade direction could be determined") 75 | continue 76 | 77 | api.placeOrder(prediction) 78 | continue 79 | -------------------------------------------------------------------------------- /apps/market_watcher.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | """This is to check is price change is within required range.""" 36 | import random 37 | from time import sleep 38 | import logging 39 | 40 | logging.basicConfig(level="INFO", format="%(asctime)s %(message)s") 41 | 42 | 43 | class MarketWatcher: 44 | """ 45 | 46 | This is an observer of market and report when certain criteria meet. 47 | 48 | There are 2 types of information to observe. 49 | 1) When absolute price change is within range. 50 | 2) When spread is within range. 51 | 52 | """ 53 | 54 | ok = False # Whether price changes obey rules. 55 | client = None # IG client 56 | epics = [] # List of epic IDs. 57 | epic = None # Current epic ID. 58 | # Market data. 59 | market_id = None 60 | current_price = None 61 | price_change = None 62 | percent_change = None 63 | bid = None 64 | ask = None 65 | # Range of price changes and spread to consider trading. 66 | min_change = None 67 | max_change = None 68 | min_spread = None 69 | max_spread = None 70 | 71 | def __init__(self, 72 | client, 73 | epics, 74 | change_range=(0.48, 1.9), 75 | spread_range=(0.0, 2.0)): 76 | 77 | assert isinstance(epics, list) 78 | 79 | def is_valid_range(x): 80 | assert isinstance(x, (tuple, list)) 81 | assert all(isinstance(i, float) and i >= 0 for i in x) 82 | return True 83 | 84 | assert is_valid_range(change_range) 85 | assert is_valid_range(spread_range) 86 | 87 | self.client = client 88 | self.epics = epics 89 | self.min_change = min(change_range) 90 | self.max_change = max(change_range) 91 | self.min_spread = min(spread_range) 92 | self.max_spread = max(spread_range) 93 | 94 | def watch(self): 95 | """This is to keep updating the market data until a valid price movement is observed.""" 96 | while not self.ok: 97 | self.epic = self.__get_epic_id() 98 | self.__update_market_data() 99 | if self.__price_change_is_in_range() and self.__spread_is_in_range( 100 | ): 101 | self.ok = True 102 | self.__log("Hit") 103 | else: 104 | self.ok = False 105 | self.__log("Pass") 106 | sleep(2) # Wait for a while before refresh. 107 | 108 | def __get_epic_id(self): 109 | """This is to get a random epic in list.""" 110 | random.shuffle(self.epics) 111 | epic = random.choice(self.epics) 112 | return epic 113 | 114 | def __update_market_data(self): 115 | """This is to update market data.""" 116 | i = self.client.markets(self.epic) 117 | instrument, snapshot = i["instrument"], i["snapshot"] 118 | 119 | self.market_id = instrument["marketId"] 120 | self.current_price = snapshot["bid"] 121 | self.price_change = snapshot["netChange"] 122 | 123 | def to_float(x): 124 | if x is None: 125 | return 0 126 | else: 127 | return float(x) 128 | 129 | # Convert percentage change to float. 130 | self.percent_change = to_float(snapshot["percentageChange"]) 131 | 132 | # # Convert bid ask prices to float. 133 | self.bid = to_float(snapshot["bid"]) 134 | self.ask = to_float(snapshot["offer"]) 135 | 136 | # Calculate spread. 137 | self.spread = self.ask - self.bid 138 | assert self.spread >= 0 139 | 140 | def __price_change_is_in_range(self): 141 | """This is to check if price change is in range.""" 142 | return self.min_change < abs(self.percent_change) < self.max_change 143 | 144 | def __spread_is_in_range(self): 145 | """ 146 | 147 | This is to check if spread is in range. 148 | 149 | Examples 150 | Spread is -30, That is too big, In-fact way too big. 151 | Spread is -1.7, This is not too bad, We can trade on this reasonably well. 152 | Spread is 0.8. This is considered a tight spread. 153 | 154 | """ 155 | return self.min_spread < self.data["spread"] < self.max_spread 156 | 157 | def __log(self, msg): 158 | logging.info( 159 | "epic: {epic}, price: {bid}/{ask}, spread: {spread}, price change: {price_change}, percentage change: {percent_change} -> {msg}" 160 | .format( 161 | msg=msg, 162 | epic=self.epic, 163 | bid=int(self.bid), 164 | ask=int(self.ask), 165 | spread=int(self.spread), 166 | price_change=round(self.price_change, 2), 167 | percent_change=round(self.percent_change, 2), 168 | )) 169 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | # Copy this file to config.conf and edit settings there 2 | # Values in config.conf override anything found in default.conf 3 | 4 | [Config] 5 | API_ENDPOINT: https://demo-api.ig.com/gateway/deal 6 | #API_ENDPOINT: https://api.ig.com/gateway/deal 7 | ACCOUNT_TYPE: SPREADBET 8 | 9 | # do NOT set API_KEY here, set it in config.conf 10 | API_KEY: **************************** 11 | 12 | [Auth] 13 | USERNAME: ************ 14 | PASSWORD: ************ 15 | 16 | [Trade] 17 | algorithm: LinearRegression 18 | 19 | # high res uses a LOT more API calls for pricing history, but is more accurate 20 | high_resolution: True 21 | 22 | # ********************************************************************* 23 | # You can use sentiment as a filter, only taking the setups going against the crowd. 24 | # You must be in the minority of 40% or less. 25 | # More information See here: 26 | # https://www.reddit.com/r/Forex/comments/7wehbq/how_much_does_client_sentiment_sway_your_decisions/ 27 | # ********************************************************************* 28 | 29 | # use client sentiment values if true, else trade solely on price 30 | use_clientsentiment: True 31 | # follow client sentiment or go against 32 | clientsentiment_contrarian: True 33 | clientsentiment_value: 69 34 | hightrend_watermark: 89 35 | 36 | # if accuracy below this point, don't attempt trade 37 | predict_accuracy: 0.89 38 | 39 | # volatility needs to be between these amounts 40 | Price_Change_Day_percent_high: 1.9 41 | Price_Change_Day_percent_low: 0.48 42 | 43 | # trade on a set (safe) amount, or a calculated amount based on prediction 44 | # this pretty much excludes most exotics, since their minimum spreads are almost always > 2, which is a lot safer, but less profitable 45 | use_max_spread: True 46 | max_spread: -2 47 | # 1.3 is /very slightly/ better than average 48 | spread_multiplier: 1.2 49 | 50 | # how much do we set as a limit, 1.00 is 100% of the way to the lowest/highest value we've seen so far, and is extremely high 51 | greed: 0.20 52 | 53 | # size of trade 54 | size: 2 55 | 56 | stopDistance_value: 150 57 | always_guarantee_stops: True 58 | never_guarantee_stops: False 59 | 60 | [Epics] 61 | EPICS: { "CS.D.AUDUSD.TODAY.IP": { "minspread": 0.6 }, 62 | "CS.D.EURCHF.TODAY.IP": { "minspread": 2.0 }, 63 | "CS.D.EURGBP.TODAY.IP": { "minspread": 0.9 }, 64 | "CS.D.EURJPY.TODAY.IP": { "minspread": 1.5 }, 65 | "CS.D.EURUSD.TODAY.IP": { "minspread": 0.6 }, 66 | "CS.D.GBPEUR.TODAY.IP": { "minspread": 2.0 }, 67 | "CS.D.GBPJPY.TODAY.IP": { "minspread": 2.5 }, 68 | "CS.D.GBPUSD.TODAY.IP": { "minspread": 0.9 }, 69 | "CS.D.USDCAD.TODAY.IP": { "minspread": 1.7 }, 70 | "CS.D.USDCHF.TODAY.IP": { "minspread": 1.5 }, 71 | "CS.D.USDJPY.TODAY.IP": { "minspread": 0.7 }, 72 | "CS.D.CADCHF.TODAY.IP": { "minspread": 2.5 }, 73 | "CS.D.CADJPY.TODAY.IP": { "minspread": 2.5 }, 74 | "CS.D.CHFJPY.TODAY.IP": { "minspread": 2.0 }, 75 | "CS.D.EURCAD.TODAY.IP": { "minspread": 3.0 }, 76 | "CS.D.EURSGD.TODAY.IP": { "minspread": 5.0 }, 77 | "CS.D.EURZAR.TODAY.IP": { "minspread": 150.0 }, 78 | "CS.D.GBPCAD.TODAY.IP": { "minspread": 3.5 }, 79 | "CS.D.GBPCHF.TODAY.IP": { "minspread": 3.0 }, 80 | "CS.D.GBPSGD.TODAY.IP": { "minspread": 8.0 }, 81 | "CS.D.GBPZAR.TODAY.IP": { "minspread": 200.0 }, 82 | "CS.D.SGDJPY.TODAY.IP": { "minspread": 4.0 }, 83 | "CS.D.USDSGD.TODAY.IP": { "minspread": 3.0 }, 84 | "CS.D.USDZAR.TODAY.IP": { "minspread": 90.0 }, 85 | "CS.D.AUDCAD.TODAY.IP": { "minspread": 2.0 }, 86 | "CS.D.AUDCHF.TODAY.IP": { "minspread": 2.5 }, 87 | "CS.D.AUDEUR.TODAY.IP": { "minspread": 1.5 }, 88 | "CS.D.AUDGBP.TODAY.IP": { "minspread": 1.5 }, 89 | "CS.D.AUDJPY.TODAY.IP": { "minspread": 1.3 }, 90 | "CS.D.AUDNZD.TODAY.IP": { "minspread": 3.0 }, 91 | "CS.D.AUDSGD.TODAY.IP": { "minspread": 5.0 }, 92 | "CS.D.EURAUD.TODAY.IP": { "minspread": 1.8 }, 93 | "CS.D.EURNZD.TODAY.IP": { "minspread": 3.0 }, 94 | "CS.D.GBPAUD.TODAY.IP": { "minspread": 1.9 }, 95 | "CS.D.GBPNZD.TODAY.IP": { "minspread": 5.0 }, 96 | "CS.D.NZDCHF.TODAY.IP": { "minspread": 4.0 }, 97 | "CS.D.NZDEUR.TODAY.IP": { "minspread": 1.5 }, 98 | "CS.D.NZDGBP.TODAY.IP": { "minspread": 2.0 }, 99 | "CS.D.NZDJPY.TODAY.IP": { "minspread": 2.5 }, 100 | "CS.D.NZDUSD.TODAY.IP": { "minspread": 2.0 }, 101 | "CS.D.NZDCAD.TODAY.IP": { "minspread": 3.5 }, 102 | "CS.D.CADNOK.TODAY.IP": { "minspread": 25.0 }, 103 | "CS.D.CHFNOK.TODAY.IP": { "minspread": 40.0 }, 104 | # "CS.D.EURDKK.TODAY.IP": { "minspread": 10.0 }, 105 | # "CS.D.EURNOK.TODAY.IP": { "minspread": 25.0 }, 106 | # "CS.D.EURSEK.TODAY.IP": { "minspread": 30.0 }, 107 | "CS.D.GBPDKK.TODAY.IP": { "minspread": 30.0 }, 108 | "CS.D.GBPNOK.TODAY.IP": { "minspread": 50.0 }, 109 | "CS.D.GBPSEK.TODAY.IP": { "minspread": 50.0 }, 110 | "CS.D.NOKSEK.TODAY.IP": { "minspread": 6.0 }, 111 | # "CS.D.USDDKK.TODAY.IP": { "minspread": 15.0 }, 112 | "CS.D.USDNOK.TODAY.IP": { "minspread": 25.0 }, 113 | "CS.D.USDSEK.TODAY.IP": { "minspread": 25.0 }, 114 | "CS.D.CHFHUF.TODAY.IP": { "minspread": 25.0 }, 115 | # "CS.D.EURCZK.TODAY.IP": { "minspread": 25.0 }, 116 | "CS.D.EURHUF.TODAY.IP": { "minspread": 20.0 }, 117 | "CS.D.EURILS.TODAY.IP": { "minspread": 40.0 }, 118 | "CS.D.EURMXN.TODAY.IP": { "minspread": 90.0 }, 119 | "CS.D.EURPLN.TODAY.IP": { "minspread": 25.0 }, 120 | "CS.D.EURTRY.TODAY.IP": { "minspread": 15.0 }, 121 | # "CS.D.GBPCZK.TODAY.IP": { "minspread": 30.0 }, 122 | "CS.D.GBPHUF.TODAY.IP": { "minspread": 25.0 }, 123 | "CS.D.GBPILS.TODAY.IP": { "minspread": 50.0 }, 124 | "CS.D.GBPMXN.TODAY.IP": { "minspread": 130.0 }, 125 | "CS.D.GBPPLN.TODAY.IP": { "minspread": 30.0 }, 126 | "CS.D.GBPTRY.TODAY.IP": { "minspread": 20.0 }, 127 | # "CS.D.MXNJPY.TODAY.IP": { "minspread": 0.6 }, 128 | # "CS.D.NOKJPY.TODAY.IP": { "minspread": 0.8 }, 129 | # "CS.D.PLNJPY.TODAY.IP": { "minspread": 2.0 }, 130 | # "CS.D.SEKJPY.TODAY.IP": { "minspread": 0.6 }, 131 | # "CS.D.TRYJPY.TODAY.IP": { "minspread": 5.0 }, 132 | # "CS.D.USDCZK.TODAY.IP": { "minspread": 25.0 }, 133 | "CS.D.USDHUF.TODAY.IP": { "minspread": 20.0 }, 134 | "CS.D.USDILS.TODAY.IP": { "minspread": 30.0 }, 135 | "CS.D.USDMXN.TODAY.IP": { "minspread": 50.0 }, 136 | "CS.D.USDPLN.TODAY.IP": { "minspread": 25.0 }, 137 | "CS.D.USDTRY.TODAY.IP": { "minspread": 12.0 }, 138 | # "CS.D.AUDCNH.TODAY.IP": { "minspread": 20.0 }, 139 | # "CS.D.CADCNH.TODAY.IP": { "minspread": 15.0 }, 140 | # "CS.D.CNHJPY.TODAY.IP": { "minspread": 0.8 }, 141 | # "CS.D.EURCNH.TODAY.IP": { "minspread": 30.0 }, 142 | # "CS.D.sp_EURRUB.TODAY.IP": { "minspread": 2000.0 }, 143 | # "CS.D.GBPCNH.TODAY.IP": { "minspread": 35.0 }, 144 | # "CS.D.NZDCNH.TODAY.IP": { "minspread": 15.0 }, 145 | # "CS.D.USDCNH.TODAY.IP": { "minspread": 10.0 }, 146 | # "CS.D.BRLJPY.TODAY.IP": { "minspread": 3.0 }, 147 | # "CS.D.GBPINR.TODAY.IP": { "minspread": 35.0 }, 148 | # "CS.D.USDBRL.TODAY.IP": { "minspread": 30.0 }, 149 | # "CS.D.USDIDR.TODAY.IP": { "minspread": 80.0 }, 150 | # "CS.D.USDINR.TODAY.IP": { "minspread": 20.0 }, 151 | "CS.D.USDKRW.TODAY.IP": { "minspread": 100.0 }, 152 | "CS.D.USDMYR.TODAY.IP": { "minspread": 100.0 }, 153 | # "CS.D.USDPHP.TODAY.IP": { "minspread": 15.0 }, 154 | "CS.D.USDTWD.TODAY.IP": { "minspread": 80.0 }, 155 | # "CS.D.NZDAUD.TODAY.IP", 156 | "CS.D.sp_USDRUB.TODAY.IP": { "minspread": 1600.0 } } 157 | -------------------------------------------------------------------------------- /lib/prediction.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | # Licensed under the Apache License, Version 2.0 (the "License"); 36 | # you may not use this file except in compliance with the License. 37 | # You may obtain a copy of the License at 38 | # 39 | # http://www.apache.org/licenses/LICENSE-2.0 40 | # 41 | # Unless required by applicable law or agreed to in writing, software 42 | # distributed under the License is distributed on an "AS IS" BASIS, 43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | # See the License for the specific language governing permissions and 45 | # limitations under the License. 46 | 47 | import numpy as np 48 | 49 | 50 | class Prediction: 51 | 52 | def __init__(self, config): 53 | self.config = config 54 | self.predict_accuracy = float(config["Trade"]["predict_accuracy"]) 55 | self.use_clientsentiment = eval(config["Trade"]["use_clientsentiment"]) 56 | self.clientsentiment_contrarian = eval( 57 | config["Trade"]["clientsentiment_contrarian"]) 58 | self.clientsentiment_value = float( 59 | config["Trade"]["clientsentiment_value"]) 60 | self.hightrend_watermark = float(config["Trade"]["hightrend_watermark"]) 61 | self.greed = float(config["Trade"]["greed"]) 62 | 63 | self.epic_id = None 64 | self.current_price = None 65 | self.direction_to_trade = None 66 | self.stopdistance = float(config["Trade"]["stopDistance_value"]) 67 | 68 | self.limitDistance = 4 # initial setting to be overridden 69 | self.ordertype = "MARKET" 70 | self.expirytype = "DFB" 71 | self.currencycode = "GBP" 72 | self.forceopen = True 73 | self.size = float(config["Trade"]["size"]) 74 | 75 | def get_tradedata(self): 76 | return { 77 | "direction": self.direction_to_trade, 78 | "epic": self.epic_id, 79 | "limitDistance": str(self.limitDistance), 80 | "orderType": self.ordertype, 81 | "size": str(self.size), 82 | "expiry": self.expirytype, 83 | "currencyCode": self.currencycode, 84 | "forceOpen": self.forceopen, 85 | "stopDistance": str(self.stopdistance), 86 | } 87 | 88 | def set_marketdata(self, market_data): 89 | self.longPositionPercentage = float( 90 | market_data["longPositionPercentage"]) 91 | self.shortPositionPercentage = float( 92 | market_data["shortPositionPercentage"]) 93 | 94 | def quick_check(self): 95 | if self.use_clientsentiment: 96 | self.trade_type_by_sentiment() 97 | else: 98 | return True 99 | 100 | return self.direction_to_trade 101 | 102 | def trade_type_by_sentiment(self): 103 | if (self.shortPositionPercentage > self.longPositionPercentage and 104 | self.shortPositionPercentage >= self.clientsentiment_value): 105 | self.direction_to_trade = "SELL" 106 | elif (self.longPositionPercentage > self.shortPositionPercentage and 107 | self.longPositionPercentage >= self.clientsentiment_value): 108 | self.direction_to_trade = "BUY" 109 | elif self.shortPositionPercentage >= self.hightrend_watermark: 110 | self.direction_to_trade = "SELL" 111 | elif self.longPositionPercentage >= self.hightrend_watermark: 112 | self.direction_to_trade = "BUY" 113 | else: 114 | print("No Trade This time") 115 | print( 116 | "!!DEBUG shortPositionPercentage:{} longPositionPercentage:{} clientsentiment_value:{} hightrend_watermark:{}" 117 | .format( 118 | self.shortPositionPercentage, 119 | self.longPositionPercentage, 120 | self.clientsentiment_value, 121 | self.hightrend_watermark, 122 | )) 123 | return None 124 | 125 | def trade_type_by_priceprediction(self): 126 | if self.price_prediction > self.current_price: 127 | self.direction_to_trade = "BUY" 128 | elif self.price_prediction < self.current_price: 129 | self.direction_to_trade = "SELL" 130 | else: 131 | self.direction_to_trade = None 132 | 133 | def reverse_direction(self): 134 | if self.direction_to_trade == "SELL": 135 | self.direction_to_trade = "BUY" 136 | elif self.direction_to_trade == "BUY": 137 | self.direction_to_trade = "SELL" 138 | 139 | def determine_trade_direction(self): 140 | 141 | current_price = self.current_price 142 | score = self.score 143 | price_prediction = self.price_prediction 144 | price_diff = float(current_price - price_prediction) 145 | 146 | print( 147 | "price_diff:{} score:{} current_price:{} limitDistance:{} predict_accuracy:{} price_prediction:{}" 148 | .format( 149 | price_diff, 150 | score, 151 | current_price, 152 | self.limitDistance, 153 | self.predict_accuracy, 154 | price_prediction, 155 | )) 156 | 157 | self.limitDistance = round(price_diff * score * self.greed, 158 | 1) # vary according to certainty and greed 159 | if self.limitDistance < 0: 160 | self.limitDistance *= -1 161 | if self.limitDistance == 0: 162 | # calculated risk isn't valid for a trade 163 | return None 164 | 165 | if score >= self.predict_accuracy: 166 | # highly accurate score - go with that 167 | self.trade_type_by_priceprediction() 168 | elif self.use_clientsentiment: 169 | self.trade_type_by_sentiment() 170 | if self.clientsentiment_contrarian: 171 | # reverse position to go against sentiment 172 | self.reverse_direction() 173 | else: 174 | pass 175 | 176 | return self.direction_to_trade 177 | 178 | def linear_regression(self, x, y, high_price, low_price): 179 | 180 | from sklearn.linear_model import LinearRegression 181 | 182 | x = np.asarray(x) 183 | y = np.asarray(y) 184 | 185 | # Initialize the model then train it on the data 186 | genius_regression_model = LinearRegression() 187 | genius_regression_model.fit(x, y) 188 | 189 | # Predict the corresponding value of Y for X 190 | pred_ict = [high_price, low_price] 191 | pred_ict = np.asarray(pred_ict) # To Numpy Array, hacky but good!! 192 | pred_ict = pred_ict.reshape(1, -1) 193 | self.price_prediction = genius_regression_model.predict(pred_ict) 194 | print("PRICE PREDICTION FOR PRICE " + self.epic_id + " IS : " + 195 | str(self.price_prediction)) 196 | 197 | self.score = genius_regression_model.score(x, y) 198 | predictions = { 199 | "intercept": genius_regression_model.intercept_, 200 | "coefficient": genius_regression_model.coef_, 201 | "current_price": self.current_price, 202 | "predicted_value": self.price_prediction, 203 | "accuracy": self.score, 204 | } 205 | print("-----------------DEBUG-----------------") 206 | print(predictions) 207 | print("-----------------DEBUG-----------------") 208 | self.determine_trade_direction() 209 | -------------------------------------------------------------------------------- /igclient.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | # Licensed under the Apache License, Version 2.0 (the "License"); 36 | # you may not use this file except in compliance with the License. 37 | # You may obtain a copy of the License at 38 | # 39 | # http://www.apache.org/licenses/LICENSE-2.0 40 | # 41 | # Unless required by applicable law or agreed to in writing, software 42 | # distributed under the License is distributed on an "AS IS" BASIS, 43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | # See the License for the specific language governing permissions and 45 | # limitations under the License. 46 | 47 | import configparser 48 | import requests 49 | import json 50 | import time 51 | 52 | 53 | def trackcall(f): 54 | # tracks number of recent api calls (in last 60s) and sleeps accordingly 55 | def wrap(*args, **kwargs): 56 | while len(args[0].recent_calls) >= 30 - 1: 57 | time.sleep(1) 58 | args[0].recent_calls = [ 59 | x for x in args[0].recent_calls if x > int(time.time() - 60) 60 | ] 61 | # args[0].recent_calls = filter(lambda x: x > int(time.time())-60, args[0].recent_calls) 62 | args[0].recent_calls.append(int(time.time())) 63 | return f(*args, **kwargs) 64 | 65 | return wrap 66 | 67 | 68 | class IGClient: 69 | 70 | def __init__(self, config=None): 71 | self.loggedin = False 72 | self.json = True # return json or obj 73 | if config is None: 74 | config = configparser.ConfigParser() 75 | config.read("default.conf") 76 | config.read("config.conf") 77 | self.config = config 78 | self.auth = {} 79 | self.debug = False 80 | self.allowance = {} 81 | self.recent_calls = [] 82 | 83 | self.API_ENDPOINT = self.config["Config"]["API_ENDPOINT"] 84 | self.API_KEY = self.config["Config"]["API_KEY"] 85 | 86 | def setdebug(self, value=True): 87 | self.debug = value 88 | try: 89 | import http.client as http_client 90 | except ImportError: 91 | import httplib as http_client 92 | http_client.HTTPConnection.debuglevel = (0, 1)[self.debug] 93 | 94 | @trackcall 95 | def session(self, set_default=True): 96 | data = { 97 | "identifier": self.config["Auth"]["USERNAME"], 98 | "password": self.config["Auth"]["PASSWORD"], 99 | } 100 | 101 | self.headers = { 102 | "Content-Type": "application/json; charset=utf-8", 103 | "Accept": "application/json; charset=utf-8", 104 | } 105 | 106 | self.headers.update({"X-IG-API-KEY": self.API_KEY}) 107 | self.session_headers = self.headers.copy() 108 | self.session_headers.update({"Version": "2"}) 109 | 110 | curr_json = self.json 111 | self.json = False # force off to let us use handlereq 112 | r = self._handlereq( 113 | requests.post( 114 | self.API_ENDPOINT + "/session", 115 | data=json.dumps(data), 116 | headers=self.session_headers, 117 | )) 118 | self.json = curr_json # set it back 119 | headers_json = dict(r.headers) 120 | for h in ["CST", "X-SECURITY-TOKEN"]: 121 | self.auth[h] = headers_json[h] 122 | 123 | self.headers.update(self.auth) 124 | self.authenticated_headers = self.headers 125 | 126 | self.loggedin = True 127 | 128 | # GET ACCOUNTS 129 | d = self.accounts() 130 | 131 | for i in d["accounts"]: 132 | if str(i["accountType"]) == self.config["Config"]["ACCOUNT_TYPE"]: 133 | # print ("Spreadbet Account ID is : " + str(i['accountId'])) 134 | self.accountId = str(i["accountId"]) 135 | break 136 | 137 | if set_default: 138 | # SET SPREAD BET ACCOUNT AS DEFAULT 139 | self.update_session({ 140 | "accountId": self.accountId, 141 | "defaultAccount": "True" 142 | }) 143 | # ERROR about account ID been the same, Ignore! 144 | 145 | return (r, json.loads(r.text))[self.json] 146 | 147 | def _handlereq(self, r): 148 | if self.debug: 149 | try: 150 | print(r.text) 151 | except Exception: 152 | pass 153 | return (r, json.loads(r.text))[self.json] 154 | 155 | def _authheadersfordelete(self): 156 | # WORKAROUND AS PER .... https://labs.ig.com/node/36 157 | delete_headers = self.authenticated_headers.copy() 158 | delete_headers.update({"_method": "DELETE"}) 159 | return delete_headers 160 | 161 | @trackcall 162 | def accounts(self): 163 | return self._handlereq( 164 | requests.get(self.API_ENDPOINT + "/accounts", 165 | headers=self.authenticated_headers)) 166 | 167 | @trackcall 168 | def update_session(self, data): 169 | return self._handlereq( 170 | requests.put( 171 | self.API_ENDPOINT + "/session", 172 | data=data, 173 | headers=self.authenticated_headers, 174 | )) 175 | 176 | def markets(self, epic_id): 177 | return self._handlereq( 178 | requests.get( 179 | self.API_ENDPOINT + "/markets/" + epic_id, 180 | headers=self.authenticated_headers, 181 | )) 182 | 183 | @trackcall 184 | def clientsentiment(self, market_id): 185 | return self._handlereq( 186 | requests.get( 187 | self.API_ENDPOINT + "/clientsentiment/" + market_id, 188 | headers=self.authenticated_headers, 189 | )) 190 | 191 | @trackcall 192 | def prices(self, epic_id, resolution): 193 | r = self._handlereq( 194 | requests.get( 195 | self.API_ENDPOINT + "/prices/" + epic_id + "/" + resolution, 196 | headers=self.authenticated_headers, 197 | )) 198 | try: 199 | self.allowance = r["allowance"] 200 | except Exception: 201 | pass 202 | return r 203 | 204 | @trackcall 205 | def positions(self, deal_id=None): 206 | if deal_id is None: 207 | url = "/positions" 208 | else: 209 | url = "/positions/" + deal_id 210 | return self._handlereq( 211 | requests.get(self.API_ENDPOINT + url, 212 | headers=self.authenticated_headers)) 213 | 214 | @trackcall 215 | def positions_otc(self, data): 216 | if eval(self.config["Trade"]["always_guarantee_stops"]): 217 | data["guaranteedStop"] = True 218 | if eval(self.config["Trade"]["never_guarantee_stops"]): 219 | data["guaranteedStop"] = False 220 | return self._handlereq( 221 | requests.post( 222 | self.API_ENDPOINT + "/positions/otc", 223 | data=json.dumps(data), 224 | headers=self.authenticated_headers, 225 | )) 226 | 227 | @trackcall 228 | def positions_otc_close(self, data): 229 | return self._handlereq( 230 | requests.post( 231 | self.API_ENDPOINT + "/positions/otc", 232 | data=json.dumps(data), 233 | headers=self._authheadersfordelete(), 234 | )) 235 | 236 | @trackcall 237 | def confirms(self, deal_ref): 238 | return self._handlereq( 239 | requests.get( 240 | self.API_ENDPOINT + "/confirms/" + deal_ref, 241 | headers=self.authenticated_headers, 242 | )) 243 | 244 | def handleDealingRules(self, data): 245 | 246 | market = self.markets(data["epic"]) 247 | dealingRules = market["dealingRules"] 248 | 249 | current_price = float(market["snapshot"]["bid"]) 250 | 251 | r = "marketOrderPreference" 252 | if dealingRules[r] == "NOT_AVAILABLE": 253 | print( 254 | "!!ERROR!! This market is not available for this dealing account" 255 | ) 256 | 257 | r = "maxStopOrLimitDistance" 258 | if dealingRules[r]["unit"] == "PERCENTAGE": 259 | if current_price / 100 * float(dealingRules[r]["value"]) < float( 260 | data["limitDistance"]): 261 | data["limitDistance"] = str( 262 | format( 263 | current_price / 100 * float(dealingRules[r]["value"]), 264 | ".2f")) 265 | elif dealingRules[r]["unit"] == "POINTS": 266 | if float(dealingRules[r]["value"]) < float(data["limitDistance"]): 267 | data["limitDistance"] = str(dealingRules[r]["value"]) 268 | 269 | if ("guaranteedStop" in data and data["guaranteedStop"] 270 | ) or self.config["Trade"]["always_guarantee_stops"]: 271 | r = "minControlledRiskStopDistance" 272 | else: # data['guaranteedStop'] == False 273 | r = "minNormalStopOrLimitDistance" 274 | if dealingRules[r]["unit"] == "PERCENTAGE": 275 | if current_price / 100 * float(dealingRules[r]["value"]) > float( 276 | data["stopDistance"]): 277 | data["stopDistance"] = str( 278 | format( 279 | current_price / 100 * float(dealingRules[r]["value"]), 280 | ".2f")) 281 | elif dealingRules[r]["unit"] == "POINTS": 282 | if float(dealingRules[r]["value"]) > float(data["stopDistance"]): 283 | data["stopDistance"] = str(dealingRules[r]["value"]) 284 | 285 | r = "minDealSize" 286 | if dealingRules[r]["unit"] == "POINTS": 287 | if float(dealingRules[r]["value"]) > float(data["size"]): 288 | data["size"] = str(dealingRules[r]["value"]) 289 | elif dealingRules[r]["unit"] == "PERCENTAGE": 290 | pass # err...what? we have to buy sell a percentage of everything? 291 | 292 | # minStepDistance 293 | # hmmm...i'm not doing this one :D 294 | # if our bid is smaller than the permitted amount, that's because: 295 | # we have no faith in the step being big enough; 296 | # or we don't have permission to make a step that big 297 | # either way, we shouldn't change it just to let the trade go through 298 | # it should fail 299 | 300 | # trailingStopsPreference # TODO 301 | 302 | return data 303 | -------------------------------------------------------------------------------- /ig.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | # Licensed under the Apache License, Version 2.0 (the "License"); 36 | # you may not use this file except in compliance with the License. 37 | # You may obtain a copy of the License at 38 | # 39 | # http://www.apache.org/licenses/LICENSE-2.0 40 | # 41 | # Unless required by applicable law or agreed to in writing, software 42 | # distributed under the License is distributed on an "AS IS" BASIS, 43 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 44 | # See the License for the specific language governing permissions and 45 | # limitations under the License. 46 | 47 | from igclient import IGClient 48 | import igstream 49 | import time as systime 50 | import lib.util 51 | import json 52 | 53 | import random 54 | 55 | 56 | def on_item_update(item_update): 57 | print(item_update) 58 | 59 | 60 | class API(IGClient): 61 | 62 | def __init__(self): 63 | super().__init__() 64 | d = super().session() 65 | self.igstreamclient = igstream.IGStream(igclient=self, loginresponse=d) 66 | 67 | subscription = igstream.Subscription( 68 | mode="DISTINCT", 69 | items=["TRADE:" + str(self.accountId)], 70 | fields=["OPU"]) 71 | 72 | self.igstreamclient.subscribe(subscription=subscription, 73 | listener=on_item_update) 74 | self.market_ids = {} 75 | 76 | # get open positions 77 | self.open_positions = super().positions() 78 | 79 | def clientsentiment(self, epic_id): 80 | market_id = self.get_market_id(epic_id) 81 | return super().clientsentiment(market_id) 82 | 83 | def get_market_id(self, epic_id): 84 | try: 85 | MARKET_ID = self.market_ids[epic_id] 86 | except KeyError: 87 | # lookup and cache - these won't change 88 | d = super().markets(epic_id) 89 | self.market_ids[epic_id] = d["instrument"]["marketId"] 90 | MARKET_ID = self.market_ids[epic_id] 91 | return MARKET_ID 92 | 93 | def fetch_day_highlow(self, epic_id): 94 | subscription = igstream.Subscription( 95 | mode="MERGE", 96 | items=["CHART:{}:HOUR".format(epic_id)], 97 | fields=["LTV", "DAY_LOW", "DAY_HIGH"], 98 | ) 99 | res = self.igstreamclient.fetch_one(subscription) 100 | return res 101 | 102 | def fetch_current_price(self, epic_id): 103 | try: 104 | subscription = igstream.Subscription( 105 | mode="MERGE", 106 | items=["MARKET:{}".format(epic_id)], 107 | fields=[ 108 | "MID_OPEN", 109 | "HIGH", 110 | "LOW", 111 | "CHANGE", 112 | "CHANGE_PCT", 113 | "UPDATE_TIME", 114 | "MARKET_DELAY", 115 | "MARKET_STATE", 116 | "BID", 117 | "OFFER", 118 | ], 119 | ) 120 | res = self.igstreamclient.fetch_one(subscription) 121 | except IndexError: 122 | # fall back to non-stream version 123 | res = super().markets(epic_id) 124 | res["values"] = {} 125 | res["values"]["BID"] = res["snapshot"]["bid"] 126 | res["values"]["OFFER"] = res["snapshot"]["offer"] 127 | res["values"]["CHANGE"] = res["snapshot"]["netChange"] 128 | res["values"]["CHANGE_PCT"] = res["snapshot"]["percentageChange"] 129 | return res 130 | 131 | def placeOrder(self, prediction): 132 | data = self.handleDealingRules(prediction.get_tradedata()) 133 | 134 | d = self.positions_otc(data) 135 | try: 136 | deal_ref = d["dealReference"] 137 | except BaseException: 138 | return 139 | 140 | systime.sleep(2) 141 | # MAKE AN ORDER 142 | 143 | # CONFIRM MARKET ORDER 144 | d = self.confirms(deal_ref) 145 | print("DEAL ID : {} - {} - {}".format(str(d["dealId"]), d["dealStatus"], 146 | d["reason"])) 147 | 148 | if (str(d["reason"]) == "ATTACHED_ORDER_LEVEL_ERROR" or 149 | str(d["reason"]) == "MINIMUM_ORDER_SIZE_ERROR" or 150 | str(d["reason"]) == "INSUFFICIENT_FUNDS" or 151 | str(d["reason"]) == "MARKET_OFFLINE"): 152 | print( 153 | "!!DEBUG!! Not enough wonga in your account for this type of trade!!, Try again!!" 154 | ) 155 | return None 156 | 157 | # let account stream provide updates, and let limit close it (for now) 158 | # TODO: monitor trades with stream thread or waste of a stream? 159 | # Obligatory Wait before doing next order 160 | systime.sleep(random.randint(1, 60)) 161 | self.open_positions = super().positions() 162 | 163 | def find_next_trade(self): 164 | """ 165 | Find our next trade. 166 | 167 | 1) suitable daily price change as % 168 | 2) suitable spread as absolute or % 169 | """ 170 | 171 | epics = json.loads(self.config["Epics"]["EPICS"]) 172 | epic_ids = list(epics.keys()) 173 | 174 | while 1: 175 | random.shuffle(epic_ids) 176 | for epic_id in epic_ids: 177 | print(str(epic_id), end="") 178 | if epic_id in map(lambda x: x["market"]["epic"], 179 | self.open_positions["positions"]): 180 | print(" already have an open position here") 181 | continue 182 | # systime.sleep(2) # we only get 30 API calls per minute :( but 183 | # streaming doesn't count, so no sleep 184 | 185 | res = self.fetch_current_price(epic_id) 186 | res["values"]["EPIC"] = epic_id 187 | 188 | current_price = res["values"]["BID"] 189 | Price_Change_Day = res["values"]["CHANGE"] 190 | 191 | if res["values"]["CHANGE_PCT"] is None: 192 | Price_Change_Day_percent = 0.0 193 | else: 194 | Price_Change_Day_percent = float( 195 | res["values"]["CHANGE_PCT"]) 196 | 197 | Price_Change_Day_percent_h = float( 198 | self.config["Trade"]["Price_Change_Day_percent_high"]) 199 | Price_Change_Day_percent_l = float( 200 | self.config["Trade"]["Price_Change_Day_percent_low"]) 201 | 202 | if (Price_Change_Day_percent_h > Price_Change_Day_percent > 203 | Price_Change_Day_percent_l) or ( 204 | (Price_Change_Day_percent_h * -1) < 205 | Price_Change_Day_percent < 206 | (Price_Change_Day_percent_l * -1)): 207 | print( 208 | " Day Price Change {}% ".format( 209 | str(Price_Change_Day_percent)), 210 | end="", 211 | ) 212 | bid_price = res["values"]["BID"] 213 | ask_price = res["values"]["OFFER"] 214 | spread = float(bid_price) - float(ask_price) 215 | 216 | if eval(self.config["Trade"]["use_max_spread"]): 217 | max_permitted_spread = float( 218 | self.config["Trade"]["max_spread"]) 219 | else: 220 | max_permitted_spread = float( 221 | epics[epic_id]["minspread"] * 222 | float(self.config["Trade"]["spread_multiplier"]) * 223 | -1) 224 | 225 | # if spread is less than -2, It's too big 226 | if float(spread) > max_permitted_spread: 227 | print( 228 | ":- GOOD SPREAD {0:.2f}>{1:.2f}".format( 229 | spread, max_permitted_spread), 230 | end="\n", 231 | flush=True, 232 | ) 233 | return res 234 | else: 235 | print( 236 | ":- spread not ok {0:.2f}<={1:.2f}".format( 237 | spread, max_permitted_spread), 238 | end="\n", 239 | flush=True, 240 | ) 241 | else: 242 | print( 243 | ": Price change {}%".format(Price_Change_Day_percent), 244 | end="\n", 245 | flush=True, 246 | ) 247 | 248 | print("sleeping for 30s, since we've hit the end of the epic list") 249 | systime.sleep(30) # that's all of them 250 | # refresh in case a limit's been hit while we were sleeping 251 | self.open_positions = super().positions() 252 | 253 | def fetch_lg_prices(self, epic_id): 254 | """ 255 | just....don't look 256 | 257 | This fetches the data required for Prediction.linear_regression 258 | This needs a LOT of work to expand/reuse/cleanup, but we might bin it, so...let's see 259 | """ 260 | # Your input data, X and Y are lists (or Numpy Arrays) 261 | # THIS IS YOUR TRAINING DATA 262 | x = [] # This is Low Price, Volume 263 | y = [] # This is High Price 264 | 265 | # disabled - this doesn't actually do anything! 266 | # resolutions = ['DAY/14'] #This is just for the Average True Range, Base it on the last 14 days trading. (14 is the default in ATR) 267 | # for resolution in resolutions: 268 | # d = self.prices(epic_id, resolution) 269 | 270 | # print ("-----------------DEBUG-----------------") 271 | # print ("Remaining API Calls left : " + str(self.allowance['remainingAllowance'])) 272 | # print ("Time to API Key reset : " + str(lib.util.humanize_time(int(self.allowance['allowanceExpiry'])))) 273 | # print ("-----------------DEBUG-----------------") 274 | 275 | # price_ranges = [] 276 | # closing_prices = [] 277 | # TR_prices = [] 278 | 279 | # for count, i in enumerate(d['prices']): 280 | # closePrice = i['closePrice']["bid"] 281 | # closing_prices.append(closePrice) 282 | # high_price = i['highPrice']["bid"] 283 | # low_price = i['lowPrice']["bid"] 284 | # if count == 0: 285 | # #First time round loop cannot get previous 286 | # price_range = float(high_price - closePrice) 287 | # price_ranges.append(price_range) 288 | # else: 289 | # prev_close = closing_prices[-1] 290 | # try: 291 | # price_range = float(high_price - closePrice) 292 | # except Exception: 293 | # print ("No data for {e}.{r}".format(e=epic_id, r=resolution)) 294 | # price_ranges.append(price_range) 295 | # TR = max(high_price-low_price, abs(high_price-prev_close), abs(low_price-prev_close)) 296 | # TR_prices.append(TR) 297 | 298 | # max_range = max(TR_prices) 299 | # # prediction.stopdistance = max_range 300 | # low_range = min(TR_prices) 301 | # if low_range > 10: 302 | # print ("!!DEBUG!! WARNING - Take Profit over high value, Might take a while for this trade!!") 303 | # systime.sleep(1.5) 304 | 305 | high_resolution = eval(self.config["Trade"]["high_resolution"]) 306 | 307 | # Price resolution (MINUTE, MINUTE_2, MINUTE_3, MINUTE_5, MINUTE_10, MINUTE_15, MINUTE_30, HOUR, HOUR_2, HOUR_2, HOUR_4, DAY, WEEK, MONTH) 308 | # This is the high roller, For the price prediction. 309 | if high_resolution: 310 | resolutions = [ 311 | "HOUR/5", "HOUR_2/5", "HOUR_3/5", "HOUR_4/5", "DAY/5" 312 | ] 313 | else: 314 | resolutions = ["HOUR_4/5", "MINUTE_30/5"] 315 | for resolution in resolutions: 316 | d = self.prices(epic_id, resolution) 317 | 318 | for i in d["prices"]: 319 | tmp_list = [] 320 | high_price = i["highPrice"]["bid"] 321 | low_price = i["lowPrice"]["bid"] 322 | close_price = i["closePrice"]["bid"] 323 | ############################################ 324 | volume = i["lastTradedVolume"] 325 | # --------------------------------- 326 | tmp_list.append(float(high_price)) 327 | tmp_list.append(float(low_price)) 328 | x.append(tmp_list) 329 | y.append(float(close_price)) 330 | 331 | return (x, y) 332 | 333 | def fetch_lg_highlow(self, epic_id): 334 | """ 335 | This fetches the data required for Prediction.linear_regression 336 | """ 337 | 338 | ####################################################################### 339 | # Here we just need a value to predict the next one of. 340 | 341 | if eval(self.config["Trade"]["high_resolution"]): 342 | d = self.prices(epic_id, "DAY/1") 343 | 344 | for i in d["prices"]: 345 | high_price = i["highPrice"]["bid"] 346 | low_price = i["lowPrice"]["bid"] 347 | else: 348 | res = self.fetch_day_highlow(epic_id) 349 | low_price = float(res["values"]["DAY_LOW"]) 350 | # this is (now) an hourly volume - will that be an issue? 351 | high_price = float(res["values"]["DAY_HIGH"]) 352 | 353 | return (high_price, low_price) 354 | -------------------------------------------------------------------------------- /igstream.py: -------------------------------------------------------------------------------- 1 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 2 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 3 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 4 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 5 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 6 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 7 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 8 | SOFTWARE.''' 9 | 10 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 11 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 12 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 13 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 14 | 15 | # contact :- github@jamessawyer.co.uk 16 | 17 | 18 | 19 | '''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 22 | NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE 23 | DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE.''' 27 | 28 | # Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk 29 | # Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB 30 | # Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu 31 | # Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd 32 | 33 | 34 | 35 | #!/usr/bin/env python3 36 | 37 | # Copyright (c) Lightstreamer Srl. 38 | # 39 | # Licensed under the Apache License, Version 2.0 (the "License"); 40 | # you may not use this file except in compliance with the License. 41 | # You may obtain a copy of the License at 42 | # 43 | # http://www.apache.org/licenses/LICENSE-2.0 44 | # 45 | # Unless required by applicable law or agreed to in writing, software 46 | # distributed under the License is distributed on an "AS IS" BASIS, 47 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 48 | # See the License for the specific language governing permissions and 49 | # limitations under the License. 50 | 51 | import sys 52 | import logging 53 | import threading 54 | import time 55 | import traceback 56 | 57 | log = logging.getLogger() 58 | 59 | # Modules aliasing and function utilities to support a 60 | # very coarse version differentiation between Python 2 and Python 3. 61 | PY3 = sys.version_info[0] == 3 62 | PY2 = sys.version_info[0] == 2 63 | 64 | if PY3: 65 | from urllib.request import urlopen as _urlopen 66 | from urllib.parse import urlparse as parse_url, urljoin, urlencode 67 | 68 | def _url_encode(params): 69 | return urlencode(params).encode("utf-8") 70 | 71 | def _iteritems(d): 72 | return iter(d.items()) 73 | 74 | def wait_for_input(): 75 | input("{0:-^80}\n".format("HIT CR TO UNSUBSCRIBE AND DISCONNECT FROM \ 76 | LIGHTSTREAMER")) 77 | 78 | else: 79 | from urllib import urlopen as _urlopen, urlencode 80 | from urlparse import urlparse as parse_url 81 | from urlparse import urljoin 82 | 83 | def _url_encode(params): 84 | return urlencode(params) 85 | 86 | def _iteritems(d): 87 | return d.iteritems() 88 | 89 | def wait_for_input(): 90 | raw_input( 91 | "{0:-^80}\n".format("HIT CR TO UNSUBSCRIBE AND DISCONNECT FROM \ 92 | LIGHTSTREAMER")) 93 | 94 | 95 | CONNECTION_URL_PATH = "lightstreamer/create_session.txt" 96 | BIND_URL_PATH = "lightstreamer/bind_session.txt" 97 | CONTROL_URL_PATH = "lightstreamer/control.txt" 98 | 99 | OP = { 100 | "ADD": "add", # Request parameter to create and activate a new Table. 101 | # Request parameter to delete a previously created Table. 102 | "DELETE": "delete", 103 | # Request parameter to force closure of an existing session. 104 | "DESTROY": "destroy", 105 | } 106 | 107 | # List of possible server responses 108 | PROBE_CMD = "PROBE" 109 | END_CMD = "END" 110 | LOOP_CMD = "LOOP" 111 | ERROR_CMD = "ERROR" 112 | SYNC_ERROR_CMD = "SYNC ERROR" 113 | OK_CMD = "OK" 114 | 115 | 116 | class Subscription: 117 | """Represents a Subscription to be submitted to a Lightstreamer Server.""" 118 | 119 | def __init__(self, mode, items, fields, adapter=""): 120 | self.item_names = items 121 | self._items_map = {} 122 | self.field_names = fields 123 | self.adapter = adapter 124 | self.mode = mode 125 | self.snapshot = "true" 126 | self._listeners = [] 127 | self._results = [] 128 | 129 | def _decode(self, value, last): 130 | """Decode the field value according to 131 | Lightstremar Text Protocol specifications. 132 | """ 133 | if value == "$": 134 | return u"" 135 | elif value == "#": 136 | return None 137 | elif not value: 138 | return last 139 | elif value[0] in "#$": 140 | value = value[1:] 141 | 142 | return value 143 | 144 | def addlistener(self, listener): 145 | self._listeners.append(listener) 146 | 147 | def notifyupdate(self, item_line): 148 | """Invoked by LSClient each time Lightstreamer Server pushes 149 | a new item event. 150 | """ 151 | # Tokenize the item line as sent by Lightstreamer 152 | toks = item_line.rstrip("\r\n").split("|") 153 | undecoded_item = dict(list(zip(self.field_names, toks[1:]))) 154 | 155 | # Retrieve the previous item stored into the map, if present. 156 | # Otherwise create a new empty dict. 157 | item_pos = int(toks[0]) 158 | curr_item = self._items_map.get(item_pos, {}) 159 | # Update the map with new values, merging with the 160 | # previous ones if any. 161 | self._items_map[item_pos] = dict([ 162 | (k, self._decode(v, curr_item.get(k))) 163 | for k, v in list(undecoded_item.items()) 164 | ]) 165 | # Make an item info as a new event to be passed to listeners 166 | item_info = { 167 | "pos": item_pos, 168 | "name": self.item_names[item_pos - 1], 169 | "values": self._items_map[item_pos], 170 | } 171 | 172 | self._results.append(item_info) 173 | # Update each registered listener with new event 174 | for on_item_update in self._listeners: 175 | on_item_update(item_info) 176 | 177 | 178 | class LSClient: 179 | """Manages the communication with Lightstreamer Server""" 180 | 181 | def __init__(self, base_url, adapter_set="", user="", password=""): 182 | self._base_url = parse_url(base_url) 183 | self._adapter_set = adapter_set 184 | self._user = user 185 | self._password = password 186 | self._session = {} 187 | self._subscriptions = {} 188 | self._current_subscription_key = 0 189 | self._stream_connection = None 190 | self._stream_connection_thread = None 191 | self._bind_counter = 0 192 | 193 | def _encode_params(self, params): 194 | """Encode the parameter for HTTP POST submissions, but 195 | only for non empty values...""" 196 | return _url_encode(dict([(k, v) for (k, v) in _iteritems(params) if v])) 197 | 198 | def _call(self, base_url, url, body): 199 | """Open a network connection and performs HTTP Post 200 | with provided body. 201 | """ 202 | # Combines the "base_url" with the 203 | # required "url" to be used for the specific request. 204 | url = urljoin(base_url.geturl(), url) 205 | return _urlopen(url, data=self._encode_params(body)) 206 | 207 | def _set_control_link_url(self, custom_address=None): 208 | """Set the address to use for the Control Connection 209 | in such cases where Lightstreamer is behind a Load Balancer. 210 | """ 211 | if custom_address is None: 212 | self._control_url = self._base_url 213 | else: 214 | parsed_custom_address = parse_url("//" + custom_address) 215 | self._control_url = parsed_custom_address._replace( 216 | scheme=self._base_url[0]) 217 | 218 | def _control(self, params): 219 | """Create a Control Connection to send control commands 220 | that manage the content of Stream Connection. 221 | """ 222 | params["LS_session"] = self._session["SessionId"] 223 | response = self._call(self._control_url, CONTROL_URL_PATH, params) 224 | return response.readline().decode("utf-8").rstrip() 225 | 226 | def _read_from_stream(self): 227 | """Read a single line of content of the Stream Connection.""" 228 | line = self._stream_connection.readline().decode("utf-8").rstrip() 229 | return line 230 | 231 | def connect(self): 232 | """Establish a connection to Lightstreamer Server to create 233 | a new session. 234 | """ 235 | self._stream_connection = self._call( 236 | self._base_url, 237 | CONNECTION_URL_PATH, 238 | { 239 | "LS_op2": "create", 240 | "LS_cid": "mgQkwtwdysogQz2BJ4Ji kOj2Bg", 241 | "LS_adapter_set": self._adapter_set, 242 | "LS_user": self._user, 243 | "LS_password": self._password, 244 | }, 245 | ) 246 | 247 | while True: 248 | stream_line = self._read_from_stream() 249 | self._handle_stream(stream_line) 250 | if ":" not in stream_line: 251 | break 252 | 253 | def bind(self): 254 | """Replace a completely consumed connection in listening for an active 255 | Session. 256 | """ 257 | self._stream_connection = self._call( 258 | self._control_url, BIND_URL_PATH, 259 | {"LS_session": self._session["SessionId"]}) 260 | 261 | self._bind_counter += 1 262 | stream_line = self._read_from_stream() 263 | self._handle_stream(stream_line) 264 | 265 | def _handle_stream(self, stream_line): 266 | if stream_line == OK_CMD: 267 | # Parsing session inkion 268 | while True: 269 | next_stream_line = self._read_from_stream() 270 | if next_stream_line: 271 | [param, value] = next_stream_line.split(":", 1) 272 | self._session[param] = value 273 | else: 274 | break 275 | 276 | # Setup of the control link url 277 | self._set_control_link_url(self._session.get("ControlAddress")) 278 | 279 | # Start a new thread to handle real time updates sent 280 | # by Lightstreamer Server on the stream connection. 281 | self._stream_connection_thread = threading.Thread( 282 | name="STREAM-CONN-THREAD-{0}".format(self._bind_counter), 283 | target=self._receive 284 | # args=(self._results[self._current_subscription_key]) 285 | ) 286 | self._stream_connection_thread.setDaemon(True) 287 | self._stream_connection_thread.start() 288 | else: 289 | lines = self._stream_connection.readlines() 290 | lines.insert(0, stream_line) 291 | log.error("Server response error: \n{0}".format("".join(lines))) 292 | raise IOError() 293 | 294 | def _join(self): 295 | """Await the natural STREAM-CONN-THREAD termination.""" 296 | if self._stream_connection_thread: 297 | log.debug("Waiting for thread to terminate") 298 | self._stream_connection_thread.join() 299 | self._stream_connection_thread = None 300 | log.debug("Thread terminated") 301 | 302 | def disconnect(self): 303 | """Request to close the session previously opened with 304 | the connect() invocation. 305 | """ 306 | if self._stream_connection is not None: 307 | # Close the HTTP connection 308 | self._stream_connection.close() 309 | log.debug("Connection closed") 310 | print("DISCONNECTED FROM LIGHTSTREAMER") 311 | else: 312 | log.warning("No connection to Lightstreamer") 313 | 314 | def destroy(self): 315 | """Destroy the session previously opened with 316 | the connect() invocation. 317 | """ 318 | if self._stream_connection is not None: 319 | server_response = self._control({"LS_op": OP["DESTROY"]}) 320 | if server_response == OK_CMD: 321 | # There is no need to explicitly close the connection, 322 | # since it is handled by thread completion. 323 | self._join() 324 | else: 325 | log.warning("No connection to Lightstreamer") 326 | 327 | def subscribe(self, subscription): 328 | """"Perform a subscription request to Lightstreamer Server.""" 329 | # Register the Subscription with a new subscription key 330 | self._current_subscription_key += 1 331 | self._subscriptions[self._current_subscription_key] = subscription 332 | 333 | # Send the control request to perform the subscription 334 | server_response = self._control({ 335 | "LS_session": self._session["SessionId"], 336 | "LS_table": self._current_subscription_key, 337 | "LS_op": OP["ADD"], 338 | # "LS_data_adapter": subscription.adapter, 339 | "LS_mode": subscription.mode, 340 | "LS_schema": " ".join(subscription.field_names), 341 | "LS_id": " ".join(subscription.item_names), 342 | }) 343 | log.debug("Server response ---> <{0}>".format(server_response)) 344 | return self._current_subscription_key 345 | 346 | def unsubscribe(self, subcription_key): 347 | """Unregister the Subscription associated to the 348 | specified subscription_key. 349 | """ 350 | if subcription_key in self._subscriptions: 351 | server_response = self._control({ 352 | "LS_Table": subcription_key, 353 | "LS_op": OP["DELETE"] 354 | }) 355 | log.debug("Server response ---> <{0}>".format(server_response)) 356 | 357 | if server_response == OK_CMD: 358 | del self._subscriptions[subcription_key] 359 | log.debug("Unsubscribed successfully") 360 | else: 361 | log.warning("Server error:" + server_response) 362 | else: 363 | log.warning( 364 | "No subscription key {0} found!".format(subcription_key)) 365 | 366 | def _forward_update_message(self, update_message): 367 | """Forwards the real time update to the relative 368 | Subscription instance for further dispatching to its listeners. 369 | """ 370 | log.debug("Received update message ---> <{0}>".format(update_message)) 371 | tok = update_message.split(",", 1) 372 | table, item = int(tok[0]), tok[1] 373 | if table in self._subscriptions: 374 | self._subscriptions[table].notifyupdate(item) 375 | else: 376 | log.warning("No subscription found!") 377 | 378 | def _receive(self): 379 | rebind = False 380 | receive = True 381 | while receive: 382 | log.debug("Waiting for a new message") 383 | try: 384 | message = self._read_from_stream() 385 | log.debug("Received message ---> <{0}>".format(message)) 386 | except Exception: 387 | log.error("Communication error") 388 | print(traceback.format_exc()) 389 | message = None 390 | 391 | if message is None: 392 | receive = False 393 | log.warning("No new message received") 394 | elif message == PROBE_CMD: 395 | # Skipping the PROBE message, keep on receiving messages. 396 | log.debug("PROBE message") 397 | elif message.startswith(ERROR_CMD): 398 | # Terminate the receiving loop on ERROR message 399 | receive = False 400 | log.error("ERROR") 401 | elif message.startswith(LOOP_CMD): 402 | # Terminate the the receiving loop on LOOP message. 403 | # A complete implementation should proceed with 404 | # a rebind of the session. 405 | log.debug("LOOP") 406 | receive = False 407 | rebind = True 408 | elif message.startswith(SYNC_ERROR_CMD): 409 | # Terminate the receiving loop on SYNC ERROR message. 410 | # A complete implementation should create a new session 411 | # and re-subscribe to all the old items and relative fields. 412 | log.error("SYNC ERROR") 413 | receive = False 414 | elif message.startswith(END_CMD): 415 | # Terminate the receiving loop on END message. 416 | # The session has been forcibly closed on the server side. 417 | # A complete implementation should handle the 418 | # "cause_code" if present. 419 | log.info("Connection closed by the server") 420 | receive = False 421 | elif message.startswith("Preamble"): 422 | # Skipping Preamble message, keep on receiving messages. 423 | log.debug("Preamble") 424 | else: 425 | self._forward_update_message(message) 426 | 427 | if not rebind: 428 | log.debug("Closing connection") 429 | # Clear internal data structures for session 430 | # and subscriptions management. 431 | self._stream_connection.close() 432 | self._stream_connection = None 433 | self._session.clear() 434 | self._subscriptions.clear() 435 | self._current_subscription_key = 0 436 | else: 437 | log.debug("Binding to this active session") 438 | self.bind() 439 | 440 | 441 | class IGStream: 442 | 443 | def __init__(self, igclient=None, loginresponse=None): 444 | from igclient import IGClient 445 | 446 | logging.basicConfig(level=logging.INFO) 447 | 448 | # reuse login if possible, else create session 449 | if igclient is None or loginresponse is None: 450 | igclient = IGClient() 451 | loginresponse = igclient.session() 452 | self.igclient = igclient 453 | self.loginresponse = loginresponse 454 | SERVER = self.loginresponse["lightstreamerEndpoint"] 455 | ACCOUNTID = self.loginresponse["currentAccountId"] 456 | PASSWORD = ("CST-" + self.igclient.auth["CST"] + "|XST-" + 457 | self.igclient.auth["X-SECURITY-TOKEN"]) 458 | 459 | # Establishing a new connection to Lightstreamer Server 460 | log.debug("Starting connection") 461 | self.lightstreamer_client = LSClient(SERVER, "", ACCOUNTID, PASSWORD) 462 | try: 463 | self.lightstreamer_client.connect() 464 | except Exception as e: 465 | print( 466 | "Unable to connect to Lightstreamer Server: {}".format(SERVER)) 467 | print(traceback.format_exc()) 468 | sys.exit(1) 469 | 470 | def fetch_one(self, subscription): 471 | # we may get more than one, or none, in which case this blocks 472 | # so yeah, fetch_one is a terrible name 473 | 474 | # we're going to need a blank listen 475 | def do_nothing(self): 476 | pass 477 | 478 | # set the subscription 479 | sub_key = self.subscribe(subscription=subscription, listener=do_nothing) 480 | 481 | # wait for input THIS IS BLOCKING 482 | count = 0 483 | while 1: 484 | count += 1 485 | if len(self.lightstreamer_client._subscriptions[sub_key]._results 486 | ) > 0: 487 | break 488 | elif count > 10000: # if nothing after 10s, bail 489 | break 490 | else: 491 | time.sleep(000.1) 492 | 493 | # grab the results before we lose it on unsubscribe 494 | ret = self.lightstreamer_client._subscriptions[sub_key]._results 495 | 496 | # clean up 497 | self.unsubscribe(sub_key) 498 | 499 | return ret[0] 500 | 501 | def subscribe(self, subscription, listener): 502 | 503 | # Adding the "on_item_update" function to Subscription 504 | subscription.addlistener(listener) 505 | 506 | # Registering the Subscription 507 | sub_key = self.lightstreamer_client.subscribe(subscription) 508 | return sub_key 509 | 510 | def unsubscribe(self, sub_key): 511 | # Unsubscribing from Lightstreamer by using the subscription key 512 | self.lightstreamer_client.unsubscribe(sub_key) 513 | 514 | def disconnect(self): 515 | # Disconnecting 516 | self.lightstreamer_client.disconnect() 517 | --------------------------------------------------------------------------------