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 |
PositionId
status
signalTime
6 |
amount
7 |
wEntry
initSL
entryTime
8 |
entry
stop
worst
Risk
9 |
10 |
11 |
12 | {{#each this}}
13 |
14 |
{{id}}
15 |
16 |
{{formatTime last_tick_tstamp}}
17 |
{{totalPos}}
18 |
{{equity}}
19 |
{{max_equity}}
20 |
{{drawdown}} {{uwdays}} d
21 |
22 |
23 |
{{formatResult totalWorstCase}}
24 |
R={{risk_reference}}
25 |
26 | {{#each positions}}
27 |
28 |
{{id}}
29 |
{{status}}
30 |
{{formatTime signal_tstamp}}
31 |
{{amount}}
32 |
{{formatPrice wanted_entry}}
33 |
{{formatPrice initial_stop}}
34 |
{{formatTime entry_tstamp}}
35 |
{{formatPrice filled_entry}}
36 |
{{formatPrice currentStop}}
37 |
{{formatResult worstCase}}
38 |
{{formatPrice initialRisk}}
39 |
40 | {{/each}}
41 | {{/each}}
42 |
43 |
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 |
--------------------------------------------------------------------------------