├── .gitmodules ├── __init__.py ├── kuegi_bot ├── bots │ ├── __init__.py │ ├── strategies │ │ ├── __init__.py │ │ ├── entry_filters.py │ │ ├── strat_with_exit_modules.py │ │ └── channel_strat.py │ └── bot_with_channel.py ├── utils │ ├── __init__.py │ ├── errors.py │ ├── dotdict.py │ ├── constants.py │ ├── math.py │ ├── log.py │ ├── telegram.py │ └── helper.py ├── voluba │ ├── __init__.py │ ├── settings.json │ └── aggregator.py ├── exchanges │ ├── __init__.py │ ├── bitmex │ │ ├── __init__.py │ │ ├── ws │ │ │ └── __init__.py │ │ └── auth │ │ │ ├── __init__.py │ │ │ ├── AccessTokenAuth.py │ │ │ ├── APIKeyAuthWithExpires.py │ │ │ └── APIKeyAuth.py │ ├── bybit │ │ ├── __init__.py │ │ └── bybit_websocket.py │ ├── huobi │ │ ├── __init__.py │ │ ├── huobi_websocket.py │ │ └── huobi_interface.py │ ├── kraken │ │ ├── __init__.py │ │ ├── kraken_websocket.py │ │ └── kraken_interface.py │ ├── phemex │ │ ├── __init__.py │ │ ├── phemex_websocket.py │ │ └── client.py │ ├── bitfinex │ │ ├── __init__.py │ │ ├── bitfinex_websocket.py │ │ └── bitfinex_interface.py │ ├── bitstamp │ │ ├── __init__.py │ │ ├── bitstamp_websocket.py │ │ └── bitstmap_interface.py │ ├── coinbase │ │ ├── __init__.py │ │ ├── coinbase_websocket.py │ │ └── coinbase_interface.py │ └── bybit_linear │ │ ├── __init__.py │ │ └── bybitlinear_websocket.py ├── indicators │ ├── __init__.py │ ├── MeanStd.py │ ├── swings.py │ ├── HMA.py │ ├── indicator.py │ └── kuegi_channel.py ├── __init__.py └── random_bot.py ├── setup.cfg ├── docs ├── kuegiBotStructure.png ├── README.md ├── aboutCodingABot │ ├── NonLinearity.md │ ├── readme.md │ ├── riskmanagement.md │ ├── performanceNumbers.md │ ├── startingAsATrader.md │ ├── sampleStrategies │ │ ├── MeanReversion.md │ │ └── MACross.md │ ├── HowToBuildABot.md │ ├── darkSideOfTrading.md │ ├── myOwnTradingFramework.md │ ├── 1yearRecap.md │ └── howIBecameProfitable.md ├── docs.json ├── kuegiBotStructure.drawio ├── kuegiChannel.ps └── ChannelStrategy.ps ├── requirements.txt ├── .gitignore ├── docker ├── history.sh ├── bot.sh ├── lighttpd.conf └── README.md ├── dashboard ├── readme.md ├── dashboard.html ├── main.css ├── templates │ └── openPositions.handlebars └── main.js ├── settings └── defaults.json ├── TODO.md ├── volubaFE ├── main.css └── voluba.html ├── setup.py ├── voluba.py ├── Dockerfile ├── scripts.py ├── history_crawler.py ├── backtest.py ├── exchange_test.py └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/bots/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/voluba/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/indicators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/bots/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitmex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bybit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/huobi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/kraken/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/phemex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitfinex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitmex/ws/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitstamp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/coinbase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bybit_linear/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kuegi_bot/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = 'v1.1' 2 | 3 | 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /docs/kuegiBotStructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuegi/kuegiBot/HEAD/docs/kuegiBotStructure.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | plotly>=5.1.0 2 | requests>=2.26.0 3 | future>=0.18.2 4 | websocket-client==1.1.0 5 | pybit>=5.8.0 -------------------------------------------------------------------------------- /kuegi_bot/utils/errors.py: -------------------------------------------------------------------------------- 1 | class AuthenticationError(Exception): 2 | pass 3 | 4 | class MarketClosedError(Exception): 5 | pass 6 | 7 | class MarketEmptyError(Exception): 8 | pass 9 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitmex/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from kuegi_bot.exchanges.bitmex.auth.AccessTokenAuth import * 2 | from kuegi_bot.exchanges.bitmex.auth.APIKeyAuth import * 3 | from kuegi_bot.exchanges.bitmex.auth.APIKeyAuthWithExpires import * -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist/ 3 | *.egg-info/ 4 | env 5 | venv/ 6 | .idea/ 7 | history/ 8 | results/ 9 | logs/ 10 | openPositions/ 11 | dashboard.json 12 | positionHistory/ 13 | *.tpl.js 14 | volubaFE/*.json 15 | volubaFE/data/ 16 | exports/ 17 | -------------------------------------------------------------------------------- /kuegi_bot/utils/dotdict.py: -------------------------------------------------------------------------------- 1 | class dotdict(dict): 2 | """dot.notation access to dictionary attributes""" 3 | def __getattr__(self, attr): 4 | return self.get(attr) 5 | __setattr__ = dict.__setitem__ 6 | __delattr__ = dict.__delitem__ 7 | -------------------------------------------------------------------------------- /docker/history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PUID=${PUID:-911} 4 | PGID=${PGID:-911} 5 | 6 | groupmod -o -g "$PGID" abc 7 | usermod -o -u "$PUID" abc 8 | 9 | 10 | chown -R abc:abc /history 11 | 12 | sudo -u abc /venv/bin/python3 -u /app/history_crawler.py $1 $2 13 | -------------------------------------------------------------------------------- /kuegi_bot/utils/constants.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | # Constants 3 | XBt_TO_XBT = 100000000 4 | VERSION = 'v1.1' 5 | try: 6 | VERSION = str(subprocess.check_output(["git", "describe", "--tags"], stderr=subprocess.DEVNULL).rstrip()) 7 | except Exception as e: 8 | # git not available, ignore 9 | pass 10 | -------------------------------------------------------------------------------- /dashboard/readme.md: -------------------------------------------------------------------------------- 1 | # How to access 2 | 3 | To test locally - navigate into the dashboard folder and execute the following command: 4 | 5 | ``` 6 | python3 -m http.server 8000 7 | ``` 8 | 9 | ## Available routes 10 | 11 | - Old Dashboard: http://localhost:8000/dashboard.html 12 | - New Dashboard: http://localhost:8000/index.html 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # How to access 2 | 3 | To read the docs in a nicer way locally navigate into your root directory of your kuegiBot and than execute the following command to run a local server. 4 | 5 | ``` 6 | python3 -m http.server 8000 7 | ``` 8 | 9 | ## Available routes 10 | 11 | - Learn: http://localhost:8000/docs/index.html 12 | -------------------------------------------------------------------------------- /settings/defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "DASHBOARD_FILE": "dashboard.json", 3 | "LOOP_INTERVAL": 5, 4 | "API_REST_INTERVAL": 1, 5 | "API_ERROR_INTERVAL": 10, 6 | "TIMEOUT": 7, 7 | "LOG_LEVEL": "INFO", 8 | "EXCHANGE": "bitmex", 9 | "API_KEY": "", 10 | "API_SECRET": "", 11 | "SYMBOL": "XBTUSD", 12 | "LOG_TO_CONSOLE": true, 13 | "LOG_TO_FILE": true, 14 | "IS_TEST": true 15 | } -------------------------------------------------------------------------------- /docker/bot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PUID=${PUID:-911} 4 | PGID=${PGID:-911} 5 | CONFIG=${CONFIG:-defaults.json} 6 | 7 | groupmod -o -g "$PGID" abc 8 | usermod -o -u "$PUID" abc 9 | 10 | chown -R abc:abc /app 11 | chown -R abc:abc /var/www 12 | chown -R abc:abc /settings 13 | chown -R abc:abc /logs 14 | 15 | echo $CONFIG 16 | 17 | sudo -u abc lighttpd -f /lighttpd.conf 18 | sudo -u abc /venv/bin/python3 -u /app/cryptobot.py /settings/$CONFIG 19 | -------------------------------------------------------------------------------- /docker/lighttpd.conf: -------------------------------------------------------------------------------- 1 | server.port = 8282 2 | server.document-root = "/var/www/" 3 | 4 | dir-listing.activate = "disable" 5 | 6 | server.username = "lighttpd" 7 | server.groupname = "lighttpd" 8 | 9 | mimetype.assign = ( 10 | ".html" => "text/html", 11 | ".txt" => "text/plain", 12 | ".jpg" => "image/jpeg", 13 | ".png" => "image/png" 14 | ) 15 | 16 | static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" ) 17 | index-file.names = ( "dashboard.html" ) 18 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Voluba 2 | ## BE 3 | * [x] fix bitfinex "tu" (update to trade info) 4 | * [ ] add orderbook info. first step: add bid/ask size of 1% into book 5 | * [ ] switch to (bid+ask)/2 instead of last traded price for bars? 6 | 7 | ## FE 8 | * [ ] add query params for TF and choosen exchanges 9 | * [ ] update queryparams when settings are changed. 10 | * [x] save settings in localStorage 11 | * [x] add google analytics 12 | * [ ] add reflinks to exchanges and bybit links 13 | 14 | # KuegiBot -------------------------------------------------------------------------------- /kuegi_bot/utils/math.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | def toNearest(num, tickSize): 4 | """Given a number, round it to the nearest tick. Very useful for sussing float error 5 | out of numbers: e.g. toNearest(401.46, 0.01) -> 401.46, whereas processing is 6 | normally with floats would give you 401.46000000000004. 7 | Use this after adding/subtracting/multiplying numbers.""" 8 | tickDec = Decimal(str(tickSize)) 9 | return float((Decimal(round(num / tickSize, 0)) * tickDec)) 10 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitmex/auth/AccessTokenAuth.py: -------------------------------------------------------------------------------- 1 | from requests.auth import AuthBase 2 | 3 | 4 | class AccessTokenAuth(AuthBase): 5 | 6 | """Attaches Access Token Authentication to the given Request object.""" 7 | 8 | def __init__(self, accessToken): 9 | """Init with Token.""" 10 | self.token = accessToken 11 | 12 | def __call__(self, r): 13 | """Called when forming a request - generates access token header.""" 14 | if (self.token): 15 | r.headers['access-token'] = self.token 16 | return r 17 | -------------------------------------------------------------------------------- /volubaFE/main.css: -------------------------------------------------------------------------------- 1 | .exchangeColor { 2 | width:10px; 3 | height:10px; 4 | display:inline-block; 5 | margin-left:5px; 6 | margin-right:5px; 7 | } 8 | 9 | .lastPrice { 10 | margin-left:5px; 11 | margin-right:5px; 12 | } 13 | 14 | #exchanges { 15 | margin-top:10px; 16 | } 17 | 18 | #chart { 19 | position:absolute; 20 | left:0px; 21 | top:0px; 22 | width:90%; 23 | height:100%; 24 | } 25 | 26 | #inputs { 27 | position:absolute; 28 | right:0px; 29 | top:0px; 30 | width:10%; 31 | height:100%; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cryptobot Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /kuegi_bot/voluba/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "LOG_LEVEL": "DEBUG", 3 | "LOG_TO_CONSOLE": true, 4 | "LOG_TO_FILE": false, 5 | "dataPath": "volubaFE/data/", 6 | "exchanges": [ 7 | { 8 | "id": "bitstamp", 9 | "SYMBOL": "btcusd" 10 | }, 11 | { 12 | "id": "binance", 13 | "SYMBOL": "btcusdt" 14 | }, 15 | { 16 | "id": "huobi", 17 | "SYMBOL": "btcusdt" 18 | }, 19 | { 20 | "id": "coinbase", 21 | "SYMBOL": "BTC-USD" 22 | }, 23 | { 24 | "id": "kraken", 25 | "SYMBOL": "XBT/USD" 26 | }, 27 | { 28 | "id": "bitfinex", 29 | "SYMBOL": "tBTCUSD" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /kuegi_bot/random_bot.py: -------------------------------------------------------------------------------- 1 | from kuegi_bot.trade_engine import TradingBot, Order, Account, OrderInterface 2 | import random 3 | 4 | 5 | class RandomBot(TradingBot): 6 | 7 | def __init__(self): 8 | super().__init__() 9 | 10 | def open_orders(self, bars: list, account: Account): 11 | if account.open_position.quantity != 0: 12 | if random.randint(0,100) > 80: 13 | self.order_interface.send_order(Order(amount=-account.open_position.quantity)) 14 | else: 15 | if random.randint(0,100) > 50: 16 | amount= random.randint(-5,5) 17 | if amount != 0: 18 | self.order_interface.send_order(Order(amount=amount)) -------------------------------------------------------------------------------- /dashboard/main.css: -------------------------------------------------------------------------------- 1 | .botContainer { 2 | border: 1px solid #000; 3 | margin: 5px; 4 | padding: 5px; 5 | border-radius: 10px; 6 | } 7 | 8 | .botContainer table { 9 | width:100%; 10 | } 11 | 12 | td { 13 | text-align:right; 14 | padding:2px 4px; 15 | } 16 | 17 | tr.head td { 18 | text-align:left !important; 19 | font-weight: bold; 20 | } 21 | 22 | tr.openLong { 23 | background-color:#0d0 24 | } 25 | 26 | tr.openShort { 27 | background-color:#d00 28 | } 29 | 30 | tr.pendingLong { 31 | background-color:#dfd 32 | } 33 | 34 | tr.pendingShort { 35 | background-color:#fdd 36 | } 37 | 38 | tr td#result { 39 | background-color:#bbb 40 | } 41 | 42 | tr.winning td#result { 43 | background-color:#0d0 44 | } 45 | tr.losing td#result { 46 | background-color:#d00 47 | } -------------------------------------------------------------------------------- /kuegi_bot/bots/strategies/entry_filters.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | 4 | from kuegi_bot.bots.strategies.strat_with_exit_modules import EntryFilter 5 | from kuegi_bot.utils.trading_classes import Bar 6 | 7 | 8 | class DayOfWeekFilter(EntryFilter): 9 | 10 | def __init__(self, allowedDaysMask: int): 11 | super().__init__() 12 | self.allowedDaysMask= allowedDaysMask 13 | 14 | def init(self, logger): 15 | super().init(logger) 16 | self.logger.info("init DayOfWeek {0:b}".format(self.allowedDaysMask)) 17 | 18 | def entries_allowed(self,bars:List[Bar]): 19 | dayOfWeek= datetime.fromtimestamp(bars[0].tstamp).weekday() 20 | mask = 1 << dayOfWeek 21 | return (self.allowedDaysMask & mask) != 0 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | from os.path import dirname, join 4 | 5 | import kuegi_bot 6 | 7 | 8 | here = dirname(__file__) 9 | 10 | 11 | setup(name='kuegi-bot', 12 | version=kuegi_bot.__version__, 13 | description='Cryptobot for executing multiple strategies', 14 | url='https://github.com/kuegi/kuegiBot', 15 | long_description=open(join(here, 'README.md')).read(), 16 | long_description_content_type='text/markdown', 17 | author='Kuegi', 18 | author_email='mythosMatheWG@gmail.com', 19 | install_requires=[ 20 | 'requests', 21 | 'websocket-client', 22 | 'future', 23 | 'plotly', 24 | 'bybit' 25 | ], 26 | packages=find_packages(), 27 | scripts=["cryptobot.py"], 28 | classifiers=["Development Status :: 3 - Alpha" ] 29 | ) 30 | -------------------------------------------------------------------------------- /voluba.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from time import sleep 3 | 4 | from kuegi_bot.utils import log 5 | from kuegi_bot.utils.helper import load_settings_from_args 6 | from kuegi_bot.voluba.aggregator import VolubaAggregator 7 | 8 | 9 | def run(settings): 10 | try: 11 | voluba= VolubaAggregator(logger=logger,settings=settings) 12 | while True: 13 | voluba.aggregate_data() 14 | voluba.serialize_current_data() 15 | sleep(10) 16 | except Exception as e: 17 | logger.error("exception in main loop:\n "+ traceback.format_exc()) 18 | 19 | 20 | if __name__ == '__main__': 21 | settings = load_settings_from_args() 22 | logger = log.setup_custom_logger("voluba", 23 | log_level=settings.LOG_LEVEL, 24 | logToConsole=settings.LOG_TO_CONSOLE, 25 | logToFile=settings.LOG_TO_FILE) 26 | run(settings) 27 | -------------------------------------------------------------------------------- /kuegi_bot/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | import os 4 | 5 | 6 | def setup_custom_logger(name="kuegi_bot",log_level=logging.INFO, 7 | logToConsole= True,logToFile= False): 8 | logger = logging.getLogger(name) 9 | logger.setLevel(log_level) 10 | 11 | if len(logger.handlers) == 0: 12 | if logToConsole: 13 | handler = logging.StreamHandler() 14 | handler.setFormatter(logging.Formatter(fmt='%(asctime)s - %(levelname)s:%(name)s - %(module)s - %(message)s')) 15 | logger.addHandler(handler) 16 | 17 | if logToFile: 18 | base = 'logs/' 19 | try: 20 | os.makedirs(base) 21 | except Exception: 22 | pass 23 | fh = logging.handlers.RotatingFileHandler(base+name+'.log', mode='a', maxBytes=200*1024, backupCount=50) 24 | fh.setFormatter(logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s')) 25 | fh.setLevel(logging.INFO) 26 | logger.addHandler(fh) 27 | 28 | return logger 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine as build-env 2 | 3 | # set the working directory in the container 4 | WORKDIR /code 5 | 6 | RUN apk add --no-cache build-base linux-headers wget 7 | 8 | RUN python3 -m venv /venv 9 | 10 | # copy the dependencies file to the working directory 11 | COPY requirements.txt . 12 | COPY Binance_Futures_python ./Binance_Futures_python 13 | 14 | # install dependencies 15 | RUN /venv/bin/pip install -r requirements.txt 16 | 17 | RUN cd Binance_Futures_python && /venv/bin/python3 setup.py install 18 | 19 | 20 | FROM python:alpine 21 | 22 | EXPOSE 8282 23 | 24 | RUN apk add --no-cache bash shadow lighttpd sudo 25 | 26 | RUN echo "**** create abc user ****" && \ 27 | groupmod -g 1000 users && \ 28 | useradd -u 911 -U -d /app -s /bin/false abc && \ 29 | usermod -G users abc 30 | 31 | COPY --from=build-env /venv /venv 32 | # copy the content of the local src directory to the working directory 33 | COPY ./docker / 34 | COPY . /app 35 | COPY ./history_crawler.py /app/ 36 | COPY ./dashboard /var/www 37 | 38 | VOLUME /settings 39 | VOLUME /logs 40 | 41 | # command to run on container start 42 | CMD [ "/bot.sh" ] 43 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/NonLinearity.md: -------------------------------------------------------------------------------- 1 | linear things are easy to understand. there are no surprises or unexpected behaviour. But reallity is never linear. We just use linear approximations to get a grasp on complex problems. If you want to be better than the herd, you better embrace nonlinearity. 2 | 3 | Why is reality not linear? Simple examples: is going double the speed on the highway make your car use double the amount of gas? is working out 2 hours instead of 1 make you double that strong? or twice as tired? No. 4 | 5 | Same with economics: if 1 bottle of milk cost 1$. How much are 10 bottles? probably 10$. Or 8$ cause its a special offer. Or much more cause the store has only 5 left -> nonlinearity 6 | 7 | So why do you use linear approximations? Like SMA (linear average), fixed-dist trailing stops etc.? because they are easy to understand and simple to apply. But guess what: Thats why everybody is doing it, and thats why they rarely work. 8 | 9 | Better wrap your head around non linear concepts. They can also be simple, but mostly need more effort to apply them. like trailing only on broken thresholds, using ParabolicTrailings, nonlinear calculation of positionSize... -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitmex/auth/APIKeyAuthWithExpires.py: -------------------------------------------------------------------------------- 1 | from requests.auth import AuthBase 2 | import time 3 | from kuegi_bot.exchanges.bitmex.auth import generate_signature 4 | 5 | 6 | class APIKeyAuthWithExpires(AuthBase): 7 | 8 | """Attaches API Key Authentication to the given Request object. This implementation uses `expires`.""" 9 | 10 | def __init__(self, apiKey, apiSecret): 11 | """Init with Key & Secret.""" 12 | self.apiKey = apiKey 13 | self.apiSecret = apiSecret 14 | 15 | def __call__(self, r): 16 | """ 17 | Called when forming a request - generates api key headers. This call uses `expires` instead of nonce. 18 | 19 | This way it will not collide with other processes using the same API Key if requests arrive out of order. 20 | For more details, see https://www.bitmex.com/app/apiKeys 21 | """ 22 | # modify and return the request 23 | expires = int(round(time.time()) + 5) # 5s grace period in case of clock skew 24 | r.headers['api-expires'] = str(expires) 25 | r.headers['api-key'] = self.apiKey 26 | r.headers['api-signature'] = generate_signature(self.apiSecret, r.method, r.url, expires, r.body or '') 27 | 28 | return r 29 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/readme.md: -------------------------------------------------------------------------------- 1 | My Path to becoming a successful trader, written in (sometimes pretty long) twitter threads. 2 | 3 | As this whole repo is mainly for myself to code and document stuff, this part is also my log to prepare and document the threads i posted. 4 | 5 | - Storytime: [Starting as a trader](startingAsATrader.md) 6 | - Don't plan a plan if you can't follow throu: [How i became profitable](howIBecameProfitable.md) 7 | - The structure behind: [my own trading framework in python](myOwnTradingFramework.md) 8 | - How to survive the learning curve: [Riskmanagement](riskmanagement.md) 9 | - How to build an automated trading system: [Building my Bot](HowToBuildABot.md) 10 | - Size matters: [Performance numbers and which i like](performanceNumbers.md) 11 | - the good, the bad and the ugly: [Could you stand the heat?](darkSideOfTrading.md) 12 | - non-linearity: [nothing is a straight line](NonLinearity.md) 13 | - entries vs. exits 14 | - benefits of bottrading: taking signals 24/7 15 | - improving your strategy: why backtesting is so important 16 | - do not overfit: the power of oos 17 | - 1 year of bot trading: [a recap](1yearRecap.md) 18 | 19 | sample implementations: i plan to implement some basic ideas into strategies, to show the process. Will write about it here: 20 | - [SMA Cross](sampleStrategies/MACross.md) 21 | - RSI entries -------------------------------------------------------------------------------- /volubaFE/voluba.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VoluBa 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | TF (minutes):
24 |
25 | 26 |
Exchanges
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitstamp/bitstamp_websocket.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 4 | 5 | 6 | class BitstampWebsocket(KuegiWebsocket): 7 | 8 | def __init__(self, wsURLs, api_key, api_secret, logger, callback, symbol): 9 | self.data = {} 10 | self.symbol = symbol 11 | super().__init__(wsURLs, api_key, api_secret, logger, callback) 12 | 13 | def subscribeTrades(self): 14 | param = {'event': 'bts:subscribe', 15 | 'data': {"channel": "live_trades_"+self.symbol} 16 | } 17 | self.ws.send(json.dumps(param)) 18 | key = "trade" 19 | if key not in self.data: 20 | self.data[key] = [] 21 | 22 | def subscribe_realtime_data(self): 23 | self.subscribeTrades() 24 | 25 | def on_message(self, message): 26 | """Handler for parsing WS messages.""" 27 | message = json.loads(message) 28 | 29 | if message['event'] == 'trade': 30 | self.data[message['event']].append(message["data"]) 31 | if self.callback is not None: 32 | self.callback(message['event']) 33 | 34 | def get_data(self, topic): 35 | if topic not in self.data: 36 | self.logger.info(" The topic %s is not subscribed." % topic) 37 | return [] 38 | if len(self.data[topic]) == 0: 39 | return [] 40 | return self.data[topic].pop() 41 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/riskmanagement.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1254395889570979841?s=20 2 | 3 | 40% of traders quit after 1 month. Only 7% make it for 5 years. If you manage to survive the first years you probably get successful. It took me 2.5 years. And i made back the losses of 2.5 years in 3 months. How: risk- and Moneymanagement. a thread. 4 | 5 | RM is what makes you survive your drawdowns. If you have no edge yet, it makes you survive the time until you find one. To have meaningful RM, you need numbers: Your max Drawdown, your max loosing streak, your max handleable drop in accountsize. 6 | 7 | The last one is really important! You can try to trade a system with inital risk per position of 10%, knowing that your max DD is 30% (cause of high winrate) and make a killing (by not adapting the positition size on every trade). 8 | 9 | But in reallity you probably can't really trade it like that. you will start to cut winners short, trail stops to fast cause of to much money on the line making you itchy. in the end you loose money. 10 | 11 | Choose your positionsize so that you don't even blink if you lost it. Choose your risk in a way that double your expected max DD won't make you stop trading. In the beginning, RM is about surving the rampup. After that its about staying sane and in the game. 12 | 13 | There is far more information available if you follow the right ppl like 14 | @SJosephBurns 15 | https://twitter.com/SJosephBurns/status/1253398626895900673 16 | 17 | @CryptoCred 18 | https://twitter.com/exitscammed/status/1253364261272854533 19 | 20 | also good follows: 21 | @SalsaTekila @pierre_crypt0 @imBagsy @JoshManMode 22 | -------------------------------------------------------------------------------- /dashboard/templates/openPositions.handlebars: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{#each this}} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{#each positions}} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {{/each}} 41 | {{/each}} 42 | 43 |
PositionIdstatussignalTimeamountwEntryinitSLentryTimeentrystopworstRisk
{{id}}{{formatTime last_tick_tstamp}}{{totalPos}}{{equity}}{{max_equity}}{{drawdown}} {{uwdays}} d{{formatResult totalWorstCase}} R={{risk_reference}}
{{id}}{{status}}{{formatTime signal_tstamp}}{{amount}}{{formatPrice wanted_entry}}{{formatPrice initial_stop}}{{formatTime entry_tstamp}}{{formatPrice filled_entry}}{{formatPrice currentStop}}{{formatResult worstCase}}{{formatPrice initialRisk}}
44 |
-------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | Build the docker image 5 | ---------------------- 6 | 7 | Make sure to also have the Binance submodule checked out 8 | 9 | ``` 10 | git submodule init 11 | git submodule update 12 | ``` 13 | 14 | Then the docker build command should work without any issues 15 | ``` 16 | docker build -t kuegibot . 17 | ``` 18 | 19 | 20 | Define the docker containers 21 | ---------------------------- 22 | The easiest way to setup the container is by using docker-compose. 23 | 24 | ### Volumes 25 | 26 | - /settings - the path to your settings 27 | - /logs - the bot logoutput if defined 28 | - /history - output of the history crawler 29 | 30 | ### Environment variables 31 | - PUID - current user id 32 | - PGID - current group id 33 | - CONFIG - your settings file in /settings, "defaults.json" if not set 34 | 35 | 36 | ```yaml 37 | 38 | version: '3.4' 39 | services: 40 | kuegibot: 41 | image: kuegibot 42 | container_name: kuegibot 43 | restart: always 44 | ports: 45 | - 8282:8282 46 | volumes: 47 | - ./data/kuegibot/settings:/settings 48 | - ./data/kuegibot/logs:/logs 49 | environment: 50 | - PUID=1000 51 | - PGID=1000 52 | - CONFIG=settings.json 53 | 54 | 55 | kuegibot_history: 56 | image: kuegibot 57 | container_name: kuegibot_history 58 | command: /history.sh bybit 59 | restart: "no" 60 | volumes: 61 | - ./data/kuegibot/history:/history 62 | environment: 63 | - PUID=1000 64 | - PGID=1000 65 | ``` 66 | 67 | Start them 68 | ---------- 69 | ``` 70 | docker-compose up -d kuegibot 71 | docker-compose up -d kuegibot_history 72 | ``` 73 | 74 | After that, the dashboard is available at http://:8282 75 | 76 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/coinbase/coinbase_websocket.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | 4 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 5 | 6 | 7 | class CoinbaseWebsocket(KuegiWebsocket): 8 | 9 | def __init__(self, wsURLs, api_key, api_secret, logger, callback, symbol): 10 | self.data = {} 11 | self.symbol = symbol 12 | self.id= 1 13 | super().__init__(wsURLs, api_key, api_secret, logger, callback) 14 | 15 | def subscribeTrades(self): 16 | param = {'type': "subscribe", 17 | "product_ids": [self.symbol], 18 | 'channels': ["ticker"] 19 | } 20 | self.id += 1 21 | self.ws.send(json.dumps(param)) 22 | key = "trade" 23 | if key not in self.data: 24 | self.data[key] = [] 25 | 26 | def subscribe_realtime_data(self): 27 | self.subscribeTrades() 28 | 29 | def on_message(self, message): 30 | """Handler for parsing WS messages.""" 31 | try: 32 | topic= None 33 | message = json.loads(message) 34 | if message['type'] == "ticker": 35 | topic = "trade" 36 | 37 | self.data[topic].append(message) 38 | if self.callback is not None and topic is not None: 39 | self.callback(topic) 40 | 41 | except Exception as e: 42 | self.logger.error("exception in on_message: "+str(e)) 43 | raise e 44 | 45 | def get_data(self, topic): 46 | if topic not in self.data: 47 | self.logger.info(" The topic %s is not subscribed." % topic) 48 | return [] 49 | if len(self.data[topic]) == 0: 50 | return [] 51 | return self.data[topic].pop() 52 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/performanceNumbers.md: -------------------------------------------------------------------------------- 1 | In developing trading strategies and specially with bots, you always need a way to measure the performance of a system. There are heaps of different numbers out there and even more ppl arguing which number to pursue. :thread: 2 | 3 | Many numbers are useless on their own. Like Winrate: Have a 90% winrate but avg. looser loses 10x your average winner? you will feel like a winner while the big losses get you broke. 10% WR with avg win of 10x your avg loss? you feel like a looser but lambo soon! 4 | 5 | Same with win/loss-ratio: means nothing without the winrate. For me, the importance in those numbers is that they need to fit your mindset. You need constant gratification? shoot for high WR. You don't care about looses but hate cutting winners? pump that win/loss ratio. 6 | 7 | You need a system that fits you, cause you need to execute it. So shoot for performance numbers that fit. For me, the most important number is yearlyProfit/maxDD. Cause thats what makes all the diff systems comparable. I have a level of max pain that i allow for every system... 8 | 9 | so i scale the position size up to that level. more position size more profit, but also more DD. So i set posSize to have an expected maxDD of 10%. Now my number tells me what to expect per year. its kinda scaling-invariant with fixed level of max pain. 10 | 11 | Second number i look at is the underwater-days: so the maximum number of days between equity ATHs. Or the max time of pain. Cause being under water sucks. Being under water for a long period of time makes you question your strategy and do stupid things. so better reduce it. 12 | 13 | All the other numbers are mainly of informative nature for me. I look at time, i monitor them during trading, but i don't care if high or low. I just look for deviations from the backtest. 14 | 15 | and i hate pain. 16 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/kraken/kraken_websocket.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | 4 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 5 | 6 | 7 | class KrakenWebsocket(KuegiWebsocket): 8 | 9 | def __init__(self, wsURLs, api_key, api_secret, logger, callback, symbol): 10 | self.data = {} 11 | self.symbol = symbol 12 | self.id= 1 13 | super().__init__(wsURLs, api_key, api_secret, logger, callback) 14 | 15 | def subscribeTrades(self): 16 | param = {'event': "subscribe", 17 | "pair": [self.symbol], 18 | 'subscription': { "name": "trade" } 19 | } 20 | self.id += 1 21 | self.ws.send(json.dumps(param)) 22 | key = "trade" 23 | if key not in self.data: 24 | self.data[key] = [] 25 | 26 | def subscribe_realtime_data(self): 27 | self.subscribeTrades() 28 | 29 | def on_message(self, message): 30 | """Handler for parsing WS messages.""" 31 | try: 32 | topic= None 33 | message = json.loads(message) 34 | if isinstance(message,list) and len(message) == 4: 35 | if message[2] == "trade": 36 | topic = "trade" 37 | for data in message[1]: 38 | self.data[topic].append(data) 39 | if self.callback is not None and topic is not None: 40 | self.callback(topic) 41 | 42 | except Exception as e: 43 | self.logger.error("exception in on_message: "+message+"\nerror: "+str(e)) 44 | raise e 45 | 46 | def get_data(self, topic): 47 | if topic not in self.data: 48 | self.logger.info(" The topic %s is not subscribed." % topic) 49 | return [] 50 | if len(self.data[topic]) == 0: 51 | return [] 52 | return self.data[topic].pop() 53 | -------------------------------------------------------------------------------- /kuegi_bot/utils/telegram.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import threading 3 | 4 | class TelegramBot: 5 | def __init__(self,logger,settings): 6 | self.logger= logger 7 | self.token= settings.token 8 | self.logChatId= settings.logChatId 9 | 10 | if self.logChatId is None: 11 | self.logger.warn("missing telegram logChatId, on purpose?") 12 | self.executionChannel= settings.executionChannel 13 | if self.executionChannel is None: 14 | self.logger.warn("missing telegram executionChannel, on purpose?") 15 | self.timer= None 16 | self.messagesToSend = {} 17 | 18 | def send_log(self,log_message,debounceId:str= None): 19 | if self.logChatId is None: 20 | return 21 | 22 | self.__internal_send(self.logChatId, log_message) 23 | ''' 24 | # if doing debounce, but might have problems with id collision on multiple ids 25 | if debounceId is None: 26 | debounceId= log_message 27 | if self.timer is not None: 28 | self.timer.cancel() 29 | 30 | self.timer= threading.Timer(interval=35, function= self.__internal_send_logs) 31 | self.timer.start() 32 | self.messagesToSend[debounceId] = log_message 33 | ''' 34 | 35 | def send_execution(self, signal_message): 36 | if self.executionChannel is not None: 37 | self.__internal_send(self.executionChannel, signal_message) 38 | 39 | def __internal_send_logs(self): 40 | self.timer= None 41 | for key, msg in self.messagesToSend.items(): 42 | self.__internal_send(self.logChatId,msg) 43 | self.messagesToSend= {} 44 | 45 | def __internal_send(self,chat_id,message): 46 | if self.token is None: 47 | self.logger.warn("missing telegram token or chatId") 48 | return 49 | 50 | url = 'https://api.telegram.org/bot' + self.token + '/sendMessage?chat_id=' + chat_id+ '&text=' + message 51 | 52 | result= requests.get(url).json() 53 | if not result["ok"]: 54 | self.logger.warning("error sending telegram messages "+str(result)) 55 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/huobi/huobi_websocket.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | 4 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 5 | 6 | 7 | class HuobiWebsocket(KuegiWebsocket): 8 | 9 | def __init__(self, wsURLs, api_key, api_secret, logger, callback, symbol): 10 | self.data = {} 11 | self.symbol = symbol 12 | self.id= 1 13 | super().__init__(wsURLs, api_key, api_secret, logger, callback) 14 | 15 | def subscribeTrades(self): 16 | param = {'sub': "market."+self.symbol+".trade.detail", 17 | 'id': "id"+str(self.id) 18 | } 19 | self.id += 1 20 | self.ws.send(json.dumps(param)) 21 | key = "trade" 22 | if key not in self.data: 23 | self.data[key] = [] 24 | 25 | def subscribe_realtime_data(self): 26 | self.subscribeTrades() 27 | 28 | def on_message(self, message): 29 | """Handler for parsing WS messages.""" 30 | try: 31 | message = gzip.decompress(message) 32 | message = json.loads(message) 33 | if "ping" in message: 34 | self.ws.send(json.dumps({"pong": message['ping']})) 35 | elif "ch" in message: 36 | if message['ch'] == "market."+self.symbol+".trade.detail": 37 | topic= "trade" 38 | for data in message['tick']['data']: 39 | self.data[topic].append(data) 40 | if self.callback is not None: 41 | self.callback(topic) 42 | except Exception as e: 43 | self.logger.error("exception in on_message: "+str(e)) 44 | raise e 45 | 46 | def get_data(self, topic): 47 | if topic not in self.data: 48 | self.logger.info(" The topic %s is not subscribed." % topic) 49 | return [] 50 | if len(self.data[topic]) == 0: 51 | return [] 52 | return self.data[topic].pop() 53 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitmex/auth/APIKeyAuth.py: -------------------------------------------------------------------------------- 1 | from requests.auth import AuthBase 2 | import time 3 | import hashlib 4 | import hmac 5 | from future.builtins import bytes 6 | from future.standard_library import hooks 7 | with hooks(): # Python 2/3 compat 8 | from urllib.parse import urlparse 9 | 10 | 11 | class APIKeyAuth(AuthBase): 12 | 13 | """Attaches API Key Authentication to the given Request object.""" 14 | 15 | def __init__(self, apiKey, apiSecret): 16 | """Init with Key & Secret.""" 17 | self.apiKey = apiKey 18 | self.apiSecret = apiSecret 19 | 20 | def __call__(self, r): 21 | """Called when forming a request - generates api key headers.""" 22 | # modify and return the request 23 | nonce = generate_expires() 24 | r.headers['api-expires'] = str(nonce) 25 | r.headers['api-key'] = self.apiKey 26 | r.headers['api-signature'] = generate_signature(self.apiSecret, r.method, r.url, nonce, r.body or '') 27 | 28 | return r 29 | 30 | 31 | def generate_expires(): 32 | return int(time.time() + 3600) 33 | 34 | 35 | # Generates an API signature. 36 | # A signature is HMAC_SHA256(secret, verb + path + nonce + data), hex encoded. 37 | # Verb must be uppercased, url is relative, nonce must be an increasing 64-bit integer 38 | # and the data, if present, must be JSON without whitespace between keys. 39 | # 40 | # For example, in psuedocode (and in real code below): 41 | # 42 | # verb=POST 43 | # url=/api/v1/order 44 | # nonce=1416993995705 45 | # data={"symbol":"XBTZ14","quantity":1,"price":395.01} 46 | # signature = HEX(HMAC_SHA256(secret, 'POST/api/v1/order1416993995705{"symbol":"XBTZ14","quantity":1,"price":395.01}')) 47 | def generate_signature(secret, verb, url, nonce, data): 48 | """Generate a request signature compatible with BitMEX.""" 49 | # Parse the url so we can remove the base and extract just the path. 50 | parsedURL = urlparse(url) 51 | path = parsedURL.path 52 | if parsedURL.query: 53 | path = path + '?' + parsedURL.query 54 | 55 | if isinstance(data, (bytes, bytearray)): 56 | data = data.decode('utf8') 57 | 58 | # print "Computing HMAC: %s" % verb + path + str(nonce) + data 59 | message = verb + path + str(nonce) + data 60 | 61 | signature = hmac.new(bytes(secret, 'utf8'), bytes(message, 'utf8'), digestmod=hashlib.sha256).hexdigest() 62 | return signature 63 | -------------------------------------------------------------------------------- /kuegi_bot/bots/strategies/strat_with_exit_modules.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from functools import reduce 3 | 4 | from kuegi_bot.bots.strategies.exit_modules import ExitModule 5 | from kuegi_bot.bots.trading_bot import TradingBot 6 | from kuegi_bot.bots.MultiStrategyBot import Strategy 7 | from kuegi_bot.utils.trading_classes import Bar, Account, Symbol, OrderType 8 | 9 | 10 | class EntryFilter: 11 | def __init__(self): 12 | self.logger= None 13 | 14 | def init(self, logger): 15 | self.logger = logger 16 | 17 | def entries_allowed(self,bars:List[Bar]): 18 | pass 19 | 20 | class StrategyWithExitModulesAndFilter(Strategy): 21 | 22 | def __init__(self): 23 | super().__init__() 24 | self.exitModules = [] 25 | self.entryFilters= [] 26 | 27 | def withExitModule(self, module: ExitModule): 28 | self.exitModules.append(module) 29 | return self 30 | 31 | def withEntryFilter(self, filter: EntryFilter): 32 | self.entryFilters.append(filter) 33 | return self 34 | 35 | def init(self, bars: List[Bar], account: Account, symbol: Symbol): 36 | super().init(bars, account, symbol) 37 | for module in self.exitModules: 38 | module.init(self.logger,symbol) 39 | for fil in self.entryFilters: 40 | fil.init(self.logger) 41 | 42 | def got_data_for_position_sync(self, bars: List[Bar]) -> bool: 43 | return reduce((lambda x, y: x and y.got_data_for_position_sync(bars)), self.exitModules, True) 44 | 45 | def get_stop_for_unmatched_amount(self, amount: float, bars: List[Bar]): 46 | for module in self.exitModules: 47 | exit = module.get_stop_for_unmatched_amount(amount, bars) 48 | if exit is not None: 49 | return exit 50 | return None 51 | 52 | def manage_open_order(self, order, position, bars, to_update, to_cancel, open_positions): 53 | orderType = TradingBot.order_type_from_order_id(order.id) 54 | if orderType == OrderType.SL: 55 | for module in self.exitModules: 56 | module.manage_open_order(order, position, bars, to_update, to_cancel, open_positions) 57 | 58 | def entries_allowed(self,bars:List[Bar]): 59 | for filter in self.entryFilters: 60 | if not filter.entries_allowed(bars): 61 | return False 62 | 63 | return True 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/docs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "/docs/aboutCodingABot/startingAsATrader.md", 4 | "title": "Starting as a Trader", 5 | "preTitle": "Storytime", 6 | "author": "Kügi", 7 | "type": "Article" 8 | }, 9 | { 10 | "path": "/docs/aboutCodingABot/howIBecameProfitable.md", 11 | "title": "How I Became Profitable", 12 | "preTitle": "Don't plan a plan if you can't follow throu", 13 | "author": "Kügi", 14 | "type": "Article" 15 | }, 16 | { 17 | "path": "/docs/aboutCodingABot/myOwnTradingFramework.md", 18 | "title": "My Own Trading Framework in python", 19 | "preTitle": "The structure behind", 20 | "author": "Kügi", 21 | "type": "Article" 22 | }, 23 | { 24 | "path": "/docs/aboutCodingABot/riskmanagement.md", 25 | "title": "Risk Management", 26 | "preTitle": "How to survive the learning curve", 27 | "author": "Kügi", 28 | "type": "Article" 29 | }, 30 | { 31 | "path": "/docs/aboutCodingABot/HowToBuildABot.md", 32 | "title": "Building my Bot", 33 | "preTitle": "How to build an automated trading system", 34 | "author": "Kügi", 35 | "type": "Article" 36 | }, 37 | { 38 | "path": "/docs/aboutCodingABot/performanceNumbers.md", 39 | "title": "Performance Numbers", 40 | "preTitle": "Size matters", 41 | "author": "Kügi", 42 | "type": "Article" 43 | }, 44 | { 45 | "path": "/docs/aboutCodingABot/darkSideOfTrading.md", 46 | "title": "Dark Side of Trading", 47 | "preTitle": "The good, the bad and the ugly", 48 | "author": "Kügi", 49 | "type": "Article" 50 | }, 51 | { 52 | "path": "/docs/aboutCodingABot/NonLinearity.md", 53 | "title": "nothing is a straight line", 54 | "preTitle": "Non Linearity", 55 | "author": "Kügi", 56 | "type": "Article" 57 | }, 58 | { 59 | "path": "/docs/aboutCodingABot/1yearRecap.md", 60 | "title": "1 year of bot trading", 61 | "preTitle": "a recap", 62 | "author": "Kügi", 63 | "type": "Article" 64 | }, 65 | { 66 | "path": "/docs/aboutCodingABot/sampleStrategies/MACross.md", 67 | "title": "SMA Cross", 68 | "preTitle": "", 69 | "author": "Kügi", 70 | "type": "Sample Strategy" 71 | } 72 | ] -------------------------------------------------------------------------------- /kuegi_bot/indicators/MeanStd.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | from kuegi_bot.indicators.indicator import Indicator, get_bar_value, highest, lowest, BarSeries, clean_range 5 | from kuegi_bot.trade_engine import Bar 6 | from kuegi_bot.utils import log 7 | 8 | logger = log.setup_custom_logger() 9 | 10 | 11 | class Data: 12 | def __init__(self, mean, std, sum): 13 | self.mean = mean 14 | self.std= std 15 | self.sum= sum 16 | 17 | 18 | class MeanStd(Indicator): 19 | ''' Mean and Standard deviation 20 | ''' 21 | 22 | def __init__(self, period: int): 23 | super().__init__("MeanStd" + str(period)) 24 | self.period = period 25 | 26 | def on_tick(self, bars: List[Bar]): 27 | first_changed = 0 28 | for idx in range(len(bars)): 29 | if bars[idx].did_change: 30 | first_changed = idx 31 | else: 32 | break 33 | 34 | for idx in range(first_changed, -1, -1): 35 | bar= bars[idx] 36 | if idx < len(bars) - self.period: 37 | data = self.get_data(bar) 38 | sum = 0 39 | sqsum= 0 40 | if data is not None: 41 | sum = data.sum + bar.close - bars[idx+self.period].close 42 | else: 43 | for sub in bars[idx:idx + self.period]: 44 | sum += sub.close 45 | mean = sum/self.period 46 | 47 | for sub in bars[idx:idx + self.period]: 48 | sqsum += (sub.close-mean)*(sub.close-mean)/self.period 49 | self.write_data(bar, Data(mean=mean,std= math.sqrt(sqsum),sum=sum)) 50 | else: 51 | self.write_data(bar, None) 52 | 53 | def get_number_of_lines(self): 54 | return 3 55 | 56 | def get_line_styles(self): 57 | return [{"width": 1, "color": "blue"}, 58 | {"width": 1, "color": "orange"}, 59 | {"width": 1, "color": "orange"} 60 | ] 61 | 62 | def get_line_names(self): 63 | return ["mean" + str(self.period), 64 | "mean+std" + str(self.period), 65 | "mean-std" + str(self.period) 66 | ] 67 | 68 | def get_data_for_plot(self, bar: Bar): 69 | data= self.get_data(bar) 70 | if data is not None: 71 | return [data.mean, data.mean+data.std, data.mean-data.std] 72 | else: 73 | return [None, None, None] 74 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitfinex/bitfinex_websocket.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import json 3 | 4 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 5 | 6 | 7 | class BitfinexWebsocket(KuegiWebsocket): 8 | 9 | def __init__(self, wsURLs, api_key, api_secret, logger, callback, symbol): 10 | self.data = {} 11 | self.symbol = symbol 12 | self.channelIds = {} 13 | super().__init__(wsURLs, api_key, api_secret, logger, callback) 14 | 15 | def subscribeTrades(self): 16 | param = {'event': "subscribe", 17 | 'channel': "trades", 18 | "symbol": self.symbol 19 | } 20 | self.ws.send(json.dumps(param)) 21 | key = "trade" 22 | if key not in self.data: 23 | self.data[key] = [] 24 | key = "tradeupdate" 25 | if key not in self.data: 26 | self.data[key] = [] 27 | 28 | def subscribe_realtime_data(self): 29 | self.subscribeTrades() 30 | 31 | def on_message(self, message): 32 | """Handler for parsing WS messages.""" 33 | try: 34 | message = json.loads(message) 35 | if "event" in message: 36 | if message["event"] == "subscribed": 37 | self.channelIds[message["chanId"]] = message["channel"]; 38 | if isinstance(message,list) and message[0] in self.channelIds: 39 | topic= None 40 | if message[1] == 'hb': #heartbeat 41 | return 42 | if self.channelIds[message[0]] == "trades": 43 | topic = "trade" 44 | if len(message) == 2: # snapshot 45 | for data in message[1]: 46 | self.data[topic].append(data) 47 | else: #update 48 | if message[1] == "tu": 49 | topic = "tradeupdate" 50 | self.data[topic].append(message[2]) 51 | 52 | if self.callback is not None and topic is not None: 53 | self.callback(topic) 54 | except Exception as e: 55 | self.logger.error("exception in on_message: "+str(e)) 56 | raise e 57 | 58 | def get_data(self, topic): 59 | if topic not in self.data: 60 | self.logger.info(" The topic %s is not subscribed." % topic) 61 | return [] 62 | if len(self.data[topic]) == 0: 63 | return [] 64 | return self.data[topic].pop() 65 | -------------------------------------------------------------------------------- /kuegi_bot/indicators/swings.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from kuegi_bot.indicators.indicator import Indicator, BarSeries, lowest, highest 4 | from kuegi_bot.utils.trading_classes import Bar 5 | 6 | 7 | class Data: 8 | def __init__(self, swingHigh, swingLow): 9 | self.swingHigh = swingHigh 10 | self.swingLow = swingLow 11 | 12 | 13 | class Swings(Indicator): 14 | 15 | def __init__(self, before: int = 2, after: int = 2): 16 | super().__init__("Swings(" + str(before) + "," + str(after) + ")") 17 | self.before = before 18 | self.after = after 19 | 20 | def on_tick(self, bars: List[Bar]): 21 | # ignore first bars 22 | for idx in range(len(bars) - self.before - self.after-2, -1, -1): 23 | if bars[idx].did_change: 24 | self.process_bar(bars[idx:]) 25 | 26 | def process_bar(self, bars: List[Bar]): 27 | prevData: Data = self.get_data(bars[1]) 28 | 29 | swingHigh = prevData.swingHigh if prevData is not None else None 30 | highestAfter = highest(bars, self.after, 1, BarSeries.HIGH) 31 | candidate = bars[self.after + 1].high 32 | highestBefore = highest(bars, self.before, self.after + 2, BarSeries.HIGH) 33 | if highestAfter <= candidate and highestBefore <= candidate: 34 | swingHigh = candidate 35 | if swingHigh is not None and bars[0].high > swingHigh: 36 | swingHigh= None 37 | 38 | swingLow = prevData.swingLow if prevData is not None else None 39 | lowestAfter = lowest(bars, self.after, 1, BarSeries.LOW) 40 | candidate = bars[self.after + 1].low 41 | lowestBefore = lowest(bars, self.before, self.after + 2, BarSeries.LOW) 42 | if lowestAfter >= candidate and lowestBefore >= candidate: 43 | swingLow = candidate 44 | if swingLow is not None and bars[0].low < swingLow: 45 | swingLow= None 46 | 47 | self.write_data(bars[0], Data(swingHigh=swingHigh, swingLow=swingLow)) 48 | 49 | def get_data_for_plot(self, bar: Bar): 50 | data: Data = self.get_data(bar) 51 | if data is not None: 52 | return [data.swingHigh, data.swingLow] 53 | else: 54 | return [bar.close, bar.close] 55 | 56 | def get_plot_offset(self): 57 | return 1 58 | 59 | def get_number_of_lines(self): 60 | return 2 61 | 62 | def get_line_styles(self): 63 | return [{"width": 1, "color": "green"}, {"width": 1, "color": "red"}] 64 | 65 | def get_line_names(self): 66 | return ["swingHigh", "swingLow"] 67 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/startingAsATrader.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1249076445432987659?s=20 2 | 3 | I started coding trading bots at the university (back in 2004) but didn't become profitable until recently. 4 | Sounds worse than it is, i had a 7 year break, but still. There is a lesson here. thread incoming. 1/ 5 | 6 | A friend got me into the financial world and i had a theoretical go on everything. 7 | Started with futures (motivated by the turtles system) and later moved to FOREX. 8 | I tried to learn all the indicators, all the strategies, applied and combined every indicator i could find. 2/ 9 | 10 | (mathematician: i love systems and theories) But with low budget i basically never really traded. 11 | All systems where backtested (and curvefitted) to the max. 12 | Sometimes even ran in forwardtests (on paper accounts), but they never made it to a live system. 3/ 13 | 14 | They just never fulfilled my performance and risk goals in the forward tests. 15 | and i wasn't in a position to "just risk it". 16 | IMHO one of the main things you need to start trading: "fuck it"-money for your first trading accounts. 17 | Because lets face it: you will burn them. 4/ 18 | 19 | And you need the mentality to just risk it. 20 | You need to be fine with "investing" that trading account into your education. 21 | If you get profitable before its gone: perfect. 22 | But if not, your reaction must be "i learned definitly more than i lost, so thats fine". 5/ 23 | 24 | OTOH, not risking real money back then made me learn a lot about trading, the theory and the markets without actually losing money. 25 | By backtesting hundreds of strategies you see how different models of riskmanagement, moneymanagement and performance numbers behave. 6/ 26 | 27 | I even wrote my own exchange with matching-engine and market-maker to get some deeper understanding of it all. 28 | And still, with all that knowledge and experience: when i started trading real money i burned it. 7/ 29 | 30 | Still risk averse as fuck, so not much in absolute numbers. but more than 1 account. 31 | I would def have lost far more if i didn't know the importance (and ways) of RM. 32 | But without putting money on the line, without knowing the man in the mirror, your knowledge is worthless. 8/ 33 | 34 | So if you think about going that road: read all you can. learn the basics (specially riskmanagement) that make the difference between gambling and trading. 35 | And then make a plan, put money on the line and get your feet wet. there is no way around it. 9/ 36 | 37 | If ppl like this thread, i will tell you how i made my plan, how it failed and then worked out. how i got profitable and whatever you wanna know about it. just leave a comment. 38 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/phemex/phemex_websocket.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 5 | from kuegi_bot.exchanges.phemex.client import Client 6 | 7 | 8 | def get_current_timestamp(): 9 | return int(round(time.time() * 1000)) 10 | 11 | 12 | class PhemexWebsocket(KuegiWebsocket): 13 | 14 | def __init__(self, wsURLs, api_key, api_secret, logger, callback,symbol, minutesPerBar): 15 | """Initialize""" 16 | self.auth_id = 0 17 | self.symbol= symbol 18 | self.minutesPerBar= minutesPerBar 19 | super().__init__(wsURLs, api_key, api_secret, logger, callback) 20 | 21 | def send(self, method, params=None): 22 | channel = dict() 23 | channel["method"] = method 24 | channel["params"] = params if params is not None else [] 25 | channel["id"] = get_current_timestamp() 26 | if method == "user.auth": 27 | self.auth_id = channel['id'] 28 | self.ws.send(json.dumps(channel)) 29 | 30 | def subscribe_realtime_data(self): 31 | self.subscribe_account_updates() 32 | subbarsIntervall = 1 if self.minutesPerBar <= 60 else 60 33 | self.subscribe_candlestick_event(self.symbol, subbarsIntervall) 34 | 35 | def do_auth(self): 36 | self.logger.info("doing auth") 37 | self.auth_id = 0 38 | [signature, expiry] = Client.generate_signature(message=self.api_key, api_secret=self.api_secret) 39 | self.send("user.auth", ["API", self.api_key, signature, expiry]) 40 | 41 | def on_message(self, message): 42 | """Handler for parsing WS messages.""" 43 | message = json.loads(message) 44 | if 0 < self.auth_id == message['id']: 45 | self.auth = True 46 | self.auth_id = 0 47 | self.logger.info("authentication success") 48 | return 49 | 50 | result = None 51 | responseType = None 52 | if 'error' in message and message['error'] is not None: 53 | self.logger.error("error in ws reply: " + message) 54 | self.on_error(message) 55 | if "accounts" in message and message['accounts']: 56 | # account update 57 | responseType = "account" 58 | result = message 59 | 60 | if "kline" in message and message['kline'] and "type" in message: 61 | responseType = "kline" 62 | result = message 63 | 64 | if self.callback is not None and result is not None: 65 | try: 66 | self.callback(responseType, result) 67 | except Exception as e: 68 | self.logger.error("Exception in callback: " + str(e) + "\n message: " + str(message)) 69 | 70 | def subscribe_candlestick_event(self, symbol: str, intervalMinutes: int): 71 | self.send("kline.subscribe", [symbol, intervalMinutes * 60]) 72 | 73 | def subscribe_account_updates(self): 74 | self.send("aop.subscribe") 75 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/sampleStrategies/MeanReversion.md: -------------------------------------------------------------------------------- 1 | Time for a sample strat. This time: Mean reversion. As always when you code a strategy we have 3 questions: 2 | - when to enter 3 | - when to exit 4 | - what size 5 | size will be based on the initial stopdiff again. Since we base this strat on mean reversion, lets first explain that. 6 | 7 | Every random list of numbers (like a price), has a mean and standard deviation: how much the numbers usually strive away from the mean. The mean might shift over time. But when the price moves far away from it, it might be likely to revert back. Or at least torward it. 8 | 9 | Based on this simple idea, we build our strat: If the price moves away from the mean by a certain level (multiple of the std-dev) we expect it to return by a certain degree. lets say to a smaller multiple of the std-dev 10 | 11 | in numbers: 68% of the values should be within +-1 std-dev. lets put a limit entry on that level, and if it triggers, we set the TP at 0.5 std-dev, and the stop at 2 std-dev (95% stay within that). The numbers will be parameters of course. 12 | 13 | This strat works best on a "random" series. So i would go for LTF (M2 in our case) since its a lot of "noise" there (aka: random moves up and down) which is exactly what we want. So our parameters are: 14 | * entry-factor 15 | * tp-factor 16 | * sl-factor 17 | * lookback for the mean 18 | 19 | Now comes the coding. To have it seperated, lets make an indicator that gives us the mean and the std-deviation. Then use this in the strat. Seems like this again will be a pretty simple one. 20 | 21 | So far so good. First test: entry at stdDev, TP at 0.5 stdDev, SL at 2 stdDev looks kinda disapointing thou. lets look at the chart. maybe we can spot something that can be improved. 22 | 23 | What jumps out is this pattern. blue line is the mean, orange, the stdDev. green lines are long positions, red lines are shorts. Now why is this big long happening? happy to hear your explanation for it. 24 | 25 | Ok, so maybe the numbers are wrong? running a quick scan shows that basically nothing works. So are the assumptions wrong? are prices not normally distributed? Not necessarily. It only means that our way of trying to benefit from it didn't work yet. 26 | 27 | The problem is that the mean is shifting. So the price is certainly reverting back to the mean. Just that the mean is now different than before which means our position is not reaching the target (which is relative to the old mean). 28 | 29 | Generally thats the main problem with mean reversion systems: It most certainly reverts back to the mean, but the mean might have shifted by a lot in the meantime. How to deal with that? we could only enter in the direction of the trend (so a mean shift works in our favor) 30 | 31 | or we could get out of the trade at the next close (so don't even give the mean time to shift). Either way: to make a mean-reversion work, you need a good filter that prevents you from getting trapped by a shifting mean. If you got that, it can work really well. 32 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/HowToBuildABot.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1254494415583948801?s=20 2 | 3 | Let's say you found a #trading strategy that works, but executing it flawlessly is challenging. So you want to build it into a bot that does all the hard work for you? Then this thread is for you. 4 | 5 | Disclaimer: This is just my experience from building 100+ bots on different plattforms. But only 2 of them were profitable, so i might know nothing. This is not financial advice and not every strategy can be automized. 6 | 7 | To build a bot, *everything* in your strategy needs to be defined and written down in detail. You need to be able to give it to any trader in the world and after explaining it 1 day, they would be able to execute it just like you. If you can't do that -> forget it. 8 | 9 | Also you need to write code, or someone that writes code for you. Depending on how much you trust others, it might be smart to learn it yourself. What language depends on the platform you use. Writing everything from scratch in python is always an option. (i did that) 10 | 11 | So you know basic coding and have your strategy defined. My first step would be to code it in some easy-to-use platform. I use TV but also heard good things bout ThinkOrSwim. This might only work for the simple parts of your sys, but you get an idea where this is heading. 12 | 13 | Why start out in TV or similar? By actually trying to code it the first time, you probably find lots of flaws in your definitions and process. Might even rethink the whole sys. Better do that on a fast iteration platform than after coding for 2 months. 14 | 15 | While writing the sys for the first time you will realize problems in the automation. Either because you don't know how to code it, or cause the platform is not providing it. In case 2: try to work around it. Get as much of it coded as possible. 16 | 17 | Even having parts of your sys automated helps a lot. f.e. an indicator showing you possible entry-signals where you only have to apply the final filter. Or automating the exit so you don't have to think about it but just enter numbers in the trading mask. 18 | 19 | Such tools may be as simple as an excel calculation (like i did for position size based on entry and SL). everything that gets more of the logic into the machine and reduces the human factor, will help. And over time you might adapt your system into full automation. 20 | 21 | If you finally come to the conclusion that the simple platforms are not enough, take the next step: decide on a powerful platform (with access to your broker) and build it there. The platform decision depends 100% on the broker and the sys. so no recommendations there. 22 | 23 | There are lots of pitfalls when writing a bot. I documented some of my exp in writing it in python here: https://twitter.com/mkuegi/status/1251223614424317953?s=20 Backtesting, Overoptimizing, API-troubles and the diff between papertrading/backtest and real executions fill books. 24 | 25 | But if you manage to build a bot, you are free to enjoy life at its fullest while your bot executes your trading (flawlessly) 24/7. Which is the dream, right? ;) 26 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/darkSideOfTrading.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1288433020547076099?s=20 2 | 3 | My bot just made 70% within 5 days while i was lying in the sun enjoying time with my family. Sounds awesome right? Who wouldn't want that? But would you stand the heat that lead up to this? 4 | Let me take you on a small tour throu reality :thread: 5 | 6 | The bot started to trade with "serious" size in feb 2020 and it hit the ground running. First 3 months saw returns of avg. 15% per month which was in sync with the backtest results. Backtest also showed months with 10% DD but also 100% gains so :dollar: 7 | 8 | For 3 months it showed "continuous" profit: there have been weeks of DDs, sometimes even 10%, but every month ended in nice gains. So i decided to raise it up a bit and increased risk by 50% (cause it had made around that profit till then). 9 | 10 | And thats when BTC started to dry up. My bot gains from volatility and the typical strong BTC moves. And they just stopped happening. For 50 days vola was just decreasing, leading my bot into more and more DD. And with the increased risk, it hurt even more. 11 | 12 | June was the first negative month with -15%. I hated it but cause of the increased risk it was still "to be expected" cause of the -10% months from the backtest. At least thats what kept me from stopping it. But the situation got tough. 13 | 14 | Mid of July there was still no end in sight. The bot operates on 4 exchanges already. 2 of them were running low on equity cause of the DD so i had to stop there. Refilling took time cause of the blocked mempool and with the loosing strike i started questioning the whole thing. 15 | 16 | On 21. of July the bot had lost all of the previous gains of the year. I questioned everything. I was ready to stop the whole thing and restart from scratch cause "it stopped working". 17 | Luckily my wife questioned my questioning. So phei analyzed deeper and realized how the whole market had changed. 18 | 19 | I found the root cause of the DD (the ever decreasing volatility) and came to the conclusion that this won't continue forever and when it ends, will probably give a good trade for the bot. so i kept it running. 20 | 21 | Truth be told, i still had the mental "it might go to zero and i start over again" mindset. And thats important cause my bot-account is of a size that i could do that (losing it wouldn't kill me). And then the market changed. Lucky me it did as i hoped. 22 | 23 | Cause of the low vola, the position (across multiple exchanges) had a tight SL and therefore "big" position size (every trade has similar risk per trade here) -> lead to a massive gain in taking the run from 9500 up to 11k. In total over 70% gain in 5 days. 24 | 25 | So in the end i went from "nice stable gains" to "loosing it all" to "lambo soon" within 5 months. And if it wasn't my bot where i know the strengths and weaknesses and can tell why the DD is happening and how to react, i would have cut it, loosing it all. 26 | 27 | Takeaway: don't run/trade bots or strats that you don't fully know and understand. You will have bigger DDs than expected and you will have to make tough decisions. Without knowing all the details you will probably make the wrong ones and loose it all. 28 | 29 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitstamp/bitstmap_interface.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | from kuegi_bot.utils.trading_classes import Bar 5 | from .bitstamp_websocket import BitstampWebsocket 6 | from ..ExchangeWithWS import ExchangeWithWS 7 | 8 | 9 | class BitstampInterface(ExchangeWithWS): 10 | 11 | def __init__(self, settings, logger, on_tick_callback=None, on_api_error=None): 12 | self.on_api_error = on_api_error 13 | self.m1_bars: List[Bar] = [] 14 | hosts = ["wss://ws.bitstamp.net/"] # no testnet on bitstamp 15 | super().__init__(settings, logger, 16 | ws=BitstampWebsocket(wsURLs=hosts, 17 | api_key=settings.API_KEY, 18 | api_secret=settings.API_SECRET, 19 | logger=logger, 20 | callback=self.socket_callback, 21 | symbol=settings.SYMBOL), 22 | on_tick_callback=on_tick_callback) 23 | 24 | def init(self): 25 | self.ws.subscribe_realtime_data() 26 | self.logger.info("subscribed to data") 27 | 28 | def get_instrument(self, symbol=None): 29 | return None 30 | 31 | def initOrders(self): 32 | pass 33 | 34 | def initPositions(self): 35 | pass 36 | 37 | def get_ticker(self, symbol=None): 38 | pass 39 | 40 | def get_bars(self, timeframe_minutes, start_offset_minutes, min_bars_needed) -> List[Bar]: 41 | if timeframe_minutes == 1: 42 | return self.m1_bars 43 | else: 44 | raise NotImplementedError 45 | 46 | def socket_callback(self, topic): 47 | try: 48 | data = self.ws.get_data(topic) 49 | gotTick = False 50 | while len(data) > 0: 51 | if topic == 'trade': 52 | tstamp = int(data['timestamp']) 53 | bar_time = math.floor(tstamp / 60) * 60 54 | price = data['price'] 55 | volume = data['amount'] 56 | if len(self.m1_bars) > 0 and self.m1_bars[-1].tstamp == bar_time: 57 | last_bar = self.m1_bars[-1] 58 | else: 59 | last_bar = Bar(tstamp=bar_time, open=price, high=price, low=price, close=price, volume=0) 60 | self.m1_bars.append(last_bar) 61 | gotTick = True 62 | last_bar.close = price 63 | last_bar.low = min(last_bar.low, price) 64 | last_bar.high = max(last_bar.high, price) 65 | last_bar.volume += volume 66 | last_bar.last_tick_tstamp = tstamp 67 | if data['type'] == 0: 68 | last_bar.buyVolume += volume 69 | else: 70 | last_bar.sellVolume += volume 71 | 72 | data = self.ws.get_data(topic) 73 | 74 | # new bars is handling directly in the messagecause we get a new one on each tick 75 | if gotTick and self.on_tick_callback is not None: 76 | self.on_tick_callback(fromAccountAction=False) 77 | except Exception as e: 78 | self.logger.error("error in socket data(%s): %s " % (topic, str(e))) 79 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/kraken/kraken_interface.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | from kuegi_bot.utils.trading_classes import Bar, parse_utc_timestamp 5 | from .kraken_websocket import KrakenWebsocket 6 | from ..ExchangeWithWS import ExchangeWithWS 7 | 8 | 9 | class KrakenInterface(ExchangeWithWS): 10 | 11 | def __init__(self, settings, logger, on_tick_callback=None, on_api_error=None): 12 | self.on_api_error = on_api_error 13 | self.m1_bars: List[Bar] = [] 14 | hosts = ["wss://ws.kraken.com"] # no testnet on spot 15 | super().__init__(settings, logger, 16 | ws=KrakenWebsocket(wsURLs=hosts, 17 | api_key=settings.API_KEY, 18 | api_secret=settings.API_SECRET, 19 | logger=logger, 20 | callback=self.socket_callback, 21 | symbol=settings.SYMBOL), 22 | on_tick_callback=on_tick_callback) 23 | 24 | def init(self): 25 | self.ws.subscribe_realtime_data() 26 | self.logger.info("subscribed to data") 27 | 28 | def get_instrument(self, symbol=None): 29 | return None 30 | 31 | def initOrders(self): 32 | pass 33 | 34 | def initPositions(self): 35 | pass 36 | 37 | def get_ticker(self, symbol=None): 38 | pass 39 | 40 | def get_bars(self, timeframe_minutes, start_offset_minutes, min_bars_needed) -> List[Bar]: 41 | if timeframe_minutes == 1: 42 | return self.m1_bars 43 | else: 44 | raise NotImplementedError 45 | 46 | def socket_callback(self, topic): 47 | try: 48 | data = self.ws.get_data(topic) 49 | gotTick = False 50 | while len(data) > 0: 51 | if topic == 'trade': 52 | tstamp = float(data[2]) 53 | bar_time = math.floor(tstamp / 60) * 60 54 | price = float(data[0]) 55 | volume = float(data[1]) 56 | isBuy= data[3] == "b" 57 | 58 | if len(self.m1_bars) > 0 and self.m1_bars[-1].tstamp == bar_time: 59 | last_bar = self.m1_bars[-1] 60 | else: 61 | last_bar = Bar(tstamp=bar_time, open=price, high=price, low=price, close=price, volume=0) 62 | self.m1_bars.append(last_bar) 63 | gotTick = True 64 | last_bar.close = price 65 | last_bar.low = min(last_bar.low, price) 66 | last_bar.high = max(last_bar.high, price) 67 | last_bar.volume += volume 68 | last_bar.last_tick_tstamp = tstamp 69 | if isBuy: 70 | last_bar.buyVolume += volume 71 | else: 72 | last_bar.sellVolume += volume 73 | 74 | data = self.ws.get_data(topic) 75 | 76 | # new bars is handling directly in the messagecause we get a new one on each tick 77 | if gotTick and self.on_tick_callback is not None: 78 | self.on_tick_callback(fromAccountAction=False) 79 | except Exception as e: 80 | self.logger.error("error in socket data(%s): %s " % (topic, str(e))) 81 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/huobi/huobi_interface.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | from kuegi_bot.utils.trading_classes import Bar 5 | from .huobi_websocket import HuobiWebsocket 6 | from ..ExchangeWithWS import ExchangeWithWS 7 | 8 | 9 | class HuobiInterface(ExchangeWithWS): 10 | 11 | def __init__(self, settings, logger, on_tick_callback=None, on_api_error=None): 12 | self.on_api_error = on_api_error 13 | self.m1_bars: List[Bar] = [] 14 | hosts = ["wss://api.huobi.pro/ws","wss://api-aws.huobi.pro/ws"] # no testnet on spot 15 | super().__init__(settings, logger, 16 | ws=HuobiWebsocket(wsURLs=hosts, 17 | api_key=settings.API_KEY, 18 | api_secret=settings.API_SECRET, 19 | logger=logger, 20 | callback=self.socket_callback, 21 | symbol=settings.SYMBOL), 22 | on_tick_callback=on_tick_callback) 23 | 24 | def init(self): 25 | self.ws.subscribe_realtime_data() 26 | self.logger.info("subscribed to data") 27 | 28 | def get_instrument(self, symbol=None): 29 | return None 30 | 31 | def initOrders(self): 32 | pass 33 | 34 | def initPositions(self): 35 | pass 36 | 37 | def get_ticker(self, symbol=None): 38 | pass 39 | 40 | def get_bars(self, timeframe_minutes, start_offset_minutes, min_bars_needed) -> List[Bar]: 41 | if timeframe_minutes == 1: 42 | return self.m1_bars 43 | else: 44 | raise NotImplementedError 45 | 46 | def socket_callback(self, topic): 47 | try: 48 | data = self.ws.get_data(topic) 49 | gotTick = False 50 | while len(data) > 0: 51 | if topic == 'trade': 52 | tstamp = int(data['ts'])/1000 53 | bar_time = math.floor(tstamp / 60) * 60 54 | price = data['price'] 55 | volume = data['amount'] 56 | isBuy= data['direction'] == "buy" 57 | 58 | if len(self.m1_bars) > 0 and self.m1_bars[-1].tstamp == bar_time: 59 | last_bar = self.m1_bars[-1] 60 | else: 61 | last_bar = Bar(tstamp=bar_time, open=price, high=price, low=price, close=price, volume=0) 62 | self.m1_bars.append(last_bar) 63 | gotTick = True 64 | last_bar.close = price 65 | last_bar.low = min(last_bar.low, price) 66 | last_bar.high = max(last_bar.high, price) 67 | last_bar.volume += volume 68 | last_bar.last_tick_tstamp = tstamp 69 | if isBuy: 70 | last_bar.buyVolume += volume 71 | else: 72 | last_bar.sellVolume += volume 73 | 74 | data = self.ws.get_data(topic) 75 | 76 | # new bars is handling directly in the messagecause we get a new one on each tick 77 | if gotTick and self.on_tick_callback is not None: 78 | self.on_tick_callback(fromAccountAction=False) 79 | except Exception as e: 80 | self.logger.error("error in socket data(%s): %s " % (topic, str(e))) 81 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/coinbase/coinbase_interface.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | from kuegi_bot.utils.trading_classes import Bar, parse_utc_timestamp 5 | from .coinbase_websocket import CoinbaseWebsocket 6 | from ..ExchangeWithWS import ExchangeWithWS 7 | 8 | 9 | class CoinbaseInterface(ExchangeWithWS): 10 | 11 | def __init__(self, settings, logger, on_tick_callback=None, on_api_error=None): 12 | self.on_api_error = on_api_error 13 | self.m1_bars: List[Bar] = [] 14 | hosts = ["wss://ws-feed.pro.coinbase.com"] # no testnet on spot 15 | super().__init__(settings, logger, 16 | ws=CoinbaseWebsocket(wsURLs=hosts, 17 | api_key=settings.API_KEY, 18 | api_secret=settings.API_SECRET, 19 | logger=logger, 20 | callback=self.socket_callback, 21 | symbol=settings.SYMBOL), 22 | on_tick_callback=on_tick_callback) 23 | 24 | def init(self): 25 | self.ws.subscribe_realtime_data() 26 | self.logger.info("subscribed to data") 27 | 28 | def get_instrument(self, symbol=None): 29 | return None 30 | 31 | def initOrders(self): 32 | pass 33 | 34 | def initPositions(self): 35 | pass 36 | 37 | def get_ticker(self, symbol=None): 38 | pass 39 | 40 | def get_bars(self, timeframe_minutes, start_offset_minutes, min_bars_needed) -> List[Bar]: 41 | if timeframe_minutes == 1: 42 | return self.m1_bars 43 | else: 44 | raise NotImplementedError 45 | 46 | def socket_callback(self, topic): 47 | try: 48 | data = self.ws.get_data(topic) 49 | gotTick = False 50 | while len(data) > 0: 51 | if topic == 'trade': 52 | tstamp = parse_utc_timestamp(data['time']) 53 | bar_time = math.floor(tstamp / 60) * 60 54 | price = float(data['price']) 55 | volume = float(data['last_size']) 56 | isBuy= data['side'] == "buy" 57 | 58 | if len(self.m1_bars) > 0 and self.m1_bars[-1].tstamp == bar_time: 59 | last_bar = self.m1_bars[-1] 60 | else: 61 | last_bar = Bar(tstamp=bar_time, open=price, high=price, low=price, close=price, volume=0) 62 | self.m1_bars.append(last_bar) 63 | gotTick = True 64 | last_bar.close = price 65 | last_bar.low = min(last_bar.low, price) 66 | last_bar.high = max(last_bar.high, price) 67 | last_bar.volume += volume 68 | last_bar.last_tick_tstamp = tstamp 69 | if isBuy: 70 | last_bar.buyVolume += volume 71 | else: 72 | last_bar.sellVolume += volume 73 | 74 | data = self.ws.get_data(topic) 75 | 76 | # new bars is handling directly in the messagecause we get a new one on each tick 77 | if gotTick and self.on_tick_callback is not None: 78 | self.on_tick_callback(fromAccountAction=False) 79 | except Exception as e: 80 | self.logger.error("error in socket data(%s): %s " % (topic, str(e))) 81 | -------------------------------------------------------------------------------- /docs/kuegiBotStructure.drawio: -------------------------------------------------------------------------------- 1 | 7V1bj6M2FP41kdqHQeAb8LjZ3W4rtWrVrdT2qXLASegQiMBpZvrra3MLxkxgEi6ZdEba2WDAsY8/jr/z+ZhZwI+7py8J3W9/in0WLoDpPy3gpwUAlukQ8Z8sec5LHLMo2CSBX1x0Kvga/MvKO4vSQ+CzVLmQx3HIg71a6MVRxDyulNEkiY/qZes4VL91TzdMK/jq0VAv/T3w+bboBbBP5d+zYLMtv9kibn5mRb3HTRIfouL7FgCus5/89I6WdRUdTbfUj4+1Ivh5AT8mcczzT7unjyyUti3Nlt/33Qtnq3YnLOJ9bqDLT3/TL6tf/9ru/n5+oH9+WT3QBwTyav6h4YGV/chay59LC2V9ZLIWawGXx23A2dc99eTZo8CEKNvyXVicXgdh+DEO4yS7F/qYOT4S5SlP4kdWO+OAFSREnCkawBLOnl7smlUZTACRxTvGk2dxSYW5/I5jbQBLfG1rg4fKQlqAZlNVdTKc+FDY7jV2NEe2I2XO2muzI/EctlpfacfyhhKrhV2RqRuWgDbDgrEMa2l2XQYRjYTVmuYVlQmfwaSNtnQvC70wPvjdph7AcAQixXCWpRvOajMcHstu+nO9fF4F/KatBma3GmpBG9+JDt6y2Swyt9mwZrb9lt262cDsZrM1s31+8rY02rAfIs6StTQGIKFoxnKViE8b+SmOmOw2EwUmKy6/bqpJYk55EEfi8ME1hzE2shoYbZmiAWwxtj3aRNJjhmaR/0FSygyXNE2DfMalCdeLawZkTwH/Q3w2DQeg4vhPec4wQXH4SdrFLA+eawe/sCQQPWRJUZY3ivkaa22YXjQ8PiQe68aXaP+G8a45Vh/K2lDhM49FwkIBn3/U5rYNX/ENv8SB6MiLSEGogYC8m8VddX7brIioFUG3UVFuB62iDE1Vt68AmE5VhgdYxphr+CJvAV9oTnxhE6ueCKPL8EUQMiA2IQaOjRCBrlotsg3i2rbwYBBBYDrTYq9HGHc19hBUsYfeAvbAvNizVZC4+GLsna9obHzBCfAFcB1fZ8E1A5DwvEBqcFd4OZAMJFyU4xLXtB1E1GoREJTFgsRFDsLQBWRalOnhF93v5eDE4tdK8t7kEOXtAaZP2U6QVo0gZ5CTF/CtvGMVZwfHQAyTxGtCOdsELKuBrYNIsGTRLFlryjgPok1q6HXSMJVNOFLubf14UzbJ2zLvUd6+lo3KOvijGOTfEurLimRhwrJvDbKTNBRnRfVmLNqWHIM06xKrt3hnaA/Wq8j84nrubqkikFVirR4n2S1Yt5rMbTjurgeYhY2XMddHa0WFlxHDIftExb+fDiEPvuYj/5zdMbOFK7dQUlW7JRRtNbE5momJZuIalMWJb35OfJZUgem3utkLhT4tnw72RD2eDUIesw4TrQ5hfruB8L5KQDOiGM76uhQw9Pxq1SbX2lz7AnmLRK+KiMN2ygJ564MpZgi3LDndnR0pt49I/kqwdk7a9pyTdhWAXh3ZOg0a6Uwc2ToaOhNGhU/dsWwe5lQHqxjmH+mKhSoYxQS4kTqTx6IMGEv5HEtf/aE4sQt8X9axFLNi8C9dZfVJ9Oxl57Lu4uUCfzrnCAo/VNy8qBbg6kg78xS+6DYE9IGDLWUoyvWJK6FiQcNVw1TTsIFaS7xeC4Yyyvi6F3qfups5eQwHKx5DRIrCsBc4jJrqgeuhp3RBHe5r3NizpCPdwhq5D/+DGnRlYmWtfIBr+EwFHiWxkJwkvRPv43Z5H2hhqE4E1+FkfM8CLtVEOwmM3ZvAIIjr7sgsRPl3+vK/cR9AA+E9+Y2OYEf4DWyWDK5kLWAQhABiAFUFF8+lOxVrAZdqkq3iIyS2qm5D+GqKUXM6EIGG07mMA83hdaxZpc77cTu6msmTYLPJHI8ZR78FUju8Cw8EuzwQILixCnfzzEWXHAdiLn2Ji+o/cIf3GNJRoL6O4j28GQRquvRaT/JZtQjWb9NNvDCv1AIcQlSlayh5BbTWOr4XgXrkqo3l1Oo3Jo3npi3rsi01azT1G/TQnya3UmOJpi0VemIr6Srs/FZ6yXXOZyV9JaVc+dNXrPIVYurxA5UVh/FGTOFyD4n0u/GeRfnSF820Ji+M0/w4lsu9olI/W0X+RtRGd9K2+e88tvx2/rGBjbFpScGcdmzgEGpQyY+kCmw1lB3idK5MXSwm3YKeDN4FoUFmZV0QutPIDHZqQ8hGg2rKZcNdo5GyZBl2IydyRNo1qDRkmo7iLU5S0QXSkKV6LEhmVoVA7yTxd1VoEGjqqtCRCtchGUbLZg/JJcSZw94XFOZOBGt4Xi4SD0X5FWVa4jAuyTFc1SXJiNM16z/YnsxF6frSknqPYpA5izZB1LLzpzsDqyUZMtgdwhw6pprPtaJpBjqZQmkKl5XyvNzb0oTLnI6WNMjkECmJlRU3fmRsn+akOEuIzBhy+d1+xrE9QX9vL/MOE9vQ0xtB2+bhiisPz4t1AeiK6UpJozbPz1SnBQtwos719P4ZZybYO8vifWYaxCPpkfO9suIXdOtqAgIYA2Usbn65AvYQh/q7ENu6eptZPYMU2wuF9BaEes610L6+Bb77lkHgqSu8d5jBlT+E54httWvw2swLo7HZ9QFAw50s9QK1vJWjIK/aSArrcnUQ1VeYRHKtS33fSVHUf7DbeOOJWQ60pR009FS7RU9t8wWjvRoF6SqHpPg5Dc9Z/r2OBVKzCSDUh6LaKjPNWPTYwDH5AoCtWonYLRu52hBrmXgsM/WIdKZfD22Yicxvph7JJ9MviDbMhOc3k66lfR+kPE6e8z0pZpZAUSzORat03yqR3My2NNzwagTNb+EBwwq5YGfWkywFH7KvCCwaSro9YYZUCbw3Fj6QocIHPHH4gPTwIYypDB+2+fOug/Itxg+oK35AFlF1CTAITizHcBoeZLzYocR2bSyLQfzLS+gxlC57ZkdMnAbcQQu9bNsfTMail1gPuF7vh0+KjJo20aXFVLIQVpRluWNxxk14fRWcHG+34oLfrIJTCqIDCYwqkK5RF7ED62iWLOO092KuRfU3gU278aIPaFVrYa9Fp90kCHpVY+Nz0JQPNeHDuQafxNa2H3Yx1bHh2Ze9zgxPoG5aA+RScELHsEWYT6BtEccqxcv5oKqHrddA1YHDgdV+FTMYEJLW/wySyDVc9d1b2EKGO+1bA/EQe5xqMzFprvNZnem459/7AGrIvmxL5oAQLTHU/brLWd93eT+MU9eKj0mQpzJVYb+5TuKdHOFCxLuTlUTcuVUKuVD1HzOmKIjD05+xyC8//a0Q+Pk/ -------------------------------------------------------------------------------- /docs/aboutCodingABot/myOwnTradingFramework.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1251223614424317953?s=20 2 | 3 | After realising that my edge is fully automizable, i implemented it in python. But since i have to trust that bot with a lot of money (at least i plan to) i really need to know what its doing. so i decided to write the whole thing from scratch: a thread. 1/14 4 | 5 | Disclaimer: This is how i did it based on my own exp and knowledge. Doesn't mean its smart to do it that way. Probably introduced lots of bugs, but so far its executing pretty good. And yes will open source it if ppl are interested. 2/14 6 | 7 | The whole thing consists of 4 parts: 8 | - bot itself, capsulated to easily add new bots 9 | - backtest module: executing a bot on historic data 10 | - livetrading module: using exchange APIs to execute bots 11 | - Exchange APIs. abstracted such that i can easily add new exchanges 12 | 3/14 13 | 14 | To be flexible for multiple exchanges and bots, i decided to roll my own datastructures (Bars, Orders, Positions, AccountData etc.). Means i have to convert data from the exchange APIs to my own structures, but that way the rest of the code is independent. 4/14 15 | 16 | For backtests i crawled M1 data from the exchange and saved it locally (performance) in my own format. During a backtest i then load each M1 Bar and add them to the real TF i work on (my case H4). The bot always gets the current bars (including the unfinished one)... 5/14 17 | 18 | and the account information(with open orders and position size) on every tick. In backtest i simulate to have one tick each minute (enough for an H4 system). So the backtestmodule only needs to provide the orderinterface (send, update, cancel), and simulate executions. 6/14 19 | 20 | For the executions to be realistic i added assumed slippage (depending on the ordertype and the M1 bar that triggered the order). And performance tracking also includes fees cause you want it to be as realistic as possible. 7/14 21 | 22 | The livetrading module is even simpler. Just take the callback from the api on each tick, forward all data to the bot. done. similar is the bot itself (in terms of me not writing more about it here;): first handle the open orders/positions, then check for opening new ones. 8/14 23 | 24 | The APIs are a different beast. Inconsistent datatypes between REST and realtime, inconsistent updates throu realtime, huge lags in updates (a second between execution event and account update)... But you only realise those problems once you are in production. 9/14 25 | 26 | So all set and done, backtest ran throu, optimization tells me lambo soon. How to put it into production and let it make money? glad you asked: get a simple server with linux, put the bot in a systemd/unit and you are good to go. 10/14 27 | 28 | During live execution you will face completely new challenges: Specially problems with connections and exchanges. I decided to restart the bot in such a case. Drastic i know, but better safe than sorry/inconsistent/undefined. Also the position sync is/was a big topic: 11/14 29 | 30 | You need to prepare for the case that your bot thinks it has positions open, but the actual data in the account is different. Then you need to recover. Either open orders that are missing, asume a position got closed without you noticing, or other way round. etc. 12/14 31 | 32 | My main concept behind everything: better miss a position than do more harm. So if the bot is in doubt he better cancels the assumed position, than increase exposure in the market etc. maybe not the smartest way but makes me sleep easier at night. 13/14 33 | 34 | 35 | -------------------------------------------------------------------------------- /dashboard/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | $.ajaxSetup ({ 3 | // Disable caching of AJAX responses 4 | cache: false 5 | }); 6 | 7 | Handlebars.registerHelper('formatPrice',function(aPrice) { 8 | if(typeof aPrice === "number") 9 | return aPrice.toFixed(Math.abs(aPrice) < 10?2:1); 10 | else 11 | return ""; 12 | }); 13 | Handlebars.registerHelper('formatTime',function(aTime) { 14 | if(typeof aTime === "number" && aTime > 100000) { 15 | var date= new Date(); 16 | date.setTime(aTime*1000); 17 | return date.toLocaleString(); 18 | } else 19 | return ""; 20 | }); 21 | Handlebars.registerHelper('classFromPosition',function(aPos) { 22 | var clazz= aPos.status; 23 | if(aPos.amount > 0) { 24 | clazz +="Long"; 25 | } else { 26 | clazz += "Short"; 27 | } 28 | if(aPos.worstCase > 0) { 29 | clazz += " winning" 30 | } else if (aPos.worstCase < 0) { 31 | clazz += " losing" 32 | } 33 | return clazz; 34 | }); 35 | Handlebars.registerHelper('formatResult',function(aResult) { 36 | if(typeof aResult === "number") { 37 | result= ""; 38 | if( aResult > 0) { 39 | result = "+"; 40 | } 41 | return result + aResult.toFixed(1)+"R"; 42 | } else 43 | return "-"; 44 | }); 45 | 46 | })(); 47 | 48 | function refresh() { 49 | $.getJSON('dashboard.json', function(data) { 50 | var template = Handlebars.templates.openPositions; 51 | var container= $('#positions')[0]; 52 | container.innerHTML= ''; 53 | bots= [] 54 | for (let id in data) { 55 | var bot= data[id]; 56 | bot.id= id; 57 | bot.drawdown = ((bot.max_equity - bot.equity)/bot.risk_reference).toFixed(1)+"R" 58 | bot.uwdays= ((Date.now()-bot.time_of_max_equity*1000)/(1000*60*60*24)).toFixed(0) 59 | bot.equity = bot.equity.toFixed(3) 60 | bot.max_equity = bot.max_equity.toFixed(3) 61 | var totalPos= 0; 62 | var totalWorstCase= 0; 63 | bot.positions.forEach(function(pos) { 64 | 65 | pos.connectedOrders.forEach(function(order) { 66 | if(order.id.includes("_SL_")) { 67 | pos.currentStop= order.stop_price; 68 | if(Math.abs(pos.amount) > 10) { 69 | pos.worstCase= (1/pos.currentStop - 1/pos.filled_entry)/(1/pos.wanted_entry-1/pos.initial_stop); 70 | } else { 71 | pos.worstCase= (pos.currentStop - pos.filled_entry)/(pos.wanted_entry-pos.initial_stop); 72 | } 73 | } 74 | if(Math.abs(pos.amount) > 10) { 75 | pos.initialRisk= pos.amount/pos.initial_stop - pos.amount/pos.wanted_entry; 76 | } else { 77 | pos.initialRisk= pos.amount*(pos.wanted_entry-pos.initial_stop); 78 | } 79 | }); 80 | if(pos.status == "open") { 81 | totalPos += pos.amount; 82 | totalWorstCase += pos.initialRisk*pos.worstCase; 83 | } 84 | }); 85 | bot.totalWorstCase= (totalWorstCase/bot.risk_reference); 86 | bot.totalPos = totalPos.toFixed(Math.abs(totalPos) > 100 ? 0 : 3); 87 | bots.push(bot) 88 | } 89 | var div= template(bots); 90 | container.insertAdjacentHTML('beforeend',div); 91 | }); 92 | } -------------------------------------------------------------------------------- /docs/aboutCodingABot/1yearRecap.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1347634489925763079?s=20 2 | 3 | about a year ago i put my bot into production. since then i switched from overworked techlead to "relaxed" bot trader. made between 70 and 100% in one year (depends on how you count), and with the increase in BTC value it actually made me around 400%. a small recap :thread: 4 | 5 | i started on bitmex in dec 2019 (only futures exchange i knew back then). then expanded to bybit and in summer also to binance and phemex. Interesstingly enough, bitmex went up and down but never really took off. and since i never liked the spikes, i cut it. 6 | 7 | binance and phemex looked good, but for some reason the bot didn't perform as the backtest promised and actually burned the accounts i put there, so i decided to focus on the things that worked and don't waste time with distractions. 8 | 9 | aka: focus on bybit and work on "real" diversification: i added ETHUSD and XRPUSD as trading pairs and so far everything is taking off. I don't know why the other exchanges failed, and honestly i don't care. i have one that works, thats enough. 10 | 11 | from a good start with +15% per month into a 2 month DD down 25% into a 70% pump up (days before i was going to cut it). back to another DD and historic finish. and thats only BTC. ETH and XRP also had their ups and downs but in the end, all of them finished with equity ATH. 12 | 13 | regarding settings: i spend nearly the full year, constantly testing and improving the strategies. adding parameters and optimizing values. fun fact: in the end, the version from a year ago would have made similar results. if only i knew... 14 | 15 | of course there were also technical problems and lessons to learn! in the beginning i had all the activity send to my telegram. cause i didn't want to miss *anything*. ended up with my phone vibrating every 4 hours and inbetween, and my wife getting annoyed... 16 | 17 | so i learned to also cut the noise here. improved the logging (with a dashboard for easy overview of the values that matter) and cut the telegram-notification to only executions and errors. unfortunatly there've been quite some errors in the beginning 18 | 19 | once i even got up in the middle of the night (cause my phone was vibrating for 40 minutes already) and had to fix some troubles. But with every problem, we learn. The bot got better errorhandling and now i think its running smoothly. :fingerscrossed: 20 | 21 | so over all: i made 120% on bybit, lost 30% on other exchanges, and that all could have happened with me just lying in the sun, enjoying the time. Ok yes, i enjoyed a lot of time, but i also spend a lot on the bot. 22 | 23 | i had some strategies starting with to much risk (cause backtests looked so nice... you know the drill) just to cut it drastically when they went into drawdown. big wins are nice, but its more important to stand the DDs. 24 | 25 | if you think bot trading means that you won't look at the charts anymore or spend less time infront of the computer: think again. You can, but having a bot doesn't automatically mean that. Its your choice and its a hard one. 26 | 27 | Bot trading brings all the problems like real trading, just on different levels. "don't interfere" "have a risksetting that fits your comfortzone" "have settings so you can really trust the bot" "don't interfere" "don't think you are smarter". 28 | 29 | and i made a lot of friends on the way! started a discord for bot trading and there are already some ppl running my bot with their own settings. super interessting to see what others make of it (and thanks everyone for using the reflink ;) 30 | 31 | in short: never underestimate what 1 year can change. i went from overworked techlead to relaxed bot-trader and this was just the beginning! believe in yourself and put in the work. It's not always easy, but always worth it! 32 | 33 | as always: since i wished to have someone to ask about that stuff when i started: my DMs are always open. feel free to reach out. AMA except for my settings ;) -------------------------------------------------------------------------------- /docs/aboutCodingABot/sampleStrategies/MACross.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1278057717643517952?s=20 2 | 3 | "How to automate a strategy?" glad you asked, lets go throu the process with a simple sample strategy. If ppl like it, i will make it a series with other strats too. Let's start with the classic: crossing SMAs. 4 | This strat might not work and this is not financial advice! :thread: 5 | 6 | When you want to automate a system you need the 3 basic questions answered in an unambiguous, complete way: 7 | - when to enter? 8 | - when to exit? 9 | - what position size to use? 10 | In our case: entry will be on fastMA crossing over the slowMA. (long on cross up, short on down) 11 | 12 | For the exit its a bit harder to decide. A MACross strat usually is a trend follower, so lets make it one. Trendfollowing means you let winners run free and only trail the stop, but how? Until the trend is broken of course. But how do we know? 13 | 14 | A break of the (up)trend is often defined as "when the price makes lower lows". So let's go with that (keep it simple): Trail the stop to the last swing low. for me this means a low where X bars before and Y bars afterwards have higher lows. 15 | 16 | Last question: position size? i usually start with risking 1% per trade to keep things simple. Its always easy to get more complex later on. How do you know what you are risking? The initial StopLoss is at the last swingLow, so the diff defines our risk. 17 | 18 | When you come up with a strat like that, you usually found it while analysing the markets. So you would/should have an idea what parameters to use (period of MAs, how much before/after the swings need). I made this all up as i write, so i also made up the params. 19 | 20 | So far, so easy. Now comes the coding. I am using my own bot-framework to keep things simple (i really like it simple), if you have questions about it: ask. 21 | First i implement the needed "indicators": MA and the swings. Both are pretty straight forward. 22 | 23 | The strat itself is also pretty easy: If MA crossed (last-bar fastMA<=slowMA && this-bar fastMA > slowMA) -> buy on close of this-bar. Buying on the close is hard, cause once its closed, the next bar is already there. its easier to execute on the first tick of the next. 24 | 25 | For the initialSL we only need the swing-indicator directly. Same for the trailing. So the strat has 4 parameters: periods of fast and slow MA, and the barcount before and after to identify a swing. So a few lines of code later we are ready to test it. Now the real fun starts. 26 | 27 | I am testing on bybit-data using the last 12 months, keeping 6 for out of sample (oos) tests. This concept is really important: only optimize on a subset. so when you are done and think you got it, you let it run on th oos and see how the system reacts to completly new data. 28 | 29 | Now i was prepared to write now that the first results are negative, and start to analyze how this can be improved etc. but well the random guessing wasn't that bad: strat made 19% in the last 12 months on a 5% maxDD. max days underwater is 36... so thats pretty nice. 30 | 31 | anyway lets still analyze it and see what we can improve. setting is fastMA:5, slowMA:34, before:3, after:2. its the typical trendfollower: long flat/down periods with some huge wins that produces the main performance. lets analyze the parts where it looses: f.e. before march 32 | 33 | What we see are some clear false signals (yellow). can't fight them in a trendfollower. But there are also some that went into a good direction but then lost it all (black circles). I don't like that, so lets see if a BreakEven logic helps. 34 | 35 | Lucky me, i already made the whole thing modularized with a generic BE-Module in place, so adding it is only one line. And the results are amazing: trailing to 0.1R once we are 0.5R in front, shoots us up to 27% profit on 2.4% maxDD. winrate 70%. i like it! 36 | 37 | But how is the OOS going? only on the 6 month we got -7% and a maxDD of -12%... not so good. maybe the system needs more work after all. if we use 8/34 instead of 5/34, we loose overall profit but have a stable performance. small changes->big impact->not good. 38 | 39 | I hope you got an idea of the process of automation and optimizing. Let it be a warning to not trust any random system with nice numbers. You need to know and understand it. If you wanna play with this system: feel free. maybe you make it into a solid performer. -------------------------------------------------------------------------------- /kuegi_bot/indicators/HMA.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | from kuegi_bot.indicators.indicator import Indicator, get_bar_value, highest, lowest, BarSeries, clean_range 5 | from kuegi_bot.trade_engine import Bar 6 | from kuegi_bot.utils import log 7 | 8 | logger = log.setup_custom_logger() 9 | 10 | 11 | class Data: 12 | def __init__(self, hma, hmasum, inner, inner1, inner2): 13 | self.inner = inner 14 | self.inner1= inner1 15 | self.inner2= inner2 16 | self.hmasum= hmasum 17 | self.hma = hma 18 | 19 | 20 | class HMA(Indicator): 21 | ''' Hull Moving Average 22 | HMA[i] = MA( (2*MA(input, period/2) – MA(input, period)), SQRT(period)) 23 | ''' 24 | 25 | def __init__(self, period: int = 15, maType: int= 0): 26 | super().__init__( 27 | 'HMA(' + str(period) + ','+str(maType)+')') 28 | self.period = period 29 | self.halfperiod= int(self.period/2) 30 | self.hmalength = int(math.sqrt(self.period)) 31 | self.maType= maType 32 | 33 | def on_tick(self, bars: List[Bar]): 34 | first_changed = 0 35 | for idx in range(len(bars)): 36 | if bars[idx].did_change: 37 | first_changed = idx 38 | else: 39 | break 40 | 41 | for idx in range(first_changed, -1, -1): 42 | self.process_bar(bars[idx:]) 43 | 44 | def process_bar(self, bars: List[Bar]): 45 | if len(bars) < self.period: 46 | self.write_data(bars[0], Data(hma=bars[0].close, hmasum=None, 47 | inner=bars[0].close, inner1=None,inner2=None)) 48 | return 49 | 50 | prevData= self.get_data(bars[1]) 51 | inner1 = 0 52 | inner2 = 0 53 | inner= 0 54 | if self.maType == 0: 55 | if prevData is not None and prevData.inner1 is not None: 56 | inner1= prevData.inner1 57 | inner2= prevData.inner2 58 | inner1 += bars[0].close - bars[self.period].close 59 | inner2 += bars[0].close - bars[self.halfperiod].close 60 | else: 61 | for idx, sub in enumerate(bars[:self.period]): 62 | inner1 += sub.close 63 | if idx < self.halfperiod: 64 | inner2 += sub.close 65 | 66 | inner = (2 * inner2 / self.halfperiod) - inner1 / self.period 67 | elif self.maType == 1: 68 | for idx, sub in enumerate(bars[:self.period]): 69 | inner1 += sub.close*(self.period-idx)/self.period 70 | if idx < self.halfperiod: 71 | inner2 += sub.close*(self.halfperiod-idx)/self.halfperiod 72 | 73 | inner = (2 * inner2*2/(self.halfperiod+1)) - inner1*2/(self.period+1) 74 | 75 | 76 | hmasum= 0 77 | hma = 0 78 | if self.maType == 0: 79 | cnt=1 80 | firstInner = self.get_data(bars[self.hmalength]) 81 | if firstInner is not None and firstInner.inner is not None and prevData.hmasum is not None: 82 | hmasum = prevData.hmasum 83 | hmasum += inner - firstInner.inner 84 | cnt = self.hmalength 85 | else: 86 | hmasum = inner 87 | cnt = 1 88 | for sub in bars[1:self.hmalength]: 89 | if self.get_data(sub) is not None: 90 | hmasum += self.get_data(sub).inner 91 | cnt += 1 92 | hma= hmasum/cnt 93 | elif self.maType == 1: 94 | hmasum = inner #*(self.hmalength)/(self.hmalength) 95 | for idx, sub in enumerate(bars[1:self.hmalength]): 96 | if self.get_data(sub) is not None: 97 | hmasum += self.get_data(sub).inner*(self.hmalength-(idx+1))/(self.hmalength) 98 | else: 99 | hmasum += sub.close*(self.hmalength-(idx+1))/(self.hmalength) 100 | 101 | hma= hmasum*2/(self.hmalength+1) 102 | 103 | self.write_data(bars[0], Data(hma=hma, hmasum=hmasum, 104 | inner=inner, inner1=inner1, inner2=inner2)) 105 | 106 | def get_line_names(self): 107 | return ["hma" + str(self.period)] 108 | 109 | def get_data_for_plot(self, bar: Bar): 110 | return [self.get_data(bar).hma] 111 | -------------------------------------------------------------------------------- /docs/kuegiChannel.ps: -------------------------------------------------------------------------------- 1 | //@version=4 2 | study("KuegiChannel") 3 | 4 | maxLookBack= input(title="Max Lookback", type=input.integer,defval=15, minval=3) 5 | thresFac= input(title="threshold Factor", type=input.float,defval=0.9, minval=0.1) 6 | bufferFac= input(title="buffer Factor", type=input.float,defval=0.05, minval=-0.1) 7 | maxDistFac= input(title="max Dist in ATR", type=input.float,defval=2, minval=0.1) 8 | 9 | highest_(values, length, offset) => 10 | float h_val = na 11 | if length >= 1 12 | for i = offset to offset+length-1 13 | if ( na(h_val) or (not na(values[i]) and values[i] > h_val ) ) 14 | h_val := values[i] 15 | h_val 16 | 17 | lowest_(values, length, offset) => 18 | float l_val = na 19 | if length >= 1 20 | for i = offset to offset+length-1 21 | if ( na(l_val) or (not na(values[i]) and values[i] < l_val ) ) 22 | l_val := values[i] 23 | l_val 24 | 25 | cleanRange(length) => 26 | float h1= na 27 | float h2= na 28 | float h3= na 29 | float h4= na 30 | for i = 0 to length - 1 31 | float range= high[i] - low[i] 32 | if na(h1) or range > h1 33 | h4:= h3 34 | h3:= h2 35 | h2:= h1 36 | h1:= range 37 | else 38 | if na(h2) or range > h2 39 | h4:= h3 40 | h3:= h2 41 | h2:= range 42 | else 43 | if na(h3) or range > h3 44 | h4:= h3 45 | h3:= range 46 | else 47 | if na(h4) or range > h4 48 | h4:= range 49 | 50 | float sum= 0 51 | int count=0 52 | for i= 0 to length - 1 53 | float range= high[i] - low[i] 54 | if (length > 20 and range < h4) or (length > 15 and range < h3) or (length > 10 and range < h2) or (length > 5 and range < h1) 55 | sum := sum + range 56 | count:= count + 1 57 | sum/count 58 | 59 | float myAtr= cleanRange(maxLookBack*2) 60 | 61 | int sinceLongReset= 0 62 | int sinceShortReset= 0 63 | int offset= 1 64 | 65 | int move= 1 66 | if((high[offset]-low[offset]) < (high[offset+1]-low[offset+1])) 67 | move:= 2 68 | 69 | float rangeHH= highest(high,2)[offset+move] 70 | float rangeLL= lowest(low,2)[offset+move] 71 | float threshold= myAtr*thresFac 72 | 73 | float maxDist= myAtr*maxDistFac 74 | float buffer= myAtr*bufferFac 75 | 76 | float moveUp= high[offset]-rangeHH 77 | float moveDown= rangeLL - low[offset] 78 | 79 | if(moveUp > threshold and sinceLongReset[1] >= move and low[offset] < low[0] and rangeHH < low) 80 | sinceLongReset:= move+1 81 | else 82 | sinceLongReset:= min(nz(sinceLongReset[1])+1,maxLookBack) 83 | float longTrail= max(lowest_(low,sinceLongReset-1,0)-maxDist,lowest_(low,sinceLongReset,0)-buffer[1]) 84 | 85 | if(moveDown > threshold and sinceShortReset[1] >= move and high[offset] > high[0] and rangeLL> high) 86 | sinceShortReset:= move+1 87 | else 88 | sinceShortReset:= min(nz(sinceShortReset[1])+1,maxLookBack) 89 | float shortTrail= min(highest_(high,sinceShortReset-1,0)+maxDist,highest_(high,sinceShortReset,0)+buffer[1]) 90 | 91 | // SWINGS 92 | 93 | float minDelta= myAtr*bufferFac 94 | swing(series,direction,default,maxLookBack) => 95 | float val= na 96 | for length = 1 to 3 97 | if ( na(val) and length < maxLookBack) 98 | float cex= lowest_(series,length,1) 99 | float ex= highest_(series,length,1) 100 | float s= highest_(series,2,length+1) 101 | float e= ex 102 | if direction < 0 103 | e:= cex 104 | s:= lowest_(series,2,length+1) 105 | if direction*(e-s) > 0 and direction*(e-series[length+1]) > minDelta and direction*(e-series[0]) > minDelta 106 | val := e + direction*minDelta 107 | 108 | if na(val) 109 | val:= default 110 | val 111 | 112 | 113 | sinceReset= min(sinceShortReset, sinceLongReset) 114 | float lastLongSwing= na 115 | lastLongSwing:= swing(high,1,lastLongSwing[1],sinceReset) 116 | if lastLongSwing < high 117 | lastLongSwing:= na 118 | 119 | float lastShortSwing= na 120 | lastShortSwing := swing(low,-1,lastShortSwing[1],sinceReset) 121 | if lastShortSwing > low 122 | lastShortSwing:= na 123 | 124 | if sinceReset < 3 125 | lastLongSwing:= na 126 | lastShortSwing:= na 127 | 128 | p1= plot(longTrail, offset=1, title ="Long Trail", style= plot.style_stepline, color= color.blue, linewidth= 1) 129 | p2= plot(shortTrail, offset=1, title ="Short Trail", style= plot.style_stepline, color= color.blue, linewidth= 1) 130 | 131 | fill(p1, p2, color=color.blue) 132 | plot(lastLongSwing, offset=1, title ="Long Swing", style= plot.style_stepline, color= color.green, linewidth= 2) 133 | plot(lastShortSwing, offset=1, title ="Short Swing", style= plot.style_stepline, color= color.red, linewidth= 2) 134 | -------------------------------------------------------------------------------- /kuegi_bot/indicators/indicator.py: -------------------------------------------------------------------------------- 1 | from kuegi_bot.utils import log 2 | 3 | from typing import List 4 | 5 | from enum import Enum 6 | 7 | from kuegi_bot.utils.trading_classes import Bar 8 | 9 | 10 | class BarSeries(Enum): 11 | OPEN = "open" 12 | HIGH = "high" 13 | LOW = "low" 14 | CLOSE = "close" 15 | VOLUME = "volume" 16 | 17 | 18 | logger = log.setup_custom_logger() 19 | 20 | 21 | def get_bar_value(bar: Bar, series: BarSeries): 22 | return getattr(bar, series.value) 23 | 24 | 25 | def highest(bars: List[Bar], length: int, offset: int, series: BarSeries): 26 | result: float = get_bar_value(bars[offset], series) 27 | for idx in range(offset, offset + length): 28 | if result < get_bar_value(bars[idx], series): 29 | result = get_bar_value(bars[idx], series) 30 | return result 31 | 32 | 33 | def lowest(bars: List[Bar], length: int, offset: int, series: BarSeries): 34 | result: float = get_bar_value(bars[offset], series) 35 | for idx in range(offset, offset + length): 36 | if result > get_bar_value(bars[idx], series): 37 | result = get_bar_value(bars[idx], series) 38 | return result 39 | 40 | 41 | class Indicator: 42 | def __init__(self, indiId: str): 43 | self.id = indiId 44 | pass 45 | 46 | def on_tick(self, bars: List[Bar]): 47 | pass 48 | 49 | def write_data(self, bar: Bar, data): 50 | self.write_data_static(bar, data, self.id) 51 | 52 | @staticmethod 53 | def write_data_static(bar: Bar, data, indiId: str): 54 | if "indicators" not in bar.bot_data.keys(): 55 | bar.bot_data['indicators'] = {} 56 | 57 | bar.bot_data["indicators"][indiId] = data 58 | 59 | def get_data(self,bar:Bar): 60 | return self.get_data_static(bar, self.id) 61 | 62 | @staticmethod 63 | def get_data_static(bar: Bar, indiId:str): 64 | if 'indicators' in bar.bot_data.keys() and indiId in bar.bot_data['indicators'].keys(): 65 | return bar.bot_data["indicators"][indiId] 66 | else: 67 | return None 68 | 69 | def get_data_for_plot(self, bar: Bar): 70 | return [self.get_data(bar)] # default 71 | 72 | def get_plot_offset(self): 73 | return 0 74 | 75 | def get_number_of_lines(self): 76 | return 1 77 | 78 | def get_line_styles(self): 79 | return [{"width": 1, "color": "blue"}] 80 | 81 | def get_line_names(self): 82 | return ["1"] 83 | 84 | 85 | class SMA(Indicator): 86 | def __init__(self, period: int): 87 | super().__init__("SMA" + str(period)) 88 | self.period = period 89 | 90 | def on_tick(self, bars: List[Bar]): 91 | first_changed = 0 92 | for idx in range(len(bars)): 93 | if bars[idx].did_change: 94 | first_changed = idx 95 | else: 96 | break 97 | 98 | for idx in range(first_changed, -1, -1): 99 | bar= bars[idx] 100 | if idx < len(bars) - self.period: 101 | sum = 0 102 | cnt = 0 103 | for sub in bars[idx:idx + self.period]: 104 | sum += sub.close 105 | cnt += 1 106 | 107 | sum /= cnt 108 | self.write_data(bar, sum) 109 | else: 110 | self.write_data(bar, None) 111 | 112 | def get_line_names(self): 113 | return ["sma" + str(self.period)] 114 | 115 | 116 | class EMA(Indicator): 117 | def __init__(self, period: int): 118 | super().__init__("EMA" + str(period)) 119 | self.period = period 120 | self.alpha = 2 / (1 + period) 121 | 122 | def on_tick(self, bars: List[Bar]): 123 | first_changed = 0 124 | for idx in range(len(bars)): 125 | if bars[idx].did_change: 126 | first_changed = idx 127 | else: 128 | break 129 | 130 | for idx in range(first_changed, -1, -1): 131 | bar = bars[idx] 132 | ema = bar.close 133 | last = self.get_data(bars[idx + 1]) if idx < len(bars) - 1 else None 134 | if last is not None: 135 | ema = bar.close * self.alpha + last * (1 - self.alpha) 136 | self.write_data(bar, ema) 137 | 138 | def get_line_names(self): 139 | return ["ema" + str(self.period)] 140 | 141 | 142 | from functools import reduce 143 | 144 | 145 | def clean_range(bars: List[Bar], offset: int, length: int): 146 | ranges = [] 147 | for idx in range(offset, offset + length): 148 | if idx < len(bars): 149 | ranges.append(bars[idx].high - bars[idx].low) 150 | 151 | ranges.sort(reverse=True) 152 | 153 | # ignore the biggest 10% of ranges 154 | ignored_count = int(length / 5) 155 | sum = reduce(lambda x1, x2: x1 + x2, ranges[ignored_count:]) 156 | return sum / (len(ranges) - ignored_count) 157 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bybit/bybit_websocket.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import json 3 | 4 | import time 5 | 6 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 7 | 8 | class PublicBybitWebSocket(KuegiWebsocket): 9 | def __init__(self, publicURL, logger, mainSocket,symbol, minutesPerBar): 10 | self.data = {} 11 | self.symbol= symbol 12 | self.minutesPerBar= minutesPerBar 13 | self.initial_subscribe_done = False 14 | super().__init__([publicURL], None, None, logger, None) 15 | self.mainSocket= mainSocket 16 | 17 | def on_message(self, message): 18 | self.mainSocket.on_message(message) 19 | 20 | def on_pong(self): 21 | self.ws.send(json.dumps({"op": "ping"})) 22 | 23 | def subscribe(self,topic:str, ws): 24 | param = dict( 25 | op='subscribe', 26 | args=[topic] 27 | ) 28 | ws.send(json.dumps(param)) 29 | if topic not in self.mainSocket.data: 30 | self.mainSocket.data[topic] = [] 31 | def subscribe_realtime_data(self): 32 | subbarsIntervall = '1' if self.minutesPerBar <= 60 else '60' 33 | self.subscribe('kline.' + subbarsIntervall + '.' + self.symbol,self.ws) 34 | self.subscribe("tickers."+self.symbol,self.ws) 35 | self.initial_subscribe_done = True 36 | 37 | 38 | class BybitWebsocket(KuegiWebsocket): 39 | # User can ues MAX_DATA_CAPACITY to control memory usage. 40 | MAX_DATA_CAPACITY = 200 41 | PRIVATE_TOPIC = ['position', 'execution', 'order'] 42 | 43 | def __init__(self, publicURL, privateURL, api_key, api_secret, logger, callback,symbol, minutesPerBar): 44 | self.data = {} 45 | self.symbol= symbol 46 | self.minutesPerBar= minutesPerBar 47 | super().__init__([privateURL], api_key, api_secret, logger, callback) 48 | self.public= PublicBybitWebSocket(publicURL, logger, self,symbol, minutesPerBar) #no auth for public 49 | 50 | def on_pong(self): 51 | self.ws.send(json.dumps({"op": "ping"})) 52 | 53 | def generate_signature(self, expires): 54 | """Generate a request signature.""" 55 | _val = 'GET/realtime' + expires 56 | return str(hmac.new(bytes(self.api_secret, "utf-8"), bytes(_val, "utf-8"), digestmod="sha256").hexdigest()) 57 | 58 | def exit(self): 59 | if self.public: 60 | self.public.exit() 61 | super().exit() 62 | 63 | def do_auth(self): 64 | expires = str(int(round(time.time()) + 5)) + "000" 65 | signature = self.generate_signature(expires) 66 | auth = {"op": "auth", "args": [self.api_key, expires, signature]} 67 | self.ws.send(json.dumps(auth)) 68 | 69 | 70 | def subscribe_realtime_data(self): 71 | self.subscribe_order() 72 | self.subscribe_execution() 73 | self.subscribe_position() 74 | self.subscribe_wallet_data() 75 | if not self.public.initial_subscribe_done: 76 | self.public.subscribe_realtime_data() 77 | 78 | def on_message(self, message): 79 | """Handler for parsing WS messages.""" 80 | #self.logger.debug("WS got message "+message) 81 | message = json.loads(message) 82 | if 'success' in message: 83 | if message["success"]: 84 | if 'op' in message and message["op"] == 'auth': 85 | self.auth = True 86 | self.logger.info("Authentication success.") 87 | if 'ret_msg' in message and message["ret_msg"] == 'pong': 88 | self.data["pong"].append("PING success") 89 | else: 90 | self.logger.error("Error in socket: " + str(message)) 91 | 92 | if 'topic' in message: 93 | self.data[message["topic"]].append(message["data"]) 94 | if len(self.data[message["topic"]]) > BybitWebsocket.MAX_DATA_CAPACITY: 95 | self.data[message["topic"]] = self.data[message["topic"]][BybitWebsocket.MAX_DATA_CAPACITY // 2:] 96 | if self.callback is not None: 97 | self.callback(message['topic']) 98 | 99 | def subscribe(self,topic:str, ws): 100 | param = dict( 101 | op='subscribe', 102 | args=[topic] 103 | ) 104 | ws.send(json.dumps(param)) 105 | if topic not in self.data: 106 | self.data[topic] = [] 107 | 108 | # privates ------------------- 109 | 110 | def subscribe_wallet_data(self): 111 | self.subscribe("wallet",self.ws) 112 | 113 | def subscribe_position(self): 114 | self.subscribe("position",self.ws) 115 | 116 | def subscribe_execution(self): 117 | self.subscribe("execution",self.ws) 118 | 119 | def subscribe_order(self): 120 | self.subscribe("order",self.ws) 121 | 122 | def get_data(self, topic): 123 | if topic not in self.data: 124 | self.logger.info(" The topic %s is not subscribed." % topic) 125 | return [] 126 | if topic.split('.')[0] in BybitWebsocket.PRIVATE_TOPIC and not self.auth: 127 | self.logger.info("Authentication failed. Please check your api_key and api_secret. Topic: %s" % topic) 128 | return [] 129 | else: 130 | if len(self.data[topic]) == 0: 131 | return [] 132 | return self.data[topic].pop() 133 | -------------------------------------------------------------------------------- /scripts.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import json 3 | from datetime import datetime 4 | 5 | from kuegi_bot.exchanges.bybit.bybit_interface import ByBitInterface 6 | from kuegi_bot.utils import log 7 | from kuegi_bot.utils.helper import known_history_files, load_settings_from_args 8 | from kuegi_bot.utils.trading_classes import parse_utc_timestamp 9 | 10 | #''' 11 | 12 | def read_ref_bars(coin): 13 | bars= [] 14 | end = known_history_files["bitstamp_" +coin.lower()+"eur"] 15 | for i in range(end-20,end+1): 16 | with open("history/bitstamp/"+coin.lower()+"eur_M1_"+str(i)+".json") as f: 17 | bars += json.load(f) 18 | return bars 19 | 20 | def eurAt(bars, wantedtstamp): 21 | if wantedtstamp is None: 22 | return None 23 | start= int(bars[0]['timestamp']) 24 | end= int(bars[-1]['timestamp']) 25 | idx= int(len(bars)*(wantedtstamp-start)/(end-start)) 26 | 27 | tstamp = int(bars[idx]['timestamp']) 28 | result= float(bars[idx]['open']) 29 | delta= 1 if tstamp < wantedtstamp else -1 30 | while (tstamp - wantedtstamp)*delta < 0 and len(bars) > idx >= 0: 31 | tstamp = int(bars[idx]['timestamp']) 32 | result= float(bars[idx]['open']) 33 | idx += delta 34 | if abs(tstamp-wantedtstamp) > 60: 35 | print("got big difference in eurAt. deltasec: %d" %(tstamp-wantedtstamp)) 36 | return result 37 | 38 | 39 | def eurAtArray(bars,format,wantedArray): 40 | result= [] 41 | for wanted in wantedArray: 42 | dt= None 43 | try: 44 | dt = datetime.strptime(wanted, format) 45 | except Exception as e: 46 | print(e) 47 | pass 48 | result.append(eurAt(bars,dt.timestamp() if dt is not None else None)) 49 | return result 50 | 51 | ''' 52 | bars= read_ref_bars("btc") 53 | 54 | res= eurAtArray(bars, "%d.%m.%Y %H:%M", [ 55 | ]) 56 | 57 | #''' 58 | 59 | ''' 60 | funding = dict() 61 | with open("history/bybit/BTCUSD_fundraw.json") as f: 62 | fund= json.load(f) 63 | for key, value in fund.items(): 64 | tstamp = parse_utc_timestamp(key)+8*60*60 # funding in history is "in 8 hours" 65 | funding[int(tstamp)]= value 66 | 67 | with open("history/bybit/BTCUSD_funding.json","w") as f: 68 | json.dump(funding,f) 69 | #''' 70 | 71 | ''' 72 | with open("exports/btceur.csv", 'w', newline='') as file: 73 | writer = csv.writer(file) 74 | writer.writerow(["time", "open", "high", "low", "close"]) 75 | for bar in reversed(bars): 76 | writer.writerow([datetime.fromtimestamp(int(bar['timestamp'])).isoformat(), 77 | bar['open'], 78 | bar['high'], 79 | bar['low'], 80 | bar['close']]) 81 | 82 | ''' 83 | 84 | ''' 85 | # getting account history, helpful for taxreports 86 | settings= load_settings_from_args() 87 | 88 | logger = log.setup_custom_logger("cryptobot", 89 | log_level=settings.LOG_LEVEL, 90 | logToConsole=True, 91 | logToFile= False) 92 | 93 | 94 | def onTick(fromAccountAction): 95 | logger.info("got Tick "+str(fromAccountAction)) 96 | 97 | 98 | 99 | interface= ByBitInterface(settings= settings,logger= logger,on_tick_callback=onTick) 100 | p= interface.pybit 101 | 102 | walletData = [] 103 | gotone = True 104 | page = 1 105 | while gotone: 106 | data = p.wallet_fund_records(start_date="2020-01-01", end_date="2021-08-01", limit="50", 107 | page=str(page))['result']['data'] 108 | gotone = data is not None and len(data) > 0 109 | walletData = walletData + data 110 | page = page + 1 111 | 112 | 113 | import csv 114 | 115 | 116 | coin= "xrp" 117 | 118 | bars= read_ref_bars(coin) 119 | 120 | with open("exports/bybitHistory"+coin+".csv", 'w', newline='') as file: 121 | writer = csv.writer(file) 122 | writer.writerow(["type","time", "amount", "balance","eurValueOfCoin"]) 123 | for entry in reversed(walletData): 124 | if entry['coin'] == coin.upper(): 125 | writer.writerow([entry['type'], 126 | entry['exec_time'], 127 | entry['amount'], 128 | entry['wallet_balance'], 129 | eurAt(bars,datetime.strptime(entry['exec_time'],"%Y-%m-%dT%H:%M:%S.%fZ").timestamp())]) 130 | 131 | 132 | print("done writing wallet history to file bybitHistory"+coin+".csv") 133 | 134 | 135 | #''' 136 | 137 | ''' 138 | 139 | bars = [] 140 | end = known_history_files["bitstamp_eurusd"] 141 | for i in range(end - 20, end + 1): 142 | with open("history/bitstamp/eurusd_M1_" + str(i) + ".json") as f: 143 | bars += json.load(f) 144 | 145 | # Date,Operation,Amount,Cryptocurrency,FIAT value,FIAT currency,Transaction ID,Withdrawal address,Reference 146 | with open("exports/cakeHistory.csv", 'r', newline='') as file: 147 | reader = csv.reader(file) 148 | with open("exports/cakeHistoryWithEur.csv", 'w', newline='') as output: 149 | writer = csv.writer(output) 150 | for row in reader: 151 | date = row[0] 152 | if date != "Date": 153 | row.append(eurAt(bars, datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z").timestamp())) 154 | else: 155 | row.append("eurusd") 156 | writer.writerow(row) 157 | 158 | 159 | #''' 160 | -------------------------------------------------------------------------------- /kuegi_bot/utils/helper.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import sys 4 | from datetime import datetime 5 | from typing import List 6 | 7 | from kuegi_bot.exchanges.bybit.bybit_interface import ByBitInterface 8 | from kuegi_bot.exchanges.bybit_linear.bybitlinear_interface import ByBitLinearInterface 9 | from kuegi_bot.exchanges.phemex.phemex_interface import PhemexInterface 10 | from kuegi_bot.indicators.indicator import Indicator 11 | from kuegi_bot.exchanges.bitmex.bitmex_interface import BitmexInterface 12 | from kuegi_bot.utils import log 13 | 14 | import plotly.graph_objects as go 15 | 16 | from kuegi_bot.utils.dotdict import dotdict 17 | from kuegi_bot.utils.trading_classes import Bar, process_low_tf_bars 18 | 19 | logger = log.setup_custom_logger() 20 | 21 | 22 | def load_settings_from_args(): 23 | with open("settings/defaults.json") as f: 24 | settings = json.load(f) 25 | 26 | settingsPath = sys.argv[1] if len(sys.argv) > 1 else None 27 | if not settingsPath: 28 | return None 29 | 30 | print("Importing settings from %s" % settingsPath) 31 | with open(settingsPath) as f: 32 | userSettings = json.load(f) 33 | settings.update(userSettings) 34 | 35 | settings = dotdict(settings) 36 | 37 | if settings.LOG_LEVEL == "ERROR": 38 | settings.LOG_LEVEL = logging.ERROR 39 | elif settings.LOG_LEVEL == "WARN": 40 | settings.LOG_LEVEL = logging.WARN 41 | elif settings.LOG_LEVEL == "INFO": 42 | settings.LOG_LEVEL = logging.INFO 43 | elif settings.LOG_LEVEL == "DEBUG": 44 | settings.LOG_LEVEL = logging.DEBUG 45 | else: 46 | settings.LOG_LEVEL = logging.INFO 47 | 48 | return settings 49 | 50 | 51 | def history_file_name(index, exchange,symbol='') : 52 | if len(symbol) > 0: 53 | symbol += "_" 54 | return 'history/' + exchange + '/' + symbol + 'M1_' + str(index) + '.json' 55 | 56 | known_history_files= { 57 | "bitmex_XBTUSD": 49, 58 | "bybit_BTCUSD": 28, 59 | "bybit_ETHUSD":23, 60 | "bybit_XRPUSD":14, 61 | "bybit_BTCUSDM21":1, 62 | "bybit-linear_BTCUSDT":9, 63 | "bybit-linear_LINKUSDT":3, 64 | "bybit-linear_ETHUSDT":3, 65 | "bybit-linear_LTCUSDT":3, 66 | "binance_BTCUSDT": 9, 67 | "binanceSpot_BTCUSD": 28, 68 | "phemex_BTCUSD": 6, 69 | "bitstamp_btceur": 99, 70 | "bitstamp_etheur": 35, 71 | "bitstamp_xrpeur": 42, 72 | "bitstamp_eurusd": 94 73 | } 74 | 75 | 76 | def load_funding(exchange='bybit',symbol='BTCUSD'): 77 | try: 78 | funding= None 79 | with open('history/' + exchange + '/' + symbol + '_funding.json') as f: 80 | fund= json.load(f) 81 | funding= {} 82 | for tstamp, value in fund.items(): 83 | funding[int(tstamp)] = value 84 | return funding 85 | except Exception as e: 86 | return None 87 | 88 | 89 | def load_bars(days_in_history, wanted_tf, start_offset_minutes=0,exchange='bybit',symbol='BTCUSD'): 90 | #empty symbol is legacy and means btcusd 91 | end = known_history_files[exchange+"_"+symbol] 92 | start = max(0,end - int(days_in_history * 1440 / 50000)-1) 93 | m1_bars_temp = [] 94 | logger.info("loading " + str(end - start+1) + " history files from "+exchange) 95 | for i in range(start, end + 1): 96 | with open(history_file_name(i,exchange,symbol)) as f: 97 | m1_bars_temp += json.load(f) 98 | logger.info("done loading files, now preparing them") 99 | start = int(max(0,len(m1_bars_temp)-(days_in_history * 1440))) 100 | m1_bars = m1_bars_temp[start:] 101 | 102 | subbars: List[Bar] = [] 103 | for b in m1_bars: 104 | if exchange == 'bybit': 105 | if b['open'] is None: 106 | continue 107 | subbars.append(ByBitInterface.barDictToBar(b)) 108 | elif exchange == 'bybit-linear': 109 | if b['open'] is None: 110 | continue 111 | subbars.append(ByBitLinearInterface.barDictToBar(b)) 112 | elif exchange == 'bitmex': 113 | if b['open'] is None: 114 | continue 115 | subbars.append(BitmexInterface.barDictToBar(b,wanted_tf)) 116 | elif exchange == 'phemex': 117 | subbars.append(PhemexInterface.barArrayToBar(b,10000)) 118 | subbars.reverse() 119 | return process_low_tf_bars(subbars, wanted_tf, start_offset_minutes) 120 | 121 | 122 | def prepare_plot(bars, indis: List[Indicator]): 123 | logger.info("calculating " + str(len(indis)) + " indicators on " + str(len(bars)) + " bars") 124 | for indi in indis: 125 | indi.on_tick(bars) 126 | 127 | logger.info("running timelines") 128 | time = list(map(lambda b: datetime.fromtimestamp(b.tstamp), bars)) 129 | open = list(map(lambda b: b.open, bars)) 130 | high = list(map(lambda b: b.high, bars)) 131 | low = list(map(lambda b: b.low, bars)) 132 | close = list(map(lambda b: b.close, bars)) 133 | 134 | logger.info("creating plot") 135 | fig = go.Figure(data=[go.Candlestick(x=time, open=open, high=high, low=low, close=close, name="XBTUSD")]) 136 | 137 | logger.info("adding indicators") 138 | for indi in indis: 139 | lines = indi.get_number_of_lines() 140 | offset = indi.get_plot_offset() 141 | for idx in range(0, lines): 142 | sub_data = list(map(lambda b: indi.get_data_for_plot(b)[idx], bars)) 143 | fig.add_scatter(x=time, y=sub_data[offset:], mode='lines', line_width=1, name=indi.id + "_" + str(idx)) 144 | 145 | fig.update_layout(xaxis_rangeslider_visible=False) 146 | return fig 147 | -------------------------------------------------------------------------------- /kuegi_bot/bots/strategies/channel_strat.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import math 3 | import plotly.graph_objects as go 4 | 5 | from kuegi_bot.bots.strategies.strat_with_exit_modules import StrategyWithExitModulesAndFilter 6 | from kuegi_bot.bots.trading_bot import TradingBot 7 | from kuegi_bot.indicators.kuegi_channel import KuegiChannel, Data 8 | from kuegi_bot.utils.trading_classes import Bar, Account, Symbol, OrderType 9 | 10 | 11 | class ChannelStrategy(StrategyWithExitModulesAndFilter): 12 | 13 | def __init__(self): 14 | super().__init__() 15 | self.channel: KuegiChannel = None 16 | self.trail_to_swing = False 17 | self.delayed_swing_trail = True 18 | self.trail_back = False 19 | self.trail_active = False 20 | 21 | def myId(self): 22 | return "ChannelStrategy" 23 | 24 | def withChannel(self, max_look_back, threshold_factor, buffer_factor, max_dist_factor, max_swing_length): 25 | self.channel = KuegiChannel(max_look_back, threshold_factor, buffer_factor, max_dist_factor, max_swing_length) 26 | return self 27 | 28 | def withTrail(self, trail_to_swing: bool = False, delayed_swing: bool = True, trail_back: bool = False): 29 | self.trail_active = True 30 | self.delayed_swing_trail = delayed_swing 31 | self.trail_to_swing = trail_to_swing 32 | self.trail_back = trail_back 33 | return self 34 | 35 | def init(self, bars: List[Bar], account: Account, symbol: Symbol): 36 | super().init(bars, account, symbol) 37 | if self.channel is None: 38 | self.logger.error("No channel provided on init") 39 | else: 40 | self.logger.info("init with %i %.1f %.3f %.1f %i | %.3f %.1f %i %.1f | %s %s %s %s" % 41 | (self.channel.max_look_back, self.channel.threshold_factor, self.channel.buffer_factor, 42 | self.channel.max_dist_factor, self.channel.max_swing_length, 43 | self.risk_factor, self.max_risk_mul, self.risk_type, self.atr_factor_risk, 44 | self.trail_active, self.delayed_swing_trail, self.trail_to_swing, self.trail_back)) 45 | self.channel.on_tick(bars) 46 | 47 | def min_bars_needed(self) -> int: 48 | return self.channel.max_look_back + 1 49 | 50 | def got_data_for_position_sync(self, bars: List[Bar]) -> bool: 51 | result= super().got_data_for_position_sync(bars) 52 | return result and (self.channel.get_data(bars[1]) is not None) 53 | 54 | def get_stop_for_unmatched_amount(self, amount: float, bars: List[Bar]): 55 | # ignore possible stops from modules for now 56 | data = self.channel.get_data(bars[1]) 57 | stopLong = int(max(data.shortSwing, data.longTrail) if data.shortSwing is not None else data.longTrail) 58 | stopShort = int(min(data.longSwing, data.shortTrail) if data.longSwing is not None else data.shortTrail) 59 | stop= stopLong if amount > 0 else stopShort 60 | 61 | def prep_bars(self, is_new_bar: bool, bars: list): 62 | if is_new_bar: 63 | self.channel.on_tick(bars) 64 | 65 | def manage_open_order(self, order, position, bars, to_update, to_cancel, open_positions): 66 | # first the modules 67 | super().manage_open_order(order,position,bars,to_update,to_cancel,open_positions) 68 | # now the channel stuff 69 | last_data: Data = self.channel.get_data(bars[2]) 70 | data: Data = self.channel.get_data(bars[1]) 71 | if data is not None: 72 | stopLong = data.longTrail 73 | stopShort = data.shortTrail 74 | if self.trail_to_swing and \ 75 | data.longSwing is not None and data.shortSwing is not None and \ 76 | (not self.delayed_swing_trail or (last_data is not None and 77 | last_data.longSwing is not None and 78 | last_data.shortSwing is not None)): 79 | stopLong = max(data.shortSwing, stopLong) 80 | stopShort = min(data.longSwing, stopShort) 81 | 82 | orderType = TradingBot.order_type_from_order_id(order.id) 83 | if position is not None and orderType == OrderType.SL: 84 | # trail 85 | newStop = order.stop_price 86 | isLong = position.amount > 0 87 | if self.trail_active: 88 | trail = stopLong if isLong else stopShort 89 | if (trail - newStop) * position.amount > 0 or \ 90 | (self.trail_back and position.initial_stop is not None 91 | and (trail - position.initial_stop) * position.amount > 0): 92 | newStop = math.floor(trail) if not isLong else math.ceil(trail) 93 | 94 | 95 | if newStop != order.stop_price: 96 | order.stop_price = newStop 97 | to_update.append(order) 98 | 99 | def add_to_plot(self, fig: go.Figure, bars: List[Bar], time): 100 | super().add_to_plot(fig, bars, time) 101 | lines = self.channel.get_number_of_lines() 102 | styles = self.channel.get_line_styles() 103 | names = self.channel.get_line_names() 104 | offset = 1 # we take it with offset 1 105 | self.logger.info("adding channel") 106 | for idx in range(0, lines): 107 | sub_data = list(map(lambda b: self.channel.get_data_for_plot(b)[idx], bars)) 108 | fig.add_scatter(x=time, y=sub_data[offset:], mode='lines', line=styles[idx], 109 | name=self.channel.id + "_" + names[idx]) 110 | 111 | -------------------------------------------------------------------------------- /history_crawler.py: -------------------------------------------------------------------------------- 1 | # importing the requests library 2 | import os 3 | 4 | import requests 5 | import json 6 | import math 7 | import sys 8 | from time import sleep 9 | 10 | from datetime import datetime 11 | # ==================================== 12 | # 13 | # api-endpoint 14 | from kuegi_bot.utils.helper import history_file_name, known_history_files 15 | from kuegi_bot.utils.trading_classes import parse_utc_timestamp 16 | 17 | exchange = sys.argv[1] if len(sys.argv) > 1 else 'bybit' 18 | symbol= sys.argv[2] if len(sys.argv) > 2 else 'BTCUSD' 19 | print("crawling from "+exchange) 20 | 21 | batchsize = 50000 22 | 23 | urls = { 24 | "bitmex": "https://www.bitmex.com/api/v1/trade/bucketed?binSize=1m&partial=false&symbol=##symbol##&count=1000&reverse=false", 25 | "bybit": "https://api.bybit.com/v2/public/kline/list?symbol=##symbol##&interval=1", 26 | "bybit-linear": "https://api.bybit.com/public/linear/kline?symbol=##symbol##&interval=1", 27 | "binance_future": "https://fapi.binance.com/fapi/v1/klines?symbol=##symbol##&interval=1m&limit=1000", 28 | "binanceSpot": "https://api.binance.com/api/v1/klines?symbol=##symbol##&interval=1m&limit=1000", 29 | "phemex":"https://api.phemex.com/phemex-user/public/md/kline?resolution=60&symbol=##symbol##", 30 | "bitstamp":"https://www.bitstamp.net/api/v2/ohlc/##symbol##/?step=60&limit=1000" 31 | } 32 | 33 | URL = urls[exchange].replace("##symbol##",symbol) 34 | 35 | result = [] 36 | start = 1 if exchange in ['bybit', 'bybit-linear'] else 0 37 | if exchange == 'phemex': 38 | start= 1574726400 # start of phemex 39 | elif exchange == 'bitstamp': 40 | if symbol == "btceur": 41 | start= 1313670000 42 | elif symbol == "etheur": 43 | start= 1502860000 44 | elif symbol == "xrpeur": 45 | start= 1483410000 46 | else: 47 | start= 1327090000 48 | 49 | 50 | offset = 0 51 | 52 | # init 53 | # TODO: adapt this to your number if you already have history files 54 | 55 | lastknown = known_history_files[exchange+"_"+symbol] if exchange+"_"+symbol in known_history_files else -1 56 | 57 | try: 58 | os.makedirs('history/'+exchange) 59 | except Exception: 60 | pass 61 | 62 | if lastknown >= 0: 63 | try: 64 | with open(history_file_name(lastknown,exchange,symbol), 'r') as file: 65 | result = json.load(file) 66 | if exchange == 'bitmex': 67 | start = lastknown * batchsize + len(result) 68 | elif exchange in ['bybit','bybit-linear']: 69 | start = int(result[-1]['open_time']) + 1 70 | elif exchange in ['phemex']: 71 | start = int(result[-1][0]) + 1 72 | elif exchange in ['binance_future','binanceSpot']: 73 | start= int(result[-1][6]) 74 | elif exchange in ['bitstamp']: 75 | start= int(result[-1]['timestamp']) 76 | offset= lastknown*batchsize 77 | except Exception as e: 78 | print("lier! you didn't have any history yet! ("+str(e)+")") 79 | lastknown = 0 80 | 81 | wroteData= False 82 | lastSync= 0 83 | while True: 84 | # sending get request and saving the response as response object 85 | url= URL+"&start="+str(start) 86 | if exchange in ['bybit','bybit-linear']: 87 | url = URL + "&from=" + str(start) 88 | elif exchange in ['binance_future','binanceSpot']: 89 | url= URL + "&startTime="+str(start) 90 | elif exchange == 'phemex': 91 | url = URL + "&from=" + str(start)+"&to="+str(start+2000*60) 92 | 93 | print(url+" __ "+str(len(result))) 94 | r = requests.get(url=url) 95 | # extracting data in json format 96 | jsonData= r.json() 97 | data=jsonData 98 | if exchange in ['bybit','bybit-linear']: 99 | data = jsonData["result"] 100 | elif exchange == 'phemex': 101 | if jsonData['msg'] == 'OK': 102 | data = jsonData['data']['rows'] 103 | else: 104 | data= [] 105 | elif exchange == "bitstamp": 106 | data= jsonData['data']['ohlc'] 107 | 108 | wasOk= len(data) >= 200 109 | if not wasOk: 110 | print(str(data)[:100]) 111 | if exchange == "bitstamp" and len(result) == 0: 112 | start+= 1000*60 113 | else: 114 | wroteData= False 115 | if exchange == 'bitmex': 116 | for b in data: 117 | b['tstamp'] = parse_utc_timestamp(b['timestamp']) 118 | result += data 119 | else: 120 | result += data 121 | lastSync += len(data) 122 | if exchange == 'bitmex': 123 | start= start +len(data) 124 | elif exchange in ['bybit','bybit-linear']: 125 | start = int(data[-1]['open_time'])+1 126 | elif exchange == 'phemex': 127 | if len(data) == 0: 128 | start += 2000*60 129 | else: 130 | start = int(data[-1][0]+1) 131 | elif exchange in ['binance_future','binanceSpot']: 132 | start= data[-1][6] # closeTime of last bar 133 | elif exchange == 'bitstamp': 134 | start= data[-1]['timestamp'] 135 | if lastSync > 15000 or (len(data) < 200 and not wroteData): 136 | wroteData= True 137 | lastSync= 0 138 | max= math.ceil((len(result)+offset)/batchsize) 139 | idx= max - 2 140 | while idx < max: 141 | if idx*batchsize-offset >= 0: 142 | with open(history_file_name(idx,exchange,symbol),'w') as file: 143 | json.dump(result[idx*batchsize-offset:(idx+1)*batchsize-offset],file) 144 | print("wrote file "+str(idx)) 145 | idx += 1 146 | 147 | if not wasOk: 148 | sleep(10) 149 | 150 | ######################################### 151 | # live tests 152 | ######## 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /backtest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from kuegi_bot.backtest_engine import BackTest 5 | from kuegi_bot.bots.MultiStrategyBot import MultiStrategyBot 6 | from kuegi_bot.bots.strategies.MACross import MACross 7 | from kuegi_bot.bots.strategies.entry_filters import DayOfWeekFilter 8 | from kuegi_bot.bots.strategies.SfpStrat import SfpStrategy 9 | from kuegi_bot.bots.strategies.exit_modules import SimpleBE, ParaTrail, MaxSLDiff 10 | from kuegi_bot.bots.strategies.kuegi_strat import KuegiStrategy 11 | from kuegi_bot.utils.helper import load_bars, prepare_plot, load_funding 12 | from kuegi_bot.utils import log 13 | from kuegi_bot.indicators.kuegi_channel import KuegiChannel 14 | from kuegi_bot.utils.trading_classes import Symbol 15 | 16 | logger = log.setup_custom_logger(log_level=logging.INFO) 17 | 18 | 19 | def plot(bars): 20 | forplot= bars[:] 21 | 22 | logger.info("initializing indicators") 23 | indis = [KuegiChannel()] 24 | 25 | logger.info("preparing plot") 26 | fig= prepare_plot(forplot, indis) 27 | fig.show() 28 | 29 | 30 | def backtest(bars): 31 | bots= [] 32 | for bot in bots: 33 | BackTest(bot,bars).run() 34 | 35 | 36 | def increment(min,max,steps,current)->bool: 37 | current[0] += steps[0] 38 | for idx in range(len(current)): 39 | if min[idx] <= current[idx] <= max[idx]: 40 | return True 41 | current[idx]= min[idx] 42 | if idx < len(current)-1: 43 | current[idx+1] += steps[idx+1] 44 | else: 45 | return False 46 | 47 | 48 | def runOpti(bars,funding,min,max,steps,symbol= None, randomCount= -1): 49 | v= min[:] 50 | total= 1 51 | while len(steps) < len(min): 52 | steps.append(1) 53 | for i in range(len(min)): 54 | total *= 1+(max[i]-min[i])/steps[i] 55 | logger.info("running %d combinations" % total) 56 | while True: 57 | msg= "" 58 | if randomCount > 0: 59 | for i in range(len(v)): 60 | v[i] = min[i] + random.randint(0, int((max[i] - min[i]) / steps[i])) * steps[i] 61 | randomCount = randomCount-1 62 | for i in v: 63 | msg += str(i) + " " 64 | logger.info(msg) 65 | bot = MultiStrategyBot(logger=logger, directionFilter=0) 66 | bot.add_strategy(KuegiStrategy() 67 | ) 68 | BackTest(bot, bars= bars,funding=funding, symbol=symbol).run() 69 | 70 | if randomCount == 0 or (randomCount < 0 and not increment(min,max,steps,v)): 71 | break 72 | 73 | 74 | def checkDayFilterByDay(bars,symbol= None): 75 | for i in range(7): 76 | msg = str(i) 77 | logger.info(msg) 78 | bot = MultiStrategyBot(logger=logger, directionFilter=0) 79 | bot.add_strategy(SfpStrategy() 80 | .withEntryFilter(DayOfWeekFilter(1 << i)) 81 | ) 82 | 83 | b= BackTest(bot, bars,symbol).run() 84 | 85 | pair= "BTCUSD" 86 | pair= "BTCUSDT" 87 | #pair= "ETHUSD" 88 | 89 | exchange= 'bybit' 90 | 91 | tf= 240 92 | monthsBack= 18 93 | 94 | if exchange == 'bybit' and "USDT" in pair: 95 | exchange= 'bybit-linear' 96 | 97 | funding = load_funding(exchange,pair) 98 | 99 | #bars_p = load_bars(30 * 12, 240,0,'phemex') 100 | #bars_n = load_bars(30 * 12, 240,0,'binance_f') 101 | #bars_ns = load_bars(30 * 24, 240,0,'binanceSpot') 102 | bars_b = load_bars(30 * monthsBack, tf,0,exchange,pair) 103 | #bars_m = load_bars(30 * 12, 240,0,'bitmex') 104 | 105 | #bars_b = load_bars(30 * 12, 60,0,'bybit') 106 | #bars_m = load_bars(30 * 24, 60,0,'bitmex') 107 | 108 | #bars1= load_bars(24) 109 | #bars2= process_low_tf_bars(m1_bars, 240, 60) 110 | #bars3= process_low_tf_bars(m1_bars, 240, 120) 111 | #bars4= process_low_tf_bars(m1_bars, 240, 180) 112 | 113 | symbol=None 114 | if pair == "BTCUSD": 115 | symbol=Symbol(symbol="BTCUSD", isInverse=True, tickSize=0.5, lotSize=1.0, makerFee=-0.025,takerFee=0.075, quantityPrecision=2,pricePrecision=2) 116 | elif pair == "XRPUSD": 117 | symbol=Symbol(symbol="XRPUSD", isInverse=True, tickSize=0.0001, lotSize=0.01, makerFee=-0.025,takerFee=0.075, quantityPrecision=2,pricePrecision=4) 118 | elif pair == "ETHUSD": 119 | symbol=Symbol(symbol="ETHUSD", isInverse=True, tickSize=0.01, lotSize=0.1, makerFee=-0.025,takerFee=0.075, quantityPrecision=2,pricePrecision=2) 120 | elif pair == "BTCUSDT": 121 | symbol=Symbol(symbol="BTCUSDT", isInverse=False, tickSize=0.5, lotSize=0.0001, makerFee=-0.025,takerFee=0.075, quantityPrecision=5,pricePrecision=4) 122 | 123 | 124 | # 125 | #for binance_f 126 | #symbol=Symbol(symbol="BTCUSDT", isInverse=False, tickSize=0.001, lotSize=0.00001, makerFee=0.02, takerFee=0.04, quantityPrecision=5) 127 | 128 | bars_full= bars_b 129 | oos_cut=int(len(bars_full)/4) 130 | bars= bars_full[oos_cut:] 131 | bars_oos= bars_full[:oos_cut] 132 | 133 | 134 | ''' 135 | checkDayFilterByDay(bars,symbol=symbol) 136 | 137 | #''' 138 | 139 | ''' 140 | # profiling stats 141 | # run it `python -m cProfile -o profile.data backtest.py` 142 | 143 | import pstats 144 | from pstats import SortKey 145 | p = pstats.Stats('profile.data') 146 | p.strip_dirs() # remove extra paths 147 | 148 | p.sort_stats(SortKey.CUMULATIVE).print_stats(20) 149 | p.sort_stats(SortKey.TIME).print_stats(10) 150 | 151 | p.print_callers('') 152 | ''' 153 | 154 | ''' 155 | runOpti(bars_oos, funding=funding, 156 | min= [-5,20], 157 | max= [5,27], 158 | steps= [1,1], 159 | randomCount=-1, 160 | symbol=symbol) 161 | 162 | #''' 163 | 164 | #''' 165 | 166 | bot=MultiStrategyBot(logger=logger, directionFilter= 0) 167 | bot.add_strategy(KuegiStrategy() 168 | ) 169 | 170 | bot.add_strategy(SfpStrategy() 171 | ) 172 | 173 | b= BackTest(bot, bars_full, funding=funding, symbol=symbol,market_slipage_percent=0.15).run() 174 | 175 | #performance chart with lots of numbers 176 | bot.create_performance_plot(bars).show() 177 | 178 | # chart with signals: 179 | b.prepare_plot().show() 180 | 181 | #''' 182 | -------------------------------------------------------------------------------- /exchange_test.py: -------------------------------------------------------------------------------- 1 | 2 | from kuegi_bot.exchanges.bitfinex.bitfinex_interface import BitfinexInterface 3 | from kuegi_bot.exchanges.bitmex.bitmex_interface import BitmexInterface 4 | from kuegi_bot.exchanges.bitstamp.bitstmap_interface import BitstampInterface 5 | from kuegi_bot.exchanges.bybit.bybit_interface import ByBitInterface 6 | from kuegi_bot.exchanges.bybit_linear.bybitlinear_interface import ByBitLinearInterface 7 | from kuegi_bot.exchanges.coinbase.coinbase_interface import CoinbaseInterface 8 | from kuegi_bot.exchanges.huobi.huobi_interface import HuobiInterface 9 | from kuegi_bot.exchanges.kraken.kraken_interface import KrakenInterface 10 | from kuegi_bot.exchanges.phemex.phemex_interface import PhemexInterface 11 | 12 | from kuegi_bot.utils import log 13 | from kuegi_bot.utils.dotdict import dotdict 14 | from kuegi_bot.utils.helper import load_settings_from_args 15 | from kuegi_bot.utils.trading_classes import Order 16 | 17 | settings= load_settings_from_args() 18 | 19 | logger = log.setup_custom_logger("cryptobot", 20 | log_level=settings.LOG_LEVEL, 21 | logToConsole=True, 22 | logToFile= False) 23 | 24 | 25 | def onTick(fromAccountAction): 26 | logger.info("got Tick "+str(fromAccountAction)) 27 | 28 | def onExecution(order_id, 29 | executed_price, 30 | amount, 31 | tstamp): 32 | logger.info(f"got execution {order_id} {amount:.2f}@{executed_price:.2f} at {tstamp}") 33 | 34 | 35 | '''bitfinex 36 | 37 | settings= dotdict({}) 38 | settings.SYMBOL = "tBTCUSD" 39 | client= BitfinexInterface(settings=settings, logger=logger, on_tick_callback=onTick) 40 | 41 | #''' 42 | 43 | '''kraken 44 | 45 | settings= dotdict({}) 46 | settings.SYMBOL = "XBT/USD" 47 | client= KrakenInterface(settings=settings, logger=logger, on_tick_callback=onTick) 48 | 49 | #''' 50 | 51 | '''coinbase 52 | 53 | settings= dotdict({}) 54 | settings.SYMBOL = "BTC-USD" 55 | client= CoinbaseInterface(settings=settings, logger=logger, on_tick_callback=onTick) 56 | 57 | #''' 58 | 59 | '''huobi 60 | 61 | settings= dotdict({}) 62 | settings.SYMBOL = "btcusdt" 63 | client= HuobiInterface(settings=settings, logger=logger, on_tick_callback=onTick) 64 | 65 | #''' 66 | 67 | '''binance_spot 68 | settings= dotdict({}) 69 | settings.SYMBOL = "btcusdt" 70 | client= BinanceSpotInterface(settings=settings, logger=logger, on_tick_callback=onTick) 71 | 72 | #''' 73 | 74 | '''bitstamp 75 | settings= dotdict({}) 76 | settings.SYMBOL = "btcusd" 77 | client= BitstampInterface(settings=settings,logger=logger,on_tick_callback=onTick) 78 | 79 | 80 | #''' 81 | 82 | '''phemex 83 | 84 | client= PhemexInterface(settings=settings,logger=logger,on_tick_callback=onTick) 85 | 86 | #''' 87 | 88 | 89 | 90 | ''' binance_future 91 | 92 | from binance_future.exception.binanceapiexception import BinanceApiException 93 | from binance_future.model.candlestickevent import Candlestick 94 | from binance_future import RequestClient, SubscriptionClient 95 | from binance_future.model import CandlestickInterval, OrderSide, OrderType, TimeInForce, SubscribeMessageType 96 | 97 | request_client = RequestClient(api_key=apiKey, secret_key=apiSecret) 98 | 99 | ws = SubscriptionClient(api_key=apiKey,secret_key=apiSecret) 100 | 101 | 102 | def callback(data_type: SubscribeMessageType, event: any): 103 | if data_type == SubscribeMessageType.RESPONSE: 104 | print("Event ID: ", event) 105 | elif data_type == SubscribeMessageType.PAYLOAD: 106 | print("Event type: ", event.eventType) 107 | print("Event time: ", event.eventTime) 108 | print(event.__dict__) 109 | if(event.eventType == "kline"): 110 | candle : Candlestick = event.data 111 | print(candle.open,candle.close,candle.high,candle.close,candle.closeTime,candle.startTime,candle.interval) 112 | elif(event.eventType == "ACCOUNT_UPDATE"): 113 | print("Event Type: ", event.eventType) 114 | elif(event.eventType == "ORDER_TRADE_UPDATE"): 115 | print("Event Type: ", event.eventType) 116 | elif(event.eventType == "listenKeyExpired"): 117 | print("Event: ", event.eventType) 118 | print("CAUTION: YOUR LISTEN-KEY HAS BEEN EXPIRED!!!") 119 | else: 120 | print("Unknown Data:") 121 | print() 122 | 123 | 124 | def error(e: BinanceApiException): 125 | print(e.error_code + e.error_message) 126 | 127 | 128 | listen_key = request_client.start_user_data_stream() 129 | ws.subscribe_user_data_event(listen_key, callback, error) 130 | 131 | request_client.keep_user_data_stream() 132 | 133 | bars = request_client.get_candlestick_data(symbol="BTCUSDT", interval=CandlestickInterval.MIN1, 134 | startTime=0, endTime=None, limit=1000) 135 | 136 | result = request_client.close_user_data_stream() 137 | 138 | ''' 139 | 140 | #''' 141 | if settings.EXCHANGE == 'bybit': 142 | interface= ByBitInterface(settings= settings,logger= logger,on_tick_callback=onTick,on_execution_callback=onExecution) 143 | b= interface.pybit 144 | w= interface.ws 145 | elif settings.EXCHANGE == 'bybit-linear': 146 | interface = ByBitLinearInterface(settings=settings, logger=logger, on_tick_callback=onTick) 147 | b = interface.pybit 148 | w = interface.ws 149 | else: 150 | interface= BitmexInterface(settings=settings,logger=logger,on_tick_callback=onTick) 151 | 152 | bars= interface.get_bars(240,0) 153 | 154 | 155 | def get_wallet_records(): 156 | result = [] 157 | gotone = True 158 | page = 1 159 | while gotone: 160 | data = b.Wallet.Wallet_getRecords(start_date="2020-01-01", end_date="2021-01-01", limit="50", 161 | page=str(page)).response().result['result']['data'] 162 | gotone = len(data) > 0 163 | result = result + data 164 | page = page + 1 165 | return result 166 | 167 | 168 | #b.Wallet.Wallet_getRecords().response().result['result']['data'] 169 | 170 | # ''' 171 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bitfinex/bitfinex_interface.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | from kuegi_bot.utils.trading_classes import Bar 5 | from .bitfinex_websocket import BitfinexWebsocket 6 | from ..ExchangeWithWS import ExchangeWithWS 7 | 8 | 9 | class BitfinexInterface(ExchangeWithWS): 10 | 11 | def __init__(self, settings, logger, on_tick_callback=None, on_api_error=None): 12 | self.on_api_error = on_api_error 13 | self.m1_bars: List[Bar] = [] 14 | hosts = ["wss://api-pub.bitfinex.com/ws/2"] # no testnet on spot 15 | super().__init__(settings, logger, 16 | ws=BitfinexWebsocket(wsURLs=hosts, 17 | api_key=settings.API_KEY, 18 | api_secret=settings.API_SECRET, 19 | logger=logger, 20 | callback=self.socket_callback, 21 | symbol=settings.SYMBOL), 22 | on_tick_callback=on_tick_callback) 23 | 24 | def init(self): 25 | self.ws.subscribe_realtime_data() 26 | self.logger.info("subscribed to data") 27 | 28 | def get_instrument(self, symbol=None): 29 | return None 30 | 31 | def initOrders(self): 32 | pass 33 | 34 | def initPositions(self): 35 | pass 36 | 37 | def get_ticker(self, symbol=None): 38 | pass 39 | 40 | def get_bars(self, timeframe_minutes, start_offset_minutes, min_bars_needed) -> List[Bar]: 41 | if timeframe_minutes == 1: 42 | if len(self.m1_bars) > 0: 43 | self.recalcBar(self.m1_bars[-1]) 44 | return self.m1_bars 45 | else: 46 | raise NotImplementedError 47 | 48 | @staticmethod 49 | def recalcBar(bar:Bar): 50 | if "trades" not in bar.bot_data or len(bar.bot_data['trades']) == 0: 51 | return 52 | lastTstamp= 0 53 | firstTstamp= bar.last_tick_tstamp 54 | bar.volume= 0 55 | bar.buyVolume= 0 56 | bar.sellVolume= 0 57 | bar.high= bar.low= list(bar.bot_data['trades'].values())[0][3] 58 | for data in bar.bot_data['trades'].values(): 59 | tstamp = int(data[1])/1000 60 | price = data[3] 61 | volume = abs(data[2]) 62 | isBuy= data[2] > 0 63 | 64 | if tstamp > lastTstamp: 65 | bar.close = price 66 | lastTstamp= tstamp 67 | if tstamp<= firstTstamp: 68 | bar.open= price 69 | firstTstamp= tstamp 70 | bar.low = min(bar.low, price) 71 | bar.high = max(bar.high, price) 72 | bar.volume += volume 73 | bar.last_tick_tstamp = tstamp 74 | if isBuy: 75 | bar.buyVolume += volume 76 | else: 77 | bar.sellVolume += volume 78 | 79 | def socket_callback(self, topic): 80 | try: 81 | data = self.ws.get_data(topic) 82 | gotTick = False 83 | while len(data) > 0: 84 | if topic == 'trade': 85 | tstamp = int(data[1])/1000 86 | bar_time = math.floor(tstamp / 60) * 60 87 | price = data[3] 88 | volume = abs(data[2]) 89 | isBuy= data[2] > 0 90 | 91 | if len(self.m1_bars) > 0 and self.m1_bars[-1].tstamp == bar_time: 92 | last_bar = self.m1_bars[-1] 93 | else: 94 | if len(self.m1_bars) > 0: 95 | self.recalcBar(self.m1_bars[-1]) 96 | for bar in self.m1_bars[-5:-2]: 97 | if "trades" in bar.bot_data: 98 | del bar.bot_data['trades'] 99 | last_bar = Bar(tstamp=bar_time, open=price, high=price, low=price, close=price, volume=0) 100 | last_bar.bot_data['trades']= {} 101 | self.m1_bars.append(last_bar) 102 | gotTick = True 103 | last_bar.close = price 104 | last_bar.low = min(last_bar.low, price) 105 | last_bar.high = max(last_bar.high, price) 106 | last_bar.volume += volume 107 | last_bar.last_tick_tstamp = tstamp 108 | last_bar.bot_data['trades'][data[0]]= data 109 | if isBuy: 110 | last_bar.buyVolume += volume 111 | else: 112 | last_bar.sellVolume += volume 113 | 114 | if topic == 'tradeupdate': 115 | tstamp = int(data[1])/1000 116 | bar_time = math.floor(tstamp / 60) * 60 117 | found= False 118 | for i in range(len(self.m1_bars)): 119 | bar = self.m1_bars[-i-1] 120 | if bar_time == bar.tstamp: 121 | found= True 122 | if "trades" in bar.bot_data: 123 | if data[0] not in bar.bot_data['trades']: 124 | self.logger.warn("got trade update before trade entry") 125 | bar.bot_data['trades'][data[0]]=data 126 | else: 127 | self.logger.error("wanted to update trade but no trades in bar at index -"+str(i+1)) 128 | if i > 0: 129 | # need to recalc, cause wasn't last bar that changed 130 | self.recalcBar(bar) 131 | break 132 | 133 | if not found: 134 | self.logger.error("didn't find bar for trade to update! "+str(data)) 135 | 136 | data = self.ws.get_data(topic) 137 | 138 | # new bars is handling directly in the messagecause we get a new one on each tick 139 | if gotTick and self.on_tick_callback is not None: 140 | self.on_tick_callback(fromAccountAction=False) 141 | except Exception as e: 142 | self.logger.error("error in socket data(%s): %s " % (topic, str(e))) 143 | -------------------------------------------------------------------------------- /docs/aboutCodingABot/howIBecameProfitable.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/mkuegi/status/1249711458184835077?s=20 2 | 3 | I came back to BTC early 2017 and discovered crypto-exchanges around mid 2017. switched from gambling to trading early 2019 and became netto profitable early 2020. 4 | it was probably the classic story of a rookie trader: a long thread. 1/ 5 | 6 | I loved btc. Full access to a "real" exchange (no CFD crap) and a "pure" technical asset to trade was awesome so i started using real money for the first time. Risk averse as i was i only funded the trading account with 100$ and had positionsizes of 1$ (thanks to knowing RM). 2/ 7 | 8 | I was in and out of positions in matter of minutes. Analysing the orderflow, reading the tape and jumping to gut feelings. Shortly i was hooked and did all the things you shouldn't as a rookie. but in 2017 this was all fun and game since it was only going up anyway. 3/ 9 | 10 | I didn't track any of it (first mistake), but i soon realised that low timeframes where full of noise, so i "went higher" to M15. Still didn't like indicators so i tried to read the naked chart. drawing lines, seeing patterns. And it worked far better than expected. 4/ 11 | 12 | But i lost money nonetheless. I wasn't trading yet, i was gambling. Playing it as a hobby. When i saw a setup i had a defined stop and risked similar amounts each trade (again thx prior knowledge), but with no real system. no documentation, no reflection. 5/ 13 | 14 | I saw a setup (where "setup" wasn't defined yet, i saw something and thought i knew how the price might behave) and opened the position with rough size calculations. I didn't even track profit/losses but the account definitely didn't grow. 6/ 15 | 16 | re-funded. "this time its different". zoomed out: M30. H1. H4. traded from mobile (red flag) w/o proper analysis (huge red flag): i often didn't know why i entered in hindsight. also had big wins, lucky shots covering the losses of 6 months. nothing to build a strategy on. 7/ 17 | 18 | To make it clear: i *knew* all the theory. i *knew* thats why beginners fail. I *knew* that emotions are your enemy and you shouldn't "trade" like that. I still did it for months. Thinking i am different. Thinking i will outsmart everyone. 8/ 19 | 20 | Finally realised that it's not working like that and started a journal. at least i was tracking my performance and trades now so i saw the curve going down. So i started reflecting and defining a strategy. I knew i had good entries but lost a lot on the way. 9/ 21 | 22 | I started trading my system. i made a plan to start with small risk (5$ per position) and on success double it every 2 months until i make enough for a living. First month went crazy and i made 20R that month. (i wasn't thinking in $ but R to detach from absolute returns) 10/ 23 | 24 | and (again despite *knowing* better) i became greedy and went to 20$ per trade. and lost 12R that month. I already traded my system, documenting every decision, analysing every trade after a week. and it showed that my system wasn't strict enough. 11/ 25 | 26 | i added clear definitions for stops, when to trail etc. I defined every part of it and was now able to backtest it manually. And it worked! The defined rules of the strategy where profitable in history and they were simple enough to follow. even made an indicator for it. 12/ 27 | 28 | So all settled and done? lambo soon? far from it! Despite the system being profitable i lost each month ("only" between 1 and 3R but still). So i added a column to my journal "according to strategy". A simple yes/no field i had to fill before entering each trade. 13/ 29 | 30 | a no-brainer right? who would trade (and acknowledge it) against a profitable system? Well: me. I thought i knew it better, trying to outperform the system. i made bad decisions all the way and lost while seeing the numbers in the journal how the pure system would perform. 14/ 31 | 32 | At the beginning of each month i promised myself to stick to the system. and failed repeatedly. i got better, but failed. Learning so far: you are your worst enemy. specially when it comes to trading! It doesn't matter what you know, it doesn't matter what you plan. 15/ 33 | 34 | You need to be able to execute that plan. every. fucking. day. And this was with a strategy based on my own mindset, risk strategy and view of the market. Can't imagine trading signals/systems from anyone else. 16/ 35 | 36 | so what did i do? i went deeper. I already had written an "indicator" showing me entry and exit levels (so i only had to follow 3 simple rules. easy af) so i took the next step and wrote a TV strategy implementing the full system (as full as possible on TV). 17/ 37 | 38 | And it was profitable. This fucking strategy i failed to execute was fully automateable with nice profits. So i spend the next months writing a python "framework" and automating the strategy fully into a bot. Deployed it to my trading account Dec. 1st 2019. 18/ 39 | 40 | Since i still thought that the master (me) had to be better than the apprentice (the bot) i still traded my own strategy manually (and still failed to really execute it). 2 months later i added another strategy to my bot (that i developed trading manually) 19/ 41 | 42 | the bot outperformed me like crazy (me still losing, bot making nice money per month). so last month i finally stopped trading manually and deployed the bot with full risk on multiple exchanges. 20/ 43 | 44 | And i still look at the bots decisions and need to hold back everyday. always wanting to trail that stop closer, prevent that entry... it would nearly always reduce the performance so i learned not to do it. but it took months! 21/ 45 | 46 | The takeaway: have enough runway for months to loose. know yourself. in trading there is only one to blame: yourself. start a journal on day 1. analyze and reflect every trade from day 1. Make it reproducable. 22/ 47 | 48 | If you can't write down the rules of your strategy you might never be able to execute it properly. If you have a defined strategy, you can backtest it, you can analyze and tune it. You can't "tune" following your gut. 23/ 49 | 50 | And plan your risk. When i made the plan it included the risk per position, the number of maxDD per month before i stop the month etc. So i knew the max amount i would lose over the next 12 months if everything goes south. 24/ 51 | 52 | i learned heaps from traders on here (who probably don't know that i exist), and all of them seem to have clearly defined strategies. Systems they strictly follow cause they know it works for them. shoutout to scottmelker, btcjack, birb, kouhran 53 | 54 | 55 | 56 | 57 | It was the strategy that evolved from my personal trading style. it fits my mentality and understanding of the market. It still uses no real indicator and is simple as fuck. 58 | -------------------------------------------------------------------------------- /kuegi_bot/voluba/aggregator.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timedelta 3 | from dateutil import tz 4 | import json 5 | import os 6 | from typing import List 7 | 8 | from kuegi_bot.exchanges.binance_spot.binance_spot_interface import BinanceSpotInterface 9 | from kuegi_bot.exchanges.bitfinex.bitfinex_interface import BitfinexInterface 10 | from kuegi_bot.exchanges.bitstamp.bitstmap_interface import BitstampInterface 11 | from kuegi_bot.exchanges.coinbase.coinbase_interface import CoinbaseInterface 12 | from kuegi_bot.exchanges.huobi.huobi_interface import HuobiInterface 13 | from kuegi_bot.exchanges.kraken.kraken_interface import KrakenInterface 14 | from kuegi_bot.utils.dotdict import dotdict 15 | from kuegi_bot.utils.trading_classes import Bar 16 | 17 | 18 | class VolubaData: 19 | def __init__(self, tstamp): 20 | self.tstamp: int = tstamp 21 | self.barsByExchange = {} 22 | 23 | 24 | class VolubaAggregator: 25 | 26 | def __init__(self, settings, logger): 27 | self.settings = settings 28 | self.exchanges = {} 29 | self.logger = logger 30 | self.m1Data = {} 31 | 32 | self.logger.info("### Starting up the Aggregator ###") 33 | base = self.settings.dataPath 34 | try: 35 | os.makedirs(base) 36 | except Exception: 37 | pass 38 | 39 | # read last data 40 | # init exchanges from settings 41 | self.read_data() 42 | for exset in settings.exchanges: 43 | exset = dotdict(exset) 44 | self.load_exchange(exset) 45 | self.logger.info("initial load of exchanges done") 46 | 47 | def load_exchange(self,settings): 48 | ex= None 49 | if settings.id == "bitstamp": 50 | ex = BitstampInterface(settings=settings, logger=self.logger) 51 | if settings.id == "binance": 52 | ex = BinanceSpotInterface(settings=settings, logger=self.logger) 53 | if settings.id == "huobi": 54 | ex = HuobiInterface(settings=settings, logger=self.logger) 55 | if settings.id == "coinbase": 56 | ex = CoinbaseInterface(settings=settings, logger=self.logger) 57 | if settings.id == "kraken": 58 | ex = KrakenInterface(settings=settings, logger=self.logger) 59 | if settings.id == "bitfinex": 60 | ex = BitfinexInterface(settings=settings, logger=self.logger) 61 | if ex is not None: 62 | self.exchanges[settings.id] = ex 63 | 64 | def aggregate_data(self): 65 | for exId, exchange in self.exchanges.items(): 66 | m1bars = exchange.get_bars(1, 0) 67 | for bar in m1bars: 68 | if bar.tstamp not in self.m1Data: 69 | self.m1Data[bar.tstamp] = VolubaData(bar.tstamp) 70 | self.m1Data[bar.tstamp].barsByExchange[exId] = bar 71 | 72 | for exId, exchange in self.exchanges.items(): 73 | if not exchange.is_open(): 74 | self.logger.warn("%s died. restarting the exchange" % exId) 75 | del self.exchanges[exId] 76 | self.load_exchange(exchange.settings) 77 | 78 | def read_data_file(self, filename): 79 | try: 80 | with open(filename, 'r') as file: 81 | data = json.load(file) 82 | for entry in data: 83 | d = VolubaData(entry['tstamp']) 84 | for exchange, bar in entry['barsByExchange'].items(): 85 | bar = dotdict(bar) 86 | b = Bar(tstamp=bar.tstamp, 87 | open=bar.open, 88 | high=bar.high, 89 | low=bar.low, 90 | close=bar.close, 91 | volume=bar.volume) 92 | b.buyVolume = bar.buyVolume 93 | b.sellVolume = bar.sellVolume 94 | d.barsByExchange[exchange] = b 95 | self.m1Data[entry['tstamp']] = d 96 | 97 | except Exception as e: 98 | self.logger.error("Error reading data " + str(e)) 99 | 100 | def read_data(self): 101 | base = self.settings.dataPath 102 | today = datetime.today() 103 | for delta in range(0, 4): 104 | date = today - timedelta(days=delta) 105 | self.read_data_file(base + date.strftime("%Y-%m-%d.json")) 106 | 107 | def serialize_current_data(self): 108 | base = self.settings.dataPath 109 | try: 110 | data: List[VolubaData] = sorted(self.m1Data.values(), key=lambda d: d.tstamp) 111 | 112 | today = datetime.today() 113 | startOfToday = datetime(today.year, today.month, today.day, tzinfo=tz.tzutc()).timestamp() 114 | yesterday = today - timedelta(days=1) 115 | now = time.time() 116 | 117 | latest = [] 118 | todayData = [] 119 | yesterdayData = [] 120 | 121 | for d in data: 122 | dic = {'tstamp': d.tstamp, 123 | 'barsByExchange': {} 124 | } 125 | for ex, bar in d.barsByExchange.items(): 126 | bard = dict(bar.__dict__) 127 | if "did_change" in bard: 128 | del bard['did_change'] 129 | if "bot_data" in bard: 130 | del bard['bot_data'] 131 | if "subbars" in bard: 132 | del bard['subbars'] 133 | dic['barsByExchange'][ex] = bard 134 | if d.tstamp >= now - 3 * 60: 135 | latest.append(dic) 136 | if d.tstamp >= startOfToday: 137 | todayData.append(dic) 138 | if startOfToday - 1440 * 60 <= d.tstamp < startOfToday: 139 | yesterdayData.append(dic) 140 | 141 | # clear old data (2 days for extra buffer) 142 | if d.tstamp < startOfToday - 1440 * 60 * 2: 143 | del self.m1Data[d.tstamp] 144 | 145 | string = json.dumps(latest, sort_keys=False, indent=4) 146 | with open(base + "latest.json", 'w') as file: 147 | file.write(string) 148 | 149 | string = json.dumps(todayData, sort_keys=False, indent=4) 150 | with open(base + today.strftime("%Y-%m-%d.json"), 'w') as file: 151 | file.write(string) 152 | 153 | string = json.dumps(yesterdayData, sort_keys=False, indent=4) 154 | with open(base + yesterday.strftime("%Y-%m-%d.json"), 'w') as file: 155 | file.write(string) 156 | 157 | # also write last two days 158 | except Exception as e: 159 | self.logger.error("Error saving data " + str(e)) 160 | -------------------------------------------------------------------------------- /kuegi_bot/indicators/kuegi_channel.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from kuegi_bot.indicators.indicator import Indicator, get_bar_value, highest, lowest, BarSeries, clean_range 4 | from kuegi_bot.trade_engine import Bar 5 | from kuegi_bot.utils import log 6 | 7 | logger = log.setup_custom_logger() 8 | 9 | class Data: 10 | def __init__(self, sinceLongReset, sinceShortReset, longTrail, shortTrail, longSwing, shortSwing, buffer, atr): 11 | self.sinceLongReset = sinceLongReset 12 | self.sinceShortReset = sinceShortReset 13 | self.longTrail = longTrail 14 | self.shortTrail = shortTrail 15 | self.longSwing = longSwing 16 | self.shortSwing = shortSwing 17 | self.buffer = buffer 18 | self.atr = atr 19 | 20 | 21 | class KuegiChannel(Indicator): 22 | ''' calculates trails and swings 23 | if the price makes a strong move the trail goes to the start of the move. 24 | there is also a max dist for the trail from the neg.extr of the last X bars 25 | 26 | swings must be confirmed by 2 bars before and 1 bar after the swing. 27 | 28 | a strong move resets the swings. the bar of the move is never considered a swing point 29 | 30 | ''' 31 | def __init__(self, max_look_back: int = 15, threshold_factor: float = 0.9, buffer_factor: float = 0.05, 32 | max_dist_factor: float = 2, max_swing_length: int = 3): 33 | super().__init__( 34 | 'KuegiChannel(' + str(max_look_back) + ',' + str(threshold_factor) + ',' + str(buffer_factor) + ',' + str( 35 | max_dist_factor) + ')') 36 | self.max_look_back = max_look_back 37 | self.threshold_factor = threshold_factor 38 | self.buffer_factor = buffer_factor 39 | self.max_dist_factor = max_dist_factor 40 | self.max_swing_length = max_swing_length 41 | 42 | def on_tick(self, bars: List[Bar]): 43 | # ignore first 5 bars 44 | for idx in range(len(bars) - self.max_look_back, -1, -1): 45 | if bars[idx].did_change: 46 | self.process_bar(bars[idx:]) 47 | 48 | def get_data_for_plot(self, bar: Bar): 49 | data: Data = self.get_data(bar) 50 | if data is not None: 51 | return [data.longTrail, data.shortTrail, data.longSwing, data.shortSwing] 52 | else: 53 | return [bar.close, bar.close, bar.close, bar.close] 54 | 55 | def get_plot_offset(self): 56 | return 1 57 | 58 | def get_number_of_lines(self): 59 | return 4 60 | 61 | def get_line_styles(self): 62 | return [{"width": 1, "color": "darkGreen", "dash": "dot"}, 63 | {"width": 1, "color": "darkRed", "dash": "dot"}, 64 | {"width": 1, "color": "green"}, 65 | {"width": 1, "color": "red"}] 66 | 67 | def get_line_names(self): 68 | return ["longTrail", "shortTrail", "longSwing", "shortSwing"] 69 | 70 | def process_bar(self, bars: List[Bar]): 71 | atr = clean_range(bars, offset=0, length=self.max_look_back * 2) 72 | 73 | offset = 1 74 | move_length = 1 75 | if (bars[offset].high - bars[offset].low) < (bars[offset + 1].high - bars[offset + 1].low): 76 | move_length = 2 77 | 78 | threshold = atr * self.threshold_factor 79 | 80 | maxDist = atr * self.max_dist_factor 81 | buffer = atr * self.buffer_factor 82 | 83 | [sinceLongReset, longTrail] = self.calc_trail(bars, offset, 1, move_length, threshold, maxDist) 84 | [sinceShortReset, shortTrail] = self.calc_trail(bars, offset, -1, move_length, threshold, maxDist) 85 | 86 | sinceReset = min(sinceLongReset, sinceShortReset) 87 | 88 | if sinceReset >= 3: 89 | last_data: Data = self.get_data(bars[1]) 90 | lastLongSwing = self.calc_swing(bars, 1, last_data.longSwing, sinceReset, buffer) 91 | lastShortSwing = self.calc_swing(bars, -1, last_data.shortSwing, sinceReset, buffer) 92 | if last_data.longSwing is not None and last_data.longSwing < bars[0].high: 93 | lastLongSwing = None 94 | if last_data.shortSwing is not None and last_data.shortSwing > bars[0].low: 95 | lastShortSwing = None 96 | else: 97 | lastLongSwing = None 98 | lastShortSwing = None 99 | 100 | self.write_data(bars[0], 101 | Data(sinceLongReset=sinceLongReset, sinceShortReset=sinceShortReset, longTrail=longTrail, 102 | shortTrail=shortTrail, longSwing=lastLongSwing, shortSwing=lastShortSwing, buffer=buffer, 103 | atr=atr)) 104 | 105 | def calc_swing(self, bars: List[Bar], direction, default, maxLookBack, minDelta): 106 | series = BarSeries.HIGH if direction > 0 else BarSeries.LOW 107 | for length in range(1, min(self.max_swing_length + 1, maxLookBack - 1)): 108 | cex = lowest(bars, length, 1, series) 109 | ex = highest(bars, length, 1, series) 110 | preRange = highest(bars, 2, length + 1, series) 111 | e = ex 112 | if direction < 0: 113 | e = cex 114 | preRange = lowest(bars, 2, length + 1, series) 115 | if direction * (e - preRange) > 0 \ 116 | and direction * (e - get_bar_value(bars[length + 1], series)) > minDelta \ 117 | and direction * (e - get_bar_value(bars[0], series)) > minDelta: 118 | return e + direction * minDelta 119 | 120 | return default 121 | 122 | def calc_trail(self, bars: List[Bar], offset, direction, move_length, threshold, maxDist): 123 | if direction > 0: 124 | range = highest(bars, 2, offset + move_length, BarSeries.HIGH) 125 | move = bars[offset].high - range 126 | last_value = bars[0].low 127 | offset_value = bars[offset].low 128 | else: 129 | range = lowest(bars, 2, offset + move_length, BarSeries.LOW) 130 | move = range - bars[offset].low 131 | last_value = bars[0].high 132 | offset_value = bars[offset].high 133 | 134 | last_data: Data = self.get_data(bars[1]) 135 | if last_data is None: 136 | # defaults 137 | last_since_reset = 0 138 | last_buffer = 0 139 | else: 140 | last_buffer = last_data.buffer 141 | if direction > 0: 142 | last_since_reset = last_data.sinceLongReset 143 | else: 144 | last_since_reset = last_data.sinceShortReset 145 | 146 | if move > threshold and last_since_reset >= move_length and (offset_value - last_value) * direction < 0 and ( 147 | range - last_value) * direction < 0: 148 | sinceReset = move_length + 1 149 | else: 150 | sinceReset = min(last_since_reset + 1, self.max_look_back) 151 | 152 | if direction > 0: 153 | trail = max( 154 | lowest(bars, sinceReset - 1, 0, BarSeries.LOW) - maxDist, 155 | lowest(bars, sinceReset, 0, BarSeries.LOW) - last_buffer) 156 | else: 157 | trail = min( 158 | highest(bars, sinceReset - 1, 0, BarSeries.HIGH) + maxDist, 159 | highest(bars, sinceReset, 0, BarSeries.HIGH) + last_buffer) 160 | 161 | return [sinceReset, trail] 162 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/bybit_linear/bybitlinear_websocket.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import json 3 | 4 | import time 5 | 6 | from kuegi_bot.exchanges.ExchangeWithWS import KuegiWebsocket 7 | 8 | 9 | class BybitLinearPublicPart(KuegiWebsocket): 10 | 11 | def __init__(self, wsURLs, logger, callback, on_message, 12 | symbol, minutesPerBar): 13 | self.messageCallback = on_message 14 | self.minutesPerBar = minutesPerBar 15 | self.symbol = symbol 16 | self.initial_subscribe_done = False 17 | # public -> no apiKey needed 18 | super().__init__(wsURLs, api_key=None, api_secret=None, logger=logger, callback=callback) 19 | 20 | def on_message(self, message): 21 | self.messageCallback(message) 22 | 23 | def subscribe_realtime_data(self): 24 | subbars_intervall = '1' if self.minutesPerBar <= 60 else '60' 25 | self.subscribe_candle(subbars_intervall, self.symbol) 26 | self.subscribe_instrument_info(self.symbol) 27 | 28 | self.initial_subscribe_done = True 29 | 30 | def subscribe_candle(self, interval: str, symbol: str): 31 | args = 'candle.' + interval + '.' + symbol 32 | param = dict( 33 | op='subscribe', 34 | args=[args] 35 | ) 36 | self.ws.send(json.dumps(param)) 37 | 38 | def subscribe_instrument_info(self, symbol): 39 | param = { 40 | 'op': 'subscribe', 41 | 'args': ['instrument_info.100ms.' + symbol] 42 | } 43 | self.ws.send(json.dumps(param)) 44 | 45 | 46 | class BybitLinearWebsocket(KuegiWebsocket): 47 | # User can ues MAX_DATA_CAPACITY to control memory usage. 48 | MAX_DATA_CAPACITY = 200 49 | PRIVATE_TOPIC = ['position', 'execution', 'order'] 50 | 51 | def __init__(self, wsprivateURLs, wspublicURLs, api_key, api_secret, logger, callback, symbol, minutes_per_bar): 52 | self.data = {} 53 | self.symbol = symbol 54 | self.minutes_per_bar = minutes_per_bar 55 | super().__init__(wsprivateURLs, api_key, api_secret, logger, callback) 56 | self.public_ws = BybitLinearPublicPart(wspublicURLs, logger, callback, self.on_message, 57 | symbol=symbol, minutesPerBar=minutes_per_bar) 58 | 59 | def generate_signature(self, expires): 60 | """Generate a request signature.""" 61 | _val = 'GET/realtime' + expires 62 | return str(hmac.new(bytes(self.api_secret, "utf-8"), bytes(_val, "utf-8"), digestmod="sha256").hexdigest()) 63 | 64 | def do_auth(self): 65 | expires = str(int(round(time.time()) + 5)) + "000" 66 | signature = self.generate_signature(expires) 67 | auth = {"op": "auth", "args": [self.api_key, expires, signature]} 68 | self.ws.send(json.dumps(auth)) 69 | 70 | def subscribe_realtime_data(self): 71 | retry_times = 5 72 | while (self.public_ws.ws.sock is None or not self.public_ws.ws.sock.connected) and retry_times > 0: 73 | time.sleep(1) 74 | retry_times -= 1 75 | if retry_times == 0 and (self.public_ws.ws.sock is None or not self.public_ws.ws.sock.connected): 76 | self.logger.error("Couldn't connect to public WebSocket! Exiting.") 77 | self.exit() 78 | self.subscribe_wallet() 79 | self.subscribe_order() 80 | self.subscribe_stop_order() 81 | self.subscribe_execution() 82 | self.subscribe_position() 83 | if not self.public_ws.initial_subscribe_done: 84 | subbars_intervall = '1' if self.minutes_per_bar <= 60 else '60' 85 | args = 'candle.' + subbars_intervall + '.' + self.symbol 86 | if args not in self.data: 87 | self.data[args] = [] 88 | if 'instrument_info.100ms.' + self.symbol not in self.data: 89 | self.data['instrument_info.100ms.' + self.symbol] = [] 90 | self.public_ws.subscribe_realtime_data() 91 | 92 | def exit(self): 93 | super().exit() 94 | self.public_ws.exit() 95 | 96 | def on_message(self, message): 97 | """Handler for parsing WS messages.""" 98 | message = json.loads(message) 99 | if 'success' in message: 100 | if message["success"]: 101 | if 'request' in message and message["request"]["op"] == 'auth': 102 | self.auth = True 103 | self.logger.info("Authentication success.") 104 | if 'ret_msg' in message and message["ret_msg"] == 'pong': 105 | self.data["pong"].append("PING success") 106 | else: 107 | self.logger.error("Error in socket: " + str(message)) 108 | 109 | if 'topic' in message: 110 | self.data[message["topic"]].append(message["data"]) 111 | if len(self.data[message["topic"]]) > BybitLinearWebsocket.MAX_DATA_CAPACITY: 112 | self.data[message["topic"]] = self.data[message["topic"]][BybitLinearWebsocket.MAX_DATA_CAPACITY // 2:] 113 | if self.callback is not None: 114 | self.callback(message['topic']) 115 | 116 | def subscribe_trade(self): 117 | self.public_ws.ws.send('{"op":"subscribe","args":["trade"]}') 118 | if "trade.BTCUSD" not in self.data: 119 | self.data["trade.BTCUSD"] = [] 120 | self.data["trade.ETHUSD"] = [] 121 | self.data["trade.EOSUSD"] = [] 122 | self.data["trade.XRPUSD"] = [] 123 | 124 | def subscribe_insurance(self): 125 | self.ws.send('{"op":"subscribe","args":["insurance"]}') 126 | if 'insurance.BTC' not in self.data: 127 | self.data['insurance.BTC'] = [] 128 | self.data['insurance.XRP'] = [] 129 | self.data['insurance.EOS'] = [] 130 | self.data['insurance.ETH'] = [] 131 | 132 | def subscribe_orderBookL2(self, symbol): 133 | param = { 134 | 'op': 'subscribe', 135 | 'args': ['orderBookL2_25.' + symbol] 136 | } 137 | self.ws.send(json.dumps(param)) 138 | if 'orderBookL2_25.' + symbol not in self.data: 139 | self.data['orderBookL2_25.' + symbol] = [] 140 | 141 | def subscribe_position(self): 142 | self.ws.send('{"op":"subscribe","args":["position"]}') 143 | if 'position' not in self.data: 144 | self.data['position'] = [] 145 | 146 | def subscribe_wallet(self): 147 | self.ws.send('{"op":"subscribe","args":["wallet"]}') 148 | if 'wallet' not in self.data: 149 | self.data['wallet'] = [] 150 | 151 | def subscribe_execution(self): 152 | self.ws.send('{"op":"subscribe","args":["execution"]}') 153 | if 'execution' not in self.data: 154 | self.data['execution'] = [] 155 | 156 | def subscribe_order(self): 157 | self.ws.send('{"op":"subscribe","args":["order"]}') 158 | if 'order' not in self.data: 159 | self.data['order'] = [] 160 | 161 | def subscribe_stop_order(self): 162 | self.ws.send('{"op":"subscribe","args":["stop_order"]}') 163 | if 'stop_order' not in self.data: 164 | self.data['stop_order'] = [] 165 | 166 | def get_data(self, topic): 167 | if topic not in self.data: 168 | self.logger.info(" The topic %s is not subscribed." % topic) 169 | return [] 170 | if topic.split('.')[0] in BybitLinearWebsocket.PRIVATE_TOPIC and not self.auth: 171 | self.logger.info("Authentication failed. Please check your api_key and api_secret. Topic: %s" % topic) 172 | return [] 173 | else: 174 | if len(self.data[topic]) == 0: 175 | return [] 176 | return self.data[topic].pop() 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kuegi Bot 2 | 3 | a simple tradingbot for BTCUSD with connectors for [bybit](https://bit.ly/2rxuv8l "Bybit Homepage"), [bitmex](https://bit.ly/2G4gSB2 "Bitmex Homepage"), [binance futures](https://www.binance.com/en/futures/ref/kuegi) and [phemex](https://phemex.com/register?referralCode=F6YFL). 4 | It just implements my manual tradingstrategy and therefore helps me to stick to the system. 5 | No financial advice. Don't use it if you don't understand what it does. You probably will loose money. 6 | 7 | I only use the bybit connector myself right now. The other connectors are still there, but might have problems due to API changes in the last months which i am not aware of yet. Feel free to make a Pull Request if you find any. 8 | 9 | ## ref links 10 | 11 | if you like it and wanna start a trading account, feel free to use the ref links: 12 | 13 | [bybit](https://www.bybit.com/en/register?affiliate_id=4555&language=en&group_id=0&group_type=1) 14 | 15 | [binance (saves you 10% of fees)](https://www.binance.com/en/register?ref=NV9XQ2JE) 16 | 17 | [binance futures (saves you 10% of fees)](https://www.binance.com/en/futures/ref/kuegi) 18 | 19 | [DigitalOcean](https://m.do.co/c/1767a7ee58ea) The cloud service provider i use for the bot. If you use the reflink you also get a 100$ credit for the first 2 months to try the service risk-free. 20 | ## disclaimer 21 | 22 | right now this is a pure expert tool for my personal use. if you find the code useful thats nice, but don't expect it to be easy to understand or work with it. 23 | It also comes with no warranty whatsoever and is for educational purposes only! 24 | 25 | ## roots 26 | 27 | started with code from https://github.com/BitMEX/sample-market-maker (connector to bitmex mainly but got to change a lot) 28 | 29 | ## donations 30 | 31 | if this helps you in any way, donations are welcome: 32 | 33 | BTC: bc1qfdm2z0xpe7lg70try8x3n3qytrjqzq5y2v6d5n 34 | 35 | ## story time 36 | 37 | i write twitter threads about my journey and the bot. you can either follow me there @mkuegi or read them [here](docs/aboutCodingABot/readme.md) 38 | 39 | # Getting started 40 | 41 | This section describes how to get the environment (i highly recommend virtual environments) set up to run the bot (backtest and trading). As mentioned before, this is highly experimental and use of the bot on real money is not recommended unless you *really* know what you are doing. 42 | 43 | ## needed modules 44 | first install the needed packages via pip. They are listed in requirements.txt and can be installed via the standard steps 45 | 46 | ## execution 47 | 48 | currently the only way to execute the bot is by running the included scripts. 49 | 50 | # bot and strategies 51 | 52 | Currently there are specific bots for the kuegi-strategy and a simpler SFP strategy. 53 | Since those two work nicely together, i also created a MultiStrategyBot which can execute multiple strategies within one account and exchange. 54 | 55 | ## Kuegi Strategy 56 | Here i will explain my strategy once i find the time 57 | 58 | ## SFP 59 | a simple swing-failure pattern strategy. I will provide more information when the time is right. 60 | 61 | # backtest 62 | 63 | ## crawling history 64 | To run backtests you first have to collect data history from the exchange. 65 | For the known exchanges i created the history_crawler script. 66 | make sure that a folder "history" is existing in the main directory (next to the script) and run 67 | ``` 68 | python3 history_crawler.py bybit 69 | ``` 70 | if you want to crawl bitmex, you have to replace `bybit` with `bitmex` 71 | 72 | for future calls adapt the known_history_files in utils/helper to the highest existing index of the M1 files. 73 | 74 | The crawler loads M1 data from the exchange in max batchsize and aggregates it in a way to be easily reused from the backtest afterwards. The download can take a long time since its lots of data to fetch. 75 | 76 | If you use another exchange or existing data, you need to make sure that the history data is saved in the same structure as the crawler does it. 77 | 78 | ## running the backtest 79 | 80 | in backtest.py you find a collection of code snippets and functions for backtesting. they are mostly commented out to be used in the python console. 81 | 82 | run 83 | ``` 84 | python3 -i backtest.py 85 | ``` 86 | to execute the script with interactive console afterwards. there you can for 87 | 88 | load bars from the history: 89 | ``` 90 | bars = load_bars(,,,) 91 | ``` 92 | 93 | where `daysInHistory` and `timeFrameInMinutes` should be obvious. 94 | `barOffset` is an option to shift bars. f.e. when the default first H4 bar starts at 00:00, with this parameter you can make him start at 01:00 etc. 95 | This is pretty useful to test for stability of the bot. small changes in input (like shifting the bars) shouldn't result in big changes of the performance. 96 | 97 | those loaded bars you can then use for backtests. create a tradingBot (a class derived from TradingBot in kuegibot/bots/trading_bot.py). I would recommend using `MultiStrategyBot` since thats the one i am using in production too. 98 | 99 | There are multiple samples in the file how to create a bot and add strategies (for MultiStrategyBot). 100 | 101 | Once the bot is created and set up, call 102 | ```b= BackTest(bot,bars).run()``` 103 | this runs the backtest and prints some performance numbers into the logs. 104 | 105 | i mainly use the "rel:" number which is the relation between profit (per year) and the maxDD. 106 | i consider a relation of greater than 4 a good performance. but you need to decide for yourself 107 | 108 | after the run you can call 109 | ```bot.create_performance_plot().show()``` 110 | to create a chart with more detailed performance analysis 111 | 112 | or 113 | ```b.prepare_plot().show()``` 114 | to create a chart with the historic bar data and the positions plotted for detailed analysis. 115 | 116 | # production 117 | 118 | ## disclaimer 119 | As stated before: i *DO NOT RECOMMEND* to run this bot on your own money out of the box. If you understand what it does, checked the code and are confident that it does what you want, feel free to run it. 120 | I am running it on my own money, but i also know every line of the code and did extensive tests over 5 months before scaling up to serious position sizes. 121 | 122 | so seriously: don't run it. You risk losing all your money! 123 | 124 | ## settings 125 | all settings should be placed within the settings folder. 126 | 127 | When started via the cryptobot script, it first reads default settings from `settings/default.json`. 128 | If a path argument is provided, the first one should be a path to the full settings file with all data needed for the bot. 129 | 130 | see a sample file in `settings/sample.json`. 131 | 132 | The settings needs to contain one "bot" directory per exchange. The minimum required changes in the sample file are 133 | - the `API_KEY` and `API_SECRET` set to your key and secret. 134 | - `KB_RISK_FACTOR` set to the (average) amount of btc to be risked per trade. 135 | *WARNING*: depending on the strategy this might not be the actual max-risked amount but a target for the average. Also a trade might loose more than this amount because of slipage and execution problems on the exchange. 136 | - If you **really** want to run it on a live exchange, also set `IS_TEST` to false. Do this at your own risk! 137 | 138 | ## realtime usage 139 | if running the bot on a real exchange you need to run it 24/7. For this i recommend getting a server and set the bot up as a daemon that is restarted on any failure. 140 | The bot generally is prepared for this usage. Open Positions are stored on the disk to easy pick up after a restart. 141 | 142 | The bot also has some security measures to prevent uncovered positions. If he finds an open position on the exchange without a matching position on file and specially without a stoploss, 143 | it closes this position. This also means that you must not trade other positions with the account of the bot. otherwise it will close them instantly and you loose money. -------------------------------------------------------------------------------- /docs/ChannelStrategy.ps: -------------------------------------------------------------------------------- 1 | //@version=4 2 | strategy("Channel Strategy", overlay=true, commission_value=0.05, slippage = 10, pyramiding=4, calc_on_order_fills=true) 3 | 4 | 5 | maxLookBack= input(title="Max Lookback", type=input.integer,defval=13, minval=3) 6 | thresFac= input(title="threshold Factor", type=input.float,defval=0.9, minval=0.1) 7 | bufferFac= input(title="buffer Factor", type=input.float,defval=0.05, minval=-0.1) 8 | maxDistFac= input(title="max Dist in ATR", type=input.float,defval=2, minval=0.1) 9 | maxChannelSizeFac= input(title="max Channelsize in ATR", type=input.float,defval=6, minval=0.5) 10 | initialRisk= input(title="initialRisk (0 means 1%)", type=input.float,defval=1000, minval=0) 11 | direction= input(title="DirectionFilter", type=input.integer,defval=0, minval=-1) 12 | monthsBack = input(title="only test months back -1 for all",type=input.integer,defval=-1,minval=-1) 13 | 14 | 15 | highest_(values, length, offset) => 16 | float h_val = na 17 | if length >= 1 18 | for i = offset to offset+length-1 19 | if ( na(h_val) or (not na(values[i]) and values[i] > h_val ) ) 20 | h_val := values[i] 21 | h_val 22 | 23 | lowest_(values, length, offset) => 24 | float l_val = na 25 | if length >= 1 26 | for i = offset to offset+length-1 27 | if ( na(l_val) or (not na(values[i]) and values[i] < l_val ) ) 28 | l_val := values[i] 29 | l_val 30 | 31 | cleanRange(length) => 32 | float h1= na 33 | float h2= na 34 | float h3= na 35 | float h4= na 36 | for i = 0 to length - 1 37 | float range= high[i] - low[i] 38 | if na(h1) or range > h1 39 | h4:= h3 40 | h3:= h2 41 | h2:= h1 42 | h1:= range 43 | else 44 | if na(h2) or range > h2 45 | h4:= h3 46 | h3:= h2 47 | h2:= range 48 | else 49 | if na(h3) or range > h3 50 | h4:= h3 51 | h3:= range 52 | else 53 | if na(h4) or range > h4 54 | h4:= range 55 | 56 | float sum= 0 57 | int count=0 58 | for i= 0 to length - 1 59 | float range= high[i] - low[i] 60 | if (length > 20 and range < h4) or (length > 15 and range < h3) or (length > 10 and range < h2) or (length > 5 and range < h1) 61 | sum := sum + range 62 | count:= count + 1 63 | sum/count 64 | 65 | float myAtr= cleanRange(maxLookBack*2) 66 | 67 | int sinceLongReset= 0 68 | int sinceShortReset= 0 69 | int offset= 1 70 | 71 | int move= 1 72 | if((high[offset]-low[offset]) < (high[offset+1]-low[offset+1])) 73 | move:= 2 74 | 75 | float rangeHH= highest(high,2)[offset+move] 76 | float rangeLL= lowest(low,2)[offset+move] 77 | float threshold= myAtr*thresFac 78 | 79 | float maxDist= myAtr*maxDistFac 80 | float buffer= myAtr*bufferFac 81 | 82 | float moveUp= high[offset]-rangeHH 83 | float moveDown= rangeLL - low[offset] 84 | 85 | if(moveUp > threshold and sinceLongReset[1] >= move and low[offset] < low[0] and rangeHH < low) 86 | sinceLongReset:= move+1 87 | else 88 | sinceLongReset:= min(nz(sinceLongReset[1])+1,maxLookBack) 89 | float longTrail= max(lowest_(low,sinceLongReset-1,0)-maxDist,lowest_(low,sinceLongReset,0)-buffer[1]) 90 | 91 | if(moveDown > threshold and sinceShortReset[1] >= move and high[offset] > high[0] and rangeLL> high) 92 | sinceShortReset:= move+1 93 | else 94 | sinceShortReset:= min(nz(sinceShortReset[1])+1,maxLookBack) 95 | float shortTrail= min(highest_(high,sinceShortReset-1,0)+maxDist,highest_(high,sinceShortReset,0)+buffer[1]) 96 | 97 | // SWINGS 98 | 99 | float minDelta= myAtr*bufferFac 100 | swing(series,direction,default,maxLookBack) => 101 | float val= na 102 | for length = 1 to 3 103 | if ( na(val) and length < maxLookBack) 104 | float cex= lowest_(series,length,1) 105 | float ex= highest_(series,length,1) 106 | float s= highest_(series,2,length+1) 107 | float e= ex 108 | if direction < 0 109 | e:= cex 110 | s:= lowest_(series,2,length+1) 111 | if direction*(e-s) > 0 and direction*(e-series[length+1]) > minDelta and direction*(e-series[0]) > minDelta 112 | val := e + direction*minDelta 113 | 114 | if na(val) 115 | val:= default 116 | val 117 | 118 | 119 | sinceReset= min(sinceShortReset, sinceLongReset) 120 | float lastLongSwing= na 121 | lastLongSwing:= swing(high,1,lastLongSwing[1],sinceReset) 122 | if lastLongSwing < high 123 | lastLongSwing:= na 124 | 125 | float lastShortSwing= na 126 | lastShortSwing := swing(low,-1,lastShortSwing[1],sinceReset) 127 | if lastShortSwing > low 128 | lastShortSwing:= na 129 | 130 | if sinceReset < 3 131 | lastLongSwing:= na 132 | lastShortSwing:= na 133 | 134 | 135 | //=========================================================== 136 | //STRATEGY 137 | //=========================================================== 138 | 139 | timethreshold= timenow - monthsBack*30*24*60*60*1000 140 | if(monthsBack <= 0) 141 | timethreshold:= 0 142 | 143 | int coolOff= 0 144 | float stopLong= lastShortSwing 145 | stopLong:= nz(stopLong[1],lastShortSwing) 146 | float stopShort= lastLongSwing 147 | stopShort:= nz(stopShort[1],lastLongSwing) 148 | 149 | if(strategy.position_size != strategy.position_size[1]) 150 | coolOff:= 0 151 | else 152 | coolOff:= nz(coolOff[1],0)+1 153 | 154 | 155 | 156 | float longEntry= na 157 | float shortEntry= na 158 | swingRange= lastLongSwing - lastShortSwing 159 | 160 | if( na(swingRange[1]) == false and na(swingRange) == false and swingRange > 0 and swingRange < myAtr*maxChannelSizeFac) 161 | float risk= (strategy.initial_capital + strategy.netprofit)*0.01 //not equity, cause dont count open profit! 162 | if(initialRisk > 0) 163 | risk := initialRisk //fixed risk 164 | 165 | if(strategy.position_size <= 0 or nz(coolOff[1],0) > 1) 166 | stopLong:= max(lastShortSwing,longTrail) 167 | if(strategy.position_size >= 0 or nz(coolOff[1],0) > 1) 168 | stopShort:= min(lastLongSwing,shortTrail) 169 | 170 | stopLong:= max(stopLong,longTrail) 171 | stopShort:= min(stopShort,shortTrail) 172 | 173 | longEntry:= lastLongSwing 174 | shortEntry:= lastShortSwing 175 | 176 | float diffLong= longEntry - stopLong 177 | if(diffLong == 0) 178 | diffLong:= swingRange 179 | 180 | float diffShort= stopShort - shortEntry 181 | if(diffShort == 0) 182 | diffShort:= swingRange 183 | if(strategy.position_size <= 0 or nz(coolOff[1],0) > 1) 184 | size= risk/diffLong 185 | strategy.order("Long Entry",strategy.long, size, stop=longEntry, when = direction >= 0 and time > timethreshold ) 186 | if(strategy.position_size >= 0 or nz(coolOff[1],0) > 1) 187 | size= risk/diffShort 188 | strategy.order("Short Entry",strategy.short, size, stop=shortEntry, when = direction <= 0 and time > timethreshold ) 189 | else 190 | stopLong:= max(stopLong,longTrail) 191 | stopShort:= min(stopShort,shortTrail) 192 | longEntry:= na 193 | shortEntry:= na 194 | strategy.cancel("Short Entry") 195 | strategy.cancel("Long Entry") 196 | 197 | 198 | strategy.exit("Long exit","Long Entry",stop=stopLong) 199 | strategy.exit("Short exit","Short Entry",stop=stopShort) 200 | 201 | // Plots 202 | 203 | plot(longEntry,offset=1,color=color.green, style= plot.style_stepline, linewidth=2) 204 | plot(shortEntry,offset=1,color=color.red, style= plot.style_stepline, linewidth=2) 205 | plot(stopLong,offset=1,color=color.blue, style= plot.style_stepline) 206 | plot(stopShort,offset=1,color=color.blue, style= plot.style_stepline) 207 | plot(strategy.position_size) 208 | 209 | 210 | //plot(lastLongSwing, offset=1, title ="Long Swing", style= plot.style_stepline, color= color.green, linewidth= 1) 211 | //plot(lastShortSwing, offset=1, title ="Short Swing", style= plot.style_stepline, color= color.red, linewidth= 1) 212 | //p1= plot(longTrail, offset=1, title ="Long Trail", style= plot.style_stepline, color= color.blue, linewidth= 1) 213 | //p2= plot(shortTrail, offset=1, title ="Short Trail", style= plot.style_stepline, color= color.blue, linewidth= 1) 214 | //fill(p1, p2, color=color.blue) 215 | -------------------------------------------------------------------------------- /kuegi_bot/exchanges/phemex/client.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hmac 3 | import hashlib 4 | import json 5 | import requests 6 | import time 7 | 8 | from math import trunc 9 | 10 | 11 | class PhemexAPIException(Exception): 12 | 13 | def __init__(self, response): 14 | self.code = 0 15 | try: 16 | json_res = response.json() 17 | except ValueError: 18 | self.message = 'Invalid error message: {}'.format(response.text) 19 | else: 20 | if 'code' in json_res: 21 | self.code = json_res['code'] 22 | self.message = json_res['msg'] 23 | else: 24 | self.code = json_res['error']['code'] 25 | self.message = json_res['error']['message'] 26 | self.status_code = response.status_code 27 | self.response = response 28 | self.request = getattr(response, 'request', None) 29 | 30 | def __str__(self): # pragma: no cover 31 | return 'HTTP(code=%s), API(errorcode=%s): %s' % (self.status_code, self.code, self.message) 32 | 33 | 34 | class Client(object): 35 | MAIN_NET_API_URL = 'https://api.phemex.com' 36 | TEST_NET_API_URL = 'https://testnet-api.phemex.com' 37 | 38 | CURRENCY_BTC = "BTC" 39 | CURRENCY_USD = "USD" 40 | 41 | SYMBOL_BTCUSD = "BTCUSD" 42 | SYMBOL_ETHUSD = "ETHUSD" 43 | SYMBOL_XRPUSD = "XRPUSD" 44 | 45 | SIDE_BUY = "Buy" 46 | SIDE_SELL = "Sell" 47 | 48 | ORDER_TYPE_MARKET = "Market" 49 | ORDER_TYPE_LIMIT = "Limit" 50 | 51 | TIF_IMMEDIATE_OR_CANCEL = "ImmediateOrCancel" 52 | TIF_GOOD_TILL_CANCEL = "GoodTillCancel" 53 | TIF_FOK = "FillOrKill" 54 | 55 | ORDER_STATUS_NEW = "New" 56 | ORDER_STATUS_PFILL = "PartiallyFilled" 57 | ORDER_STATUS_FILL = "Filled" 58 | ORDER_STATUS_CANCELED = "Canceled" 59 | ORDER_STATUS_REJECTED = "Rejected" 60 | ORDER_STATUS_TRIGGERED = "Triggered" 61 | ORDER_STATUS_UNTRIGGERED = "Untriggered" 62 | 63 | def __init__(self, api_key=None, api_secret=None, is_testnet=False): 64 | self.api_key = api_key 65 | self.api_secret = api_secret 66 | self.api_URL = self.MAIN_NET_API_URL 67 | if is_testnet: 68 | self.api_URL = self.TEST_NET_API_URL 69 | 70 | self.session = requests.session() 71 | 72 | @staticmethod 73 | def generate_signature(message, api_secret, body_string=None): 74 | expiry = trunc(time.time()) + 60 75 | message += str(expiry) 76 | if body_string is not None: 77 | message += body_string 78 | return [hmac.new(api_secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest(), expiry] 79 | 80 | def _send_request(self, method, endpoint, params={}, body={}): 81 | query_string = '&'.join(['{}={}'.format(k, v) for k, v in params.items()]) 82 | message = endpoint + query_string 83 | body_str = "" 84 | if body: 85 | body_str = json.dumps(body, separators=(',', ':')) 86 | [signature, expiry] = self.generate_signature(message, self.api_secret, body_string=body_str) 87 | self.session.headers.update({ 88 | 'x-phemex-request-signature': signature, 89 | 'x-phemex-request-expiry': str(expiry), 90 | 'x-phemex-access-token': self.api_key, 91 | 'Content-Type': 'application/json'}) 92 | 93 | url = self.api_URL + endpoint 94 | if query_string: 95 | url += '?' + query_string 96 | response = self.session.request(method, url, data=body_str.encode()) 97 | if not str(response.status_code).startswith('2'): 98 | raise PhemexAPIException(response) 99 | try: 100 | res_json = response.json() 101 | except ValueError: 102 | raise PhemexAPIException('Invalid Response: %s' % response.text) 103 | if "code" in res_json and res_json["code"] != 0: 104 | # accepted errorcodes 105 | if res_json['code'] in [10002]: 106 | res_json['data'] = None 107 | else: 108 | raise PhemexAPIException(response) 109 | if "error" in res_json and res_json["error"]: 110 | raise PhemexAPIException(response) 111 | return res_json 112 | 113 | def query_account_n_positions(self, currency: str): 114 | """ 115 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#querytradeaccount 116 | """ 117 | return self._send_request("get", "/accounts/accountPositions", {'currency': currency}) 118 | 119 | def query_products(self): 120 | """ 121 | https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#query-product-information 122 | """ 123 | return self._send_request("get", "/v1/exchange/public/products", {}) 124 | 125 | def query_kline(self, symbol: str, fromTimestamp: int, toTimestamp: int, resolutionSeconds: int): 126 | """ 127 | """ 128 | return self._send_request("get", "/phemex-user/public/md/kline", 129 | {"symbol": symbol, 130 | "from": fromTimestamp, 131 | "to": toTimestamp, 132 | "resolution": resolutionSeconds}) 133 | 134 | def place_order(self, params={}): 135 | """ 136 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#placeorder 137 | """ 138 | return self._send_request("post", "/orders", body=params) 139 | 140 | def amend_order(self, symbol, orderID, params={}): 141 | """ 142 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#622-amend-order-by-orderid 143 | """ 144 | params["symbol"] = symbol 145 | params["orderID"] = orderID 146 | return self._send_request("put", "/orders/replace", params=params) 147 | 148 | def cancel_order(self, symbol, orderID): 149 | """ 150 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#623-cancel-single-order 151 | """ 152 | return self._send_request("delete", "/orders/cancel", params={"symbol": symbol, "orderID": orderID}) 153 | 154 | def _cancel_all(self, symbol, untriggered_order=False): 155 | """ 156 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#625-cancel-all-orders 157 | """ 158 | return self._send_request("delete", "/orders/all", 159 | params={"symbol": symbol, "untriggered": str(untriggered_order).lower()}) 160 | 161 | def cancel_all_normal_orders(self, symbol): 162 | self._cancel_all(symbol, untriggered_order=False) 163 | 164 | def cancel_all_untriggered_conditional_orders(self, symbol): 165 | self._cancel_all(symbol, untriggered_order=True) 166 | 167 | def cancel_all(self, symbol): 168 | self._cancel_all(symbol, untriggered_order=False) 169 | self._cancel_all(symbol, untriggered_order=True) 170 | 171 | def change_leverage(self, symbol, leverage=0): 172 | """ 173 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#627-change-leverage 174 | """ 175 | return self._send_request("PUT", "/positions/leverage", params={"symbol": symbol, "leverage": leverage}) 176 | 177 | def change_risklimit(self, symbol, risk_limit=0): 178 | """ 179 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#628-change-position-risklimit 180 | """ 181 | return self._send_request("PUT", "/positions/riskLimit", params={"symbol": symbol, "riskLimit": risk_limit}) 182 | 183 | def query_open_orders(self, symbol): 184 | """ 185 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#6210-query-open-orders-by-symbol 186 | """ 187 | return self._send_request("GET", "/orders/activeList", params={"symbol": symbol}) 188 | 189 | def query_24h_ticker(self, symbol): 190 | """ 191 | https://github.com/phemex/phemex-api-docs/blob/master/Public-API-en.md#633-query-24-hours-ticker 192 | """ 193 | return self._send_request("GET", "/md/ticker/24hr", params={"symbol": symbol}) 194 | -------------------------------------------------------------------------------- /kuegi_bot/bots/bot_with_channel.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import math 3 | import plotly.graph_objects as go 4 | 5 | from kuegi_bot.bots.trading_bot import TradingBot 6 | from kuegi_bot.indicators.kuegi_channel import KuegiChannel, Data 7 | from kuegi_bot.utils.trading_classes import Bar, Account, Symbol, OrderType, Position 8 | 9 | 10 | class BotWithChannel(TradingBot): 11 | def __init__(self, logger, directionFilter: int = 0): 12 | super().__init__(logger, directionFilter) 13 | self.channel: KuegiChannel = None 14 | self.risk_factor = 1 15 | self.risk_type = 0 # 0= all equal, 1= 1 atr eq 1 R 16 | self.max_risk_mul = 1 17 | self.be_factor = 0 18 | self.be_buffer = 0 19 | self.trail_to_swing = False 20 | self.delayed_swing_trail = True 21 | self.trail_back = False 22 | self.trail_active = False 23 | 24 | def withRM(self, risk_factor: float = 0.01, max_risk_mul: float = 2, risk_type: int = 0): 25 | self.risk_factor = risk_factor 26 | self.risk_type = risk_type # 0= all equal, 1= 1 atr eq 1 R 27 | self.max_risk_mul = max_risk_mul 28 | return self 29 | 30 | def withChannel(self, max_look_back, threshold_factor, buffer_factor, max_dist_factor, max_swing_length): 31 | self.channel = KuegiChannel(max_look_back, threshold_factor, buffer_factor, max_dist_factor, max_swing_length) 32 | return self 33 | 34 | def withBE(self, factor, buffer): 35 | self.be_factor = factor 36 | self.be_buffer = buffer 37 | return self 38 | 39 | def withTrail(self, trail_to_swing: bool = False, delayed_swing: bool = True, trail_back: bool = False): 40 | self.trail_active = True 41 | self.delayed_swing_trail = delayed_swing 42 | self.trail_to_swing = trail_to_swing 43 | self.trail_back = trail_back 44 | return self 45 | 46 | def init(self, bars: List[Bar], account: Account, symbol: Symbol, unique_id: str = ""): 47 | if self.channel is None: 48 | self.logger.error("No channel provided on init") 49 | else: 50 | self.logger.info("init %s with %i %.1f %.3f %.1f %i | %.3f %.1f %i | %.1f %.1f | %s %s %s %s" % 51 | (unique_id, 52 | self.channel.max_look_back, self.channel.threshold_factor, self.channel.buffer_factor, 53 | self.channel.max_dist_factor, self.channel.max_swing_length, 54 | self.risk_factor, self.max_risk_mul, self.risk_type, 55 | self.be_factor, self.be_buffer, 56 | self.trail_active, self.delayed_swing_trail, self.trail_to_swing, self.trail_back)) 57 | self.channel.on_tick(bars) 58 | super().init(bars=bars, account=account, symbol=symbol, unique_id=unique_id) 59 | 60 | def min_bars_needed(self): 61 | return self.channel.max_look_back + 1 62 | 63 | def prep_bars(self, bars: list): 64 | if self.is_new_bar: 65 | self.channel.on_tick(bars) 66 | 67 | def got_data_for_position_sync(self, bars: List[Bar]): 68 | return self.channel.get_data(bars[1]) is not None 69 | 70 | def get_stop_for_unmatched_amount(self, amount: float, bars: List[Bar]): 71 | data = self.channel.get_data(bars[1]) 72 | stopLong = int(max(data.shortSwing, data.longTrail) if data.shortSwing is not None else data.longTrail) 73 | stopShort = int(min(data.longSwing, data.shortTrail) if data.longSwing is not None else data.shortTrail) 74 | return stopLong if amount > 0 else stopShort 75 | 76 | def manage_open_orders(self, bars: List[Bar], account: Account): 77 | self.sync_executions(bars, account) 78 | 79 | # Trailing 80 | if len(bars) < 5: 81 | return 82 | 83 | # trail stop only on new bar 84 | last_data: Data = self.channel.get_data(bars[2]) 85 | data: Data = self.channel.get_data(bars[1]) 86 | if data is not None: 87 | stopLong = data.longTrail 88 | stopShort = data.shortTrail 89 | if self.trail_to_swing and \ 90 | data.longSwing is not None and data.shortSwing is not None and \ 91 | (not self.delayed_swing_trail or (last_data is not None and 92 | last_data.longSwing is not None and 93 | last_data.shortSwing is not None)): 94 | stopLong = max(data.shortSwing, stopLong) 95 | stopShort = min(data.longSwing, stopShort) 96 | 97 | to_update = [] 98 | for order in account.open_orders: 99 | posId = self.position_id_from_order_id(order.id) 100 | if posId not in self.open_positions.keys(): 101 | continue 102 | pos: Position = self.open_positions[posId] 103 | orderType = self.order_type_from_order_id(order.id) 104 | if pos is not None and orderType == OrderType.SL: 105 | # trail 106 | newStop = order.stop_price 107 | isLong = pos.amount > 0 108 | if self.trail_active: 109 | newStop = self.__trail_stop(direction=1 if isLong else -1, 110 | current_stop=newStop, 111 | trail=stopLong if isLong else stopShort, 112 | initial_stop=pos.initial_stop) 113 | 114 | if self.be_factor > 0 and pos.wanted_entry is not None and pos.initial_stop is not None: 115 | entry_diff = (pos.wanted_entry - pos.initial_stop) 116 | ep = bars[0].high if isLong else bars[0].low 117 | if (ep - (pos.wanted_entry + entry_diff * self.be_factor)) * pos.amount > 0: 118 | newStop = self.__trail_stop(direction=1 if isLong else -1, 119 | current_stop=newStop, 120 | trail=pos.wanted_entry + entry_diff * self.be_buffer, 121 | initial_stop=pos.initial_stop) 122 | if newStop != order.stop_price: 123 | order.stop_price = newStop 124 | to_update.append(order) 125 | 126 | for order in to_update: 127 | self.order_interface.update_order(order) 128 | 129 | def calc_pos_size(self, risk, entry, exitPrice, data: Data): 130 | if self.risk_type <= 2: 131 | delta = entry - exitPrice 132 | if self.risk_type == 1: 133 | # use atr as delta reference, but max X the actual delta. so risk is never more than X times the 134 | # wanted risk 135 | delta = math.copysign(min(self.max_risk_mul * abs(delta), self.max_risk_mul * data.atr), delta) 136 | 137 | if not self.symbol.isInverse: 138 | size = risk / delta 139 | else: 140 | size = -int(risk / (1 / entry - 1 / (entry - delta))) 141 | return size 142 | 143 | def __trail_stop(self, direction, current_stop, trail, initial_stop): 144 | # direction should be > 0 for long and < 0 for short 145 | if (trail - current_stop) * direction > 0 or \ 146 | (self.trail_back and initial_stop is not None and (trail - initial_stop) * direction > 0): 147 | return math.floor(trail) if direction < 0 else math.ceil(trail) 148 | else: 149 | return current_stop 150 | 151 | def add_to_plot(self, fig: go.Figure, bars: List[Bar], time): 152 | super().add_to_plot(fig, bars, time) 153 | lines = self.channel.get_number_of_lines() 154 | styles = self.channel.get_line_styles() 155 | names = self.channel.get_line_names() 156 | offset = 1 # we take it with offset 1 157 | self.logger.info("adding channel") 158 | for idx in range(0, lines): 159 | sub_data = list(map(lambda b: self.channel.get_data_for_plot(b)[idx], bars)) 160 | fig.add_scatter(x=time, y=sub_data[offset:], mode='lines', line=styles[idx], 161 | name=self.channel.id + "_" + names[idx]) 162 | --------------------------------------------------------------------------------