diff --git a/.dockerignore b/.dockerignore index f7ace482..ac0a530a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,6 @@ .gitignore .gitmodules docs -tests scripts docker-compose.yml diff --git a/Dockerfile b/Dockerfile index 2fd6a730..bba04485 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,21 @@ FROM navikey/raspbian-bullseye:latest as base-image RUN apt update && \ + apt upgrade -y \ apt install -y \ --no-install-recommends \ python3 python3-rpi.gpio libatlas-base-dev libopenjp2-7 libtiff5 libxcb1 libfreetype6-dev \ && rm -rf /var/lib/apt/lists/* FROM base-image AS build-image -RUN apt update && \ - apt install -y \ +RUN apt install -y \ --no-install-recommends \ python3-pip git \ && rm -rf /var/lib/apt/lists/* RUN pip3 install --upgrade pip COPY requirements.txt . -RUN pip3 install -v --extra-index-url https://www.piwheels.org/simple --user -r requirements.txt +RUN pip3 install -v --prefer-binary --extra-index-url https://www.piwheels.org/simple --user -r requirements.txt FROM base-image AS release-image COPY --from=build-image /root/.local /root/.local diff --git a/config/base.mplstyle b/config/base.mplstyle index 03577908..b18432b9 100644 --- a/config/base.mplstyle +++ b/config/base.mplstyle @@ -1,31 +1,38 @@ figure.dpi: 100 -#figure.autolayout: True -#savefig.pad_inches: 0 -font.family: sans-serif -font.sans-serif: 04b03 -font.weight: light +# figure.autolayout: True +#figure.constrained_layout.use: True + +savefig.pad_inches: 0 savefig.transparent: True + +font.family: sans-serif +font.sans-serif: basis33 + text.hinting_factor:1 text.hinting: native text.antialiased: False + patch.antialiased: False lines.antialiased: False + figure.subplot.hspace: 0 +figure.frameon: False axes.facecolor: white +axes.edgecolor: red axes.linewidth: 0.5 axes.spines.left: True axes.spines.bottom: True axes.spines.top: False axes.spines.right: False axes.grid: False +# margin between candles and axes +axes.xmargin : 0 +axes.ymargin : 0 -grid.linestyle: - +grid.linestyle: : grid.linewidth: 0.5 grid.color: black -axes.edgecolor: red - -ytick.major.pad: 0 xtick.color: red ytick.color: red @@ -33,18 +40,20 @@ ytick.color: red xtick.labelcolor: black ytick.labelcolor: black -xtick.labelsize: 12 -ytick.labelsize: 12 +xtick.labelsize: 11 +ytick.labelsize: 11 -xtick.alignment: center +xtick.alignment: left ytick.alignment: bottom ytick.major.size: 5 xtick.major.size: 5 xtick.minor.size: 3 -xtick.direction: inout -ytick.direction: inout +xtick.direction: in +ytick.direction: in ytick.major.width: 0.5 -xtick.major.width: 0.5 \ No newline at end of file +xtick.major.width: 0.5 + +# xtick.major.pad: 0 \ No newline at end of file diff --git a/config/default.expanded.mplstyle b/config/default.expanded.mplstyle index 8081f161..8de5e3be 100644 --- a/config/default.expanded.mplstyle +++ b/config/default.expanded.mplstyle @@ -4,16 +4,14 @@ axes.spines.top: False axes.spines.right: False axes.autolimit_mode: data -axes.xmargin: 0.1 -axes.ymargin: 0.1 xtick.major.size: 5 ytick.major.size: 5 xtick.direction: in ytick.direction: in ytick.minor.visible: False -xtick.major.pad: -20 -ytick.major.pad: -5 +xtick.major.pad: -5 +ytick.major.pad: -0 figure.subplot.left: 0 figure.subplot.right: 1 diff --git a/config/small.mplstyle b/config/small.mplstyle index 059c74ce..a2d47d3a 100644 --- a/config/small.mplstyle +++ b/config/small.mplstyle @@ -1,3 +1,8 @@ + +font.family: sans-serif +font.sans-serif: 04b03 +font.weight: light + xtick.labelsize: 6 ytick.labelsize: 6 diff --git a/docs/images/HardwareDiagrams/InkyWhat.Diagram.svg b/docs/images/HardwareDiagrams/InkyWhat.Diagram.svg index 56d7899c..196c9d34 100644 --- a/docs/images/HardwareDiagrams/InkyWhat.Diagram.svg +++ b/docs/images/HardwareDiagrams/InkyWhat.Diagram.svg @@ -4,5 +4,5 @@ svgOutput made with tinkercad - + \ No newline at end of file diff --git a/docs/notes.md b/docs/notes.md index 465b259a..19e72e70 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -1,13 +1,14 @@ todo: - - ## layout + * fix expanded mode - fit candle count to screen size - - work out margins, calc mad candles that can fit and generate date to provide set of candles + - keep an eye on the overlay least intrusive position algo, seems to be flaky - ## multi-currency support - button to toggle between curencies - multi-plot display - overlapping coloured multi-coin charts + - ## impression: - better button actions! - make these state-based, so photo mode behaves different to chart mode @@ -40,7 +41,7 @@ docker run -e QEMU_CPU=arm1176 --privileged --rm -it --platform linux/arm/v6 bit # remove all containers docker container rm $(docker container ls -q -a) #' which cpus to use for the build ---cpuset-cpus=0-3' +# --cpuset-cpus=0-3' # wifi-connect docker pull balenablocks/wifi-connect:rpi docker run --network=host -v /run/dbus/:/run/dbus/ balenablocks/wifi-connect:rpi @@ -52,6 +53,13 @@ docker run --network=host -v /run/dbus/:/run/dbus/ balenablocks/wifi-connect:rpi # test run docker run --rm --env BITBOT_TESTRUN=true --env BITBOT_OUTPUT=disk --env BITBOT_SHOWIMAGE=false bb +# run tests +docker run --rm \ +--name bitbot_tests \ +--env BITBOT_TESTRUN=true --env BITBOT_OUTPUT=disk --env BITBOT_SHOWIMAGE=false \ +--mount type=bind,source="$(pwd)",target=/code/tests/images \ +bb \ +python3 -m unittest discover ``` > get linux os version diff --git a/hotdogs.ipynb b/hotdogs.ipynb new file mode 100644 index 00000000..e59f132e --- /dev/null +++ b/hotdogs.ipynb @@ -0,0 +1,466 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Bitbot" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Jupyter" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "TGhh4E4ctNiS" + }, + "outputs": [], + "source": [ + "# This allows multiple outputs from a single jupyter notebook cell:\n", + "from IPython.core.interactiveshell import InteractiveShell\n", + "InteractiveShell.ast_node_interactivity = \"all\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "gohAI0ditNiT", + "outputId": "49b29a80-c7e0-42a4-b407-a02f62d744a5" + }, + "outputs": [], + "source": [ + "%%capture output\n", + "!{sys.executable} -m pip install ccxt yfinance git+https://github.com/donbing/mplfinance.git\n", + "import pandas as pd\n", + "import sys\n", + "import ccxt\n", + "import mplfinance as mpf\n", + "from matplotlib.pyplot import imshow\n", + "import yfinance\n", + "import pathlib\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from src.bitbot import BitBot\n", + "from src.configuration.bitbot_files import use_config_dir\n", + "from src.configuration.bitbot_config import load_config_ini" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load config files" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "curdir = pathlib.Path(os.path.abspath('')).resolve()\n", + "files = use_config_dir(curdir)\n", + "config = load_config_ini(files)\n", + "config.set('display', 'output', 'disk')\n", + "config.set('display', 'resolution', '400,300')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bitbot display output " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "app = BitBot(config, files)\n", + "img = app.display_chart()\n", + "img.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bitbot chart generation" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# fetch configured candle history\n", + "from src.exchanges import crypto_exchanges\n", + "exchange = crypto_exchanges.Exchange(config)\n", + "chart_data = exchange.fetch_history()\n", + "chart_data" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "from src.drawing.mpf_plotted_chart import NewPlottedChart\n", + "from src.display.picker import picker as display_picker\n", + "import io\n", + "from PIL import Image\n", + "\n", + "display = display_picker(config)\n", + "chart = NewPlottedChart(config, display, files, chart_data)\n", + "with io.BytesIO() as file_stream:\n", + " chart.write_to_stream(file_stream)\n", + " chart_image = Image.open(file_stream)\n", + " chart_image.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYEAAAEXCAYAAABLZvh6AAAJN0lEQVR4nO3dS3bbOhYFUKKWJlkjdD8TRDXeUy3b4UcUSQHE2buVyJTMIDLPvQAol1rrVEqp04paa/n92O/nrB0z9zUA2vtP6xNYtBFMABz3+P6XVyr2pep+T9WvQwDoQ7+dAACX+9EJvDLPf4QOAKAvOgGAYI9pWq7Mz6rct3YfAdCGTgAg2GOaliv1uQ7g+die9YMjO4kAuI5OACBYqbXT6fpS6qRTALiUTgAgmBAACCYEAII9tg9pqJQ/C1/5mmr9+ui5AAzIwjBAMNNBAMGEAEAwIQAQTAgABBMCAMGEAEAwIQAQTAgABBMCAMGEAEAwIQAQTAgABBMCAMGEAEAwIQAQTAgABBMCAMGEAEAwIQAQTAgABBMCAMGEAEAwIQDEKqXU1ufQmhAAWJAQEkIAIJgQAAgmBACCCQGAYEIAIJgQAHjXALuHhADAVW4QEkIAIJgQAPillFKfN4rN3jBWSv1/lX+Dan+NEAAIJgQAggkBgF59YKpJCABxNuf8gwgBgF9qraXWWp5/3v0CN1o4FgLAsC6r8mst0zMc3gmJjggBgGBCABjOWXP+b00F3YwQAOIcnvPvxBnTXUIAoJEediYJASDWnbuAswgBgGBCAOBdjTqJM292EwIAwYQAQDAhAPBhPf2+AiEAEEwIAAR7zD34bE/m9tBurUR/f87a6wDQnk4AINiPTmDPftMj1b0OAaAPOgGAYI9p+rsyf6Uj+H3MK1W9DgDgBc9rZCl17q7k79fqo9dTnQBAsNU1gbnKfSl1juwoAqANnQBArzames6YWn/MvdA7Vf3ayex5fYDb++Cc/lE6gTszzQYcNHvH8J6qfs3Ssa2TD4B/6AQAggkBoEtD7Cq8wayHEAAINrsm0I1S/ix85Wuq9euj59JADzsHgLGVWjvtuBa2ViXZDAFjxMCOFEHvfKxNCz0UeqaDgNsaYt2gMSEAEEwIAAQTAg1pZYHWhABwO6WU+iyiFFPHCIFOfX+Dz77Jn4+VUn2GEPxUay3PXTetd9/0TggAwxIA24QA0J3NTpjTCAGgiSMXd9M95xECAMGEwMi00dC1HroYIQB83Flz/j1cRO9OCIzKFlJu7Pt8vwv9tYTAXT1/MGotPkkUeJcQAAgmBACCCQGAYEIAIJgQALpkV9BnCAHgLT7TZwxCACCYELgz7TJwkBBoxEflAj0QAp1y2zzwCUIAuIQO9x6EAEAwIQCczprXfQiBjlkLYFTWvPohBACCCQGAYEKAbplLhusJgWQushBPCKTyO4iHp5PiFUJgVH4H8fBaXuRtAR2HEIBUF168bQG9DyEAA9qs1DuYDhQOfRACAMGEAPdlLhoOEwJ06Q7TGTACIQAQTAgABBMCI7P7omtH9tfbp89ZhAAMyD59XiUEgEsIn3sQAsA800wRhAD06uBFWCXOK4QAQDAhABBMCAB/c0d2DCEAify+Cf4lBKABn41ELx5zDz7flGu7C36/ceeOfeV1AGhHJwD8bWO6yB3J4/jRCbzyGSRL1f2eql+HABtqLVMpdWm+vtZaSin10M+Qnz8mnQDwJkXcGB7T9HdlftWnEuoAAPqiEwAItromcFbl7vPOAfqkEwAI9pimfTt9ltYN1rqFIzuJALiOTgB61bpIav39+YjZO4ZfqdCPHDNMB7Cyj5sO+P+BTToBgGBCAG5qmI6apqJDwNZVIN3smkA3Svmz8JWvqdavj54LwIBKrZ0Wwx9Y1Dv8AVxb57jy9T1bbC/xuwvqbGphc3xeOf+L30Ob75+e///hX9HTQQDphABAMCGwwsLxhuDx8eshGYUQYJEQhPEJAa4jRKB7QoAxvTAdo9MBIcCCT8x5H7oIb/widOA1Y4eASo8jvH8IMHYIHGD3B4dsdCrPm8NqrcWNYrQkBK4iJIAbEALvuvuctEoVmIQAQDQhABBMCAAEEwJEsvsL/iEEaGLzIhzAgjs9EAJ06fDuJLuf4CVCgG5dfXF28QchcMzgFxEXSRifEEh25UXewircQmwIWJg8qIc7pnUqcNi4IXCwErVwCCQYNwRAeMMmIQAQTAjwPpX2OuPDDQgBgGBCYIUF4QPcsQu3IARoxsUf2hMCAMGEwFVMh9yb/z9CCAGAYEIAlqjwCSAE4E2mgRiBEGjIRQRoTQgABBs3BOzuANg0bggAsEkIcB0dFnRv7BBwEQJYNXYItCaEgM4JAYBgQgAgmBAACCYEAIIJAYBgQgAgmBAACCYEAIIJAYBgQgAgmBAACPaYpmkqpdS5L859zv7SsXPPeR7r8/oB+qQTAAj2mKblSn2tkj9S3esQAPqgEwAI9vj+l9/z/WuV+p5jfz9HBwDQB50AQLAfncDvCn2ucn9n/WBrRxEAbegEAIKddp/A2jz/Kx0GAJ+nEwAItnqfwJwzjtUBAPRBJwAQTAgABHtsH9JQKX8WvvI11fr10XMBGFCptdMt/KXU6eK1g1JKtT4BJDMdBBBMCAAEEwIAwYQAQDAhABBMCAAEEwIAwYQAQLDoEHCjGJAuOgQA0gkBgGBCACCYEAAIJgQAggkBgGBCACCYEAAIJgQAggkBgGBCACCYEAAIJgQAggkBgGBCACCYEAAIJgQAggkBgGBCACCYEAAIJgQAggkBgGBCACCYEAAIJgQAggkBgGBCACCYEAAINnYIlPLf1qdwa8bvGON3jPE77oUxHDsEpsmb6Bjjd4zxO8b4HRcfAgCsEAIAwYQAQDAhABBMCAAEK7XW1ucwr5ROTwxgHP2GAACXMx0EEEwIAAQTAgDBHq1P4GplYYG51lqOvN4rz9/zvc8+z7Ns/Xv3jMdZ5/K0ZxzXnnOlHsZvadx6G6s5PY7f0x3ef6/QCQAEG74TeHqncl97zivH7qleeqi01/w+j62KZ+2Y7/+WpdddO2br8aXHWmo5flvfs7exmtPD+G2d09L36J1OACBYTCewVrkvVQN7kn7t2LM7jBaWKqW5amurcp9zdsfT2zj2PH69jdWcHsZvhJ/jOToBgGAxncCeNYGzq9E9r7enw2jhqvM463V7X1vpafx6H6s5LcZvz3jccUx1AgDBYjqBNXvWAp6OzA/OPefOOzd+27OD48jr3el+iz3OHr8lI4zVnKvGb9SfY50AQDCfIgoQTCcAEEwIAAQTAgDB/gc3DE7m4bKCbwAAAABJRU5ErkJggg==", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# plot chart and show image\n", + "from src.drawing.market_chart import MarketChart\n", + "from src.display.picker import picker as display_picker\n", + "from PIL import Image\n", + "import io\n", + "\n", + "display = display_picker(config)\n", + "chart = MarketChart(config, display, files)\n", + "\n", + "with io.BytesIO() as file_stream:\n", + " chart.create_plot(chart_data).write_to_stream(file_stream)\n", + " chart_image = Image.open(file_stream)\n", + " chart_image.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MPF plot" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Fetch Data" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "eN618pI5tNiT" + }, + "outputs": [], + "source": [ + "# load markets for selected exchange\n", + "exchange = getattr(ccxt, 'bitmex')()\n", + "mkts = exchange.loadMarkets()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 175 + }, + "id": "9u0BXPm_tNiU", + "outputId": "5aa5560e-5886-4bae-926e-e75310b6a341" + }, + "outputs": [], + "source": [ + "# fetch candles\n", + "exchange_data = exchange.fetchOHLCV('BTC/USD', '5m', limit=40)\n", + "# convert candles to dataframe\n", + "df = pd.DataFrame(exchange_data)\n", + "df.columns = [\"Date\", \"Open\", \"Low\", \"High\", \"Close\", \"Volume\"]\n", + "df['Date'] = pd.to_datetime(df['Date'].astype('datetime64[ms]'))\n", + "#df.index = pd.DatetimeIndex(df[\"Date\"].astype('datetime64[ms]'))\n", + "df.set_index('Date', inplace=True)\n", + "# df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Try to draw an attractive graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### πŸ›³οΈ imports" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib.ticker import EngFormatter\n", + "matplotlib.use('Agg')\n", + "import numpy \n", + "display_size = (400,300)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### ⏲️ select datetime format" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "candle_time_delta = df.index.values[1] - df.index.values[0]\n", + "if(candle_time_delta <= numpy.timedelta64(1,'h')):\n", + " format = '%H:%M'\n", + "elif(candle_time_delta <= numpy.timedelta64(1,'D')): \n", + " format = '%b.%d'\n", + "else:\n", + " format = '%b'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### πŸ“ mpf style" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "s = mpf.make_mpf_style(\n", + " marketcolors=mpf.make_marketcolors(\n", + " alpha=1.0,\n", + " up='black', down='red',\n", + " edge={'up': 'black', 'down': 'red'}, # 'none',\n", + " wick={'up': 'black', 'down': 'red'},\n", + " volume={'up': 'black', 'down': 'red'}\n", + " ),\n", + " base_mpl_style=[files.base_style, files.default_style],\n", + " mavcolors=['#1f77b4', '#ff7f0e', '#2ca02c'],\n", + " rc={}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### πŸ“ˆ create plot figure" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = mpf.plot(\n", + " df,\n", + " scale_width_adjustment=dict(volume=0.9, candle=0.7, lines=0.05),\n", + " update_width_config=dict(candle_linewidth=0.6),\n", + " returnfig=True,\n", + " type='candle',\n", + " # mav=(10, 20),\n", + " volume=False,\n", + " style=s,\n", + " tight_layout=True,\n", + " figsize=tuple(dim/100 for dim in display_size),\n", + " xrotation=0,\n", + " datetime_format=format,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### πŸͺ“ customise axes" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "for a in ax: \n", + " a.yaxis.set_major_formatter(EngFormatter(sep=''))\n", + " a.autoscale(enable=True, axis=\"x\", tight=True)\n", + " a.autoscale(enable=True, axis=\"y\", tight=True)\n", + " a.margins(0.1, 0.2)\n", + " _ = a.set_ylabel(\"\")\n", + " _ = a.set_xlabel(\"\")\n", + "\n", + " # for label in a.yaxis.get_ticklabels():\n", + " # label.set_horizontalalignment('left')\n", + " \n", + " # for label in a.xaxis.get_ticklabels():\n", + " # label.set_verticalalignment('bottom')\n", + " #a.tick_params(rotation = 45, ha='left')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Output" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with io.BytesIO() as file_stream:\n", + " fig.savefig(file_stream, dpi=fig.dpi, bbox_inches='tight', pad_inches=0.0)\n", + " chart_image = Image.open(file_stream)\n", + " chart_image.show()" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "Copy of external_axes.ipynb", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/run.py b/run.py index f9301802..f2008ae6 100644 --- a/run.py +++ b/run.py @@ -22,10 +22,12 @@ config = load_config_ini(config_files) # πŸ“ˆ create bitbot chart updater app = BitBot(config, config_files) +# 🎁 oobex +display = picker(config) +config.on_first_run(lambda: IntroPlayer(display, config)) + # πŸ‘‰ button handlers buttons = Buttons(config) -# 🎁 oobex -config.on_first_run(lambda: IntroPlayer(picker(config), config)) @info_log diff --git a/src/bitbot.py b/src/bitbot.py index 7b7da527..5e4f9ed8 100644 --- a/src/bitbot.py +++ b/src/bitbot.py @@ -40,7 +40,8 @@ def display_chart(self): # πŸ“‘ await internet connection wait_for_internet_connection(self.display.draw_connection_error) # πŸ“ˆ fetch chart data - chart_data = self.market_exchange().fetch_history() + market_exchange = self.market_exchange() + chart_data = market_exchange.fetch_history() if(any(chart_data.candle_data)): # πŸ–ŠοΈ draw the chart on the display with io.BytesIO() as file_stream: @@ -52,11 +53,13 @@ def display_chart(self): overlay.draw_on(chart_image) # πŸ“Ί display the image self.display.show(chart_image) + return chart_image else: img = Image.new('RGBA', self.display.size()) draw = ImageDraw.Draw(img) - draw.text((0, 0), f'{self.config.instrument_name()} was not found on {self.config.exchange_name()}') + draw.text((0, 0), f'{chart_data.instrument} was not found on {market_exchange.name}') self.display.show(img) + return img @info_log diff --git a/src/configuration/bitbot_config.py b/src/configuration/bitbot_config.py index 42b85fa7..e272f2ad 100644 --- a/src/configuration/bitbot_config.py +++ b/src/configuration/bitbot_config.py @@ -2,6 +2,7 @@ import configparser from .log_decorator import info_log from os.path import join as pjoin +from dateutil.parser import parse @info_log def load_config_ini(config_files): @@ -58,15 +59,22 @@ def stock_symbol(self): def portfolio_size(self): try: - return self.config.getfloat('currency', 'holdings', fallback=0) - except ValueError: + return self.config.getfloat('currency', 'holdings') + except: return 0 def entry_price(self): return self.config.getfloat('currency', 'entry_price', fallback=0) def chart_since(self): - return self.config.get('currency', 'chart_since', fallback=None) + date = self.config.get('currency', 'chart_since') + try: + return parse(date) + except: + return None + + def entry_price(self): + return float(self.config.get('currency', 'entry_price', fallback=0)) def set_currency(self, formData): for key in BitBotConfig.currency_keys: @@ -165,6 +173,9 @@ def save(self): with open(self.config_files.config_ini, 'w') as f: self.config.write(f) + def read_dict(self, dict): + self.config.read_dict(dict) + # 🌱 intro setup def on_first_run(self, action): if self.config["first_run"]['enabled'] == "true": diff --git a/src/configuration/bitbot_files.py b/src/configuration/bitbot_files.py index 8a658976..5dd048ee 100644 --- a/src/configuration/bitbot_files.py +++ b/src/configuration/bitbot_files.py @@ -24,8 +24,8 @@ def __init__(self, base_path): self.logging_ini = self.existing_file_path('logging.ini') self.base_style = self.existing_file_path('base.mplstyle') - self.volume_style = self.existing_file_path('volume.mplstyle') self.default_style = self.existing_file_path('default.mplstyle') + self.volume_style = self.existing_file_path('volume.mplstyle') self.small_screen_style = self.existing_file_path('small.mplstyle') self.expanded_style = self.existing_file_path('default.expanded.mplstyle') self.small_expanded_style = self.existing_file_path('small.expanded.mplstyle') diff --git a/src/display/__init__.py b/src/display/__init__.py index b9bba015..e17fe6d5 100644 --- a/src/display/__init__.py +++ b/src/display/__init__.py @@ -64,6 +64,7 @@ def apply_rotation(self, image): else: return image + @info_log # πŸ–ΌοΈ crop and rescale image if needed def resize_image(self, image): if image.size != self.size(): diff --git a/src/display/disk.py b/src/display/disk.py index d196e643..09939398 100644 --- a/src/display/disk.py +++ b/src/display/disk.py @@ -19,6 +19,7 @@ def draw_connection_error(self): def show(self, image): image = self.apply_rotation(image) + image = self.resize_image(image) image = quantise_image(image, white_black_red) self.save_image(self.config.output_file_name(), image) diff --git a/src/drawing/chart_overlay.py b/src/drawing/chart_overlay.py index 5631b53d..77a6028b 100644 --- a/src/drawing/chart_overlay.py +++ b/src/drawing/chart_overlay.py @@ -46,7 +46,7 @@ def display_elements(self): def overlay1(self, chartdata): portfolio_value = self.value_held(chartdata) portfolio_entry_value = self.entry_value() - portfolio_delta = self.profit(chartdata) + portfolio_pnl = self.profit(chartdata) yield TextBlock([ [ # 🎹 draw instrument name and candle width text @@ -55,14 +55,14 @@ def overlay1(self, chartdata): DrawText.percentage(chartdata.percentage_change(), self.title_font), ], # 🐘 large font price text - [DrawText.humanised_price(chartdata.last_close(), self.price_font)], + [DrawText.number_5sf(chartdata.last_close(), self.price_font)], # πŸ’¬ draw holdings or comment [ - DrawText.number(portfolio_value, self.title_font) + DrawText.number(portfolio_pnl, self.title_font) if portfolio_value else DrawText.random_from_bool(self.ai_comments(), self.price_increasing(chartdata), self.title_font), - DrawText.humanised_price(portfolio_delta, self.title_font, prefix=" up " if portfolio_delta > 0 else " down ") + DrawText.pip_calc(self.entry_price(), chartdata.last_close(), self.title_font, prefix=" ") if portfolio_entry_value != 0 else DrawText.empty(self.title_font) ] @@ -81,14 +81,14 @@ def overlay2(self, chartdata): # βž— draw coloured change percentage [DrawText.percentage(chartdata.percentage_change(), self.title_font)], # 🐘 large font price text - [DrawText.humanised_price(chartdata.last_close(), self.price_font)], + [DrawText.number_5sf(chartdata.last_close(), self.price_font)], # πŸ’¬ draw holdings or comment [ DrawText.humanised_price(portfolio_value, self.title_font) if portfolio_value else DrawText.random_from_bool(self.ai_comments(), self.price_increasing(chartdata), self.title_font), - DrawText.humanised_price(portfolio_delta, self.title_font, prefix=" up " if portfolio_delta > 0 else " down ") + DrawText.humanised_price(portfolio_delta, self.title_font) if portfolio_entry_value != 0 else DrawText.empty(self.title_font) ] @@ -98,7 +98,7 @@ def price_increasing(self, chartdata): return chartdata.start_price() < chartdata.last_close() def format_time(self): - return datetime.now().strftime("%b %-d %-H:%M") + return datetime.now().strftime("%-H:%M%b%-d") def ai_comments(self): return self.config.get_price_action_comments() diff --git a/src/drawing/image_utils.py b/src/drawing/image_utils.py index 9bee0500..406f149b 100644 --- a/src/drawing/image_utils.py +++ b/src/drawing/image_utils.py @@ -24,11 +24,26 @@ def percentage(percentage, font): def humanised_price(price, font, prefix=""): return DrawText(prefix + format_title_price(price), font) + @staticmethod + def pip_calc(open, close, font, prefix=""): + if str(open).index('.') >= 3: # JPY pair + multiplier = 0.01 + else: + multiplier = 0.0001 + + pips = round((close - open) / multiplier) + return DrawText(prefix + '({:+})'.format(int(pips)), font) + # 🏷️ number text @staticmethod def number(value, font, colour='black'): return DrawText("{:+.2f}".format(value), font, colour) + # 🏷️ number text + @staticmethod + def number_5sf(value, font): + return DrawText("{:.5g}".format(value), font, 'black') + # 🎲 randomly selected up/down comment @staticmethod def random_from_bool(options, up_or_down, font): diff --git a/src/drawing/legacy_mpf_plotted_chart.py b/src/drawing/legacy_mpf_plotted_chart.py new file mode 100644 index 00000000..abaecbc0 --- /dev/null +++ b/src/drawing/legacy_mpf_plotted_chart.py @@ -0,0 +1,97 @@ +import matplotlib +import tzlocal +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from mplfinance.original_flavor import candlestick_ohlc, volume_overlay +from src.drawing import price_humaniser + +matplotlib.use('Agg') +local_tz = tzlocal.get_localzone() + +price_formatter = matplotlib.ticker.FuncFormatter( + price_humaniser.format_scale_price +) + + +class PlottedChart: + layouts = { + '3mo': (20, mdates.YearLocator(), mdates.YearLocator(1), mdates.DateFormatter('%Y'), local_tz), + '1mo': (0.01, mdates.MonthLocator(), mdates.YearLocator(1), mdates.DateFormatter('%Y'), local_tz), + '1d': (0.01, mdates.DayLocator(bymonthday=range(1, 31, 7)), mdates.MonthLocator(), mdates.DateFormatter('%b'), local_tz), + '1h': (0.005, mdates.HourLocator(byhour=range(0, 23, 4)), mdates.DayLocator(), mdates.DateFormatter('%a %d %b', local_tz)), + "5m": (0.0005, mdates.MinuteLocator(byminute=[0, 30]), mdates.HourLocator(interval=1), mdates.DateFormatter('%-I.%p', local_tz)), + } + + def __init__(self, config, display, files, chart_data): + self.candle_width = chart_data.candle_width + # πŸ–¨οΈ create MPL plot + self.fig, ax = self.create_chart_figure(config, display, files) + # πŸ“ find suiteable layout for timeframe + layout = self.layouts[self.candle_width] + # βž– locate/format x axis ticks for chosen layout + ax[0].xaxis.set_minor_locator(layout[1]) + ax[0].xaxis.set_minor_formatter(plt.NullFormatter()) + ax[0].xaxis.set_major_locator(layout[2]) + ax[0].xaxis.set_major_formatter(layout[3]) + # πŸ’²currency amount uses custom formatting + ax[0].yaxis.set_major_formatter(price_formatter) + + self.plot_chart(config, layout, ax, chart_data.candle_data) + + def plot_chart(self, config, layout, ax, candle_data): + # βœ’οΈ draw candles to MPL plot + candlestick_ohlc(ax[0], candle_data, colorup='green', colordown='red', width=layout[0]) + # βœ’οΈ draw volumes to MPL plot + if config.show_volume(): + ax[1].yaxis.set_major_formatter(price_formatter) + _, opens, _, _, closes, volumes = list(zip(*candle_data)) + volume_overlay(ax[1], opens, closes, volumes, colorup='white', colordown='red', width=1) + self.fig.subplots_adjust(bottom=0.01) + + # πŸ“‘ styles overide each other left to right? + def get_default_styles(self, config, display, files): + small_display = self.is_small_display(display) + + if small_display: + yield files.small_screen_style + yield files.default_style + + if config.expand_chart(): + yield files.expanded_style + if small_display: + yield files.small_expanded_style + + def is_small_display(self, display): + small_display = display.size()[0] < 300 + return small_display + + def create_chart_figure(self, config, display, files): + # πŸ“ apply global base style + plt.style.use(files.base_style) + num_plots = 2 if config.show_volume() else 1 + heights = [4, 1] if config.show_volume() else [1] + plt.tight_layout() + # πŸ“ select mpl style + stlyes = list(self.get_default_styles(config, display, files)) + # πŸ“ scope styles to just this plot + with plt.style.context(stlyes): + display_width, display_height = display.size() + fig = plt.figure(figsize=(display_width / 100, display_height / 100)) + gs = fig.add_gridspec(num_plots, hspace=0, height_ratios=heights) + ax1 = fig.add_subplot(gs[0], zorder=1) + ax2 = None + + # πŸ“ align price tick labels for expanded chart + if(config.expand_chart()): + ax1.set_yticklabels(ax1.get_yticklabels(), ha='left') + + if config.show_volume(): + with plt.style.context(files.volume_style): + ax2 = fig.add_subplot(gs[1], zorder=0) + + return (fig, (ax1, ax2)) + + def write_to_stream(self, stream): + self.fig.savefig(stream, dpi=self.fig.dpi, pad_inches=0) + stream.seek(0) + plt.close(self.fig) diff --git a/src/drawing/market_chart.py b/src/drawing/market_chart.py index 6d127b43..2e4e4d0e 100644 --- a/src/drawing/market_chart.py +++ b/src/drawing/market_chart.py @@ -1,18 +1,7 @@ import matplotlib import tzlocal -import matplotlib.pyplot as plt -import matplotlib.dates as mdates import matplotlib.font_manager as font_manager -from mplfinance.original_flavor import candlestick_ohlc, volume_overlay -from src.drawing import price_humaniser - -matplotlib.use('Agg') -local_tz = tzlocal.get_localzone() - -price_formatter = matplotlib.ticker.FuncFormatter( - price_humaniser.format_scale_price -) - +from src.drawing.mpf_plotted_chart import NewPlottedChart # ☝️ single instance for lifetime of app class MarketChart: @@ -25,115 +14,4 @@ def __init__(self, config, display, files): font_manager.fontManager.addfont(font_file) def create_plot(self, chart_data): - return PlottedChart(self.config, self.display, self.files, chart_data) - - -class PlottedChart: - layouts = { - '3mo': (20, - mdates.YearLocator(), plt.NullFormatter(), - mdates.YearLocator(1), mdates.DateFormatter('%Y'), - local_tz), - '1mo': (0.01, - mdates.MonthLocator(), plt.NullFormatter(), - mdates.YearLocator(1), mdates.DateFormatter('%Y'), - local_tz), - '1d': (0.01, - mdates.DayLocator(bymonthday=range(1, 31, 7)), plt.NullFormatter(), - mdates.MonthLocator(), mdates.DateFormatter('%b'), - local_tz), - '1h': (0.005, - mdates.HourLocator(byhour=range(0, 23, 4)), mdates.DateFormatter('%-I.%p'), - mdates.DayLocator(), mdates.DateFormatter('%b%d'), - local_tz), - "5m": (0.0005, - mdates.MinuteLocator(byminute=[0, 30]), plt.NullFormatter(), - mdates.HourLocator(interval=1), mdates.DateFormatter('%-I.%p'), - local_tz), - } - - def __init__(self, config, display, files, chart_data): - self.candle_width = chart_data.candle_width - # πŸ–¨οΈ create MPL plot - self.fig, ax = self.create_chart_figure(config, display, files) - # πŸ“ find suiteable layout for timeframe - layout = self.layouts[self.candle_width] - # βž– locate/format x axis ticks for chosen layout - ax[0].xaxis.set_minor_locator(layout[1]) - ax[0].xaxis.set_minor_formatter(layout[2]) - ax[0].xaxis.set_major_locator(layout[3]) - ax[0].xaxis.set_major_formatter(layout[4]) - # πŸ’²currency amount uses custom formatting - ax[0].yaxis.set_major_formatter(price_formatter) - - self.plot_chart(config, layout[0], ax, chart_data.candle_data) - - def plot_chart(self, config, candle_width, ax, candle_data): - # βœ’οΈ draw candles to MPL plot - candlestick_ohlc( - ax[0], - candle_data, - colorup='green', - colordown='red', - width=candle_width) - # βœ’οΈ draw volumes to MPL plot - if config.show_volume(): - ax[1].yaxis.set_major_formatter(price_formatter) - _, opens, _, _, closes, volumes = list(zip(*candle_data)) - volume_overlay( - ax[1], - opens, - closes, - volumes, - colorup='white', - colordown='red', - width=1) - self.fig.subplots_adjust(bottom=0.01) - - # πŸ“‘ styles overide each other left to right? - def get_default_styles(self, config, display, files): - small_display = self.is_small_display(display) - - if small_display: - yield files.small_screen_style - yield files.default_style - - if config.expand_chart(): - yield files.expanded_style - if small_display: - yield files.small_expanded_style - - def is_small_display(self, display): - small_display = display.size()[0] < 300 - return small_display - - def create_chart_figure(self, config, display, files): - # πŸ“ apply global base style - plt.style.use(files.base_style) - num_plots = 2 if config.show_volume() else 1 - heights = [4, 1] if config.show_volume() else [1] - plt.tight_layout() - # πŸ“ select mpl style - stlyes = list(self.get_default_styles(config, display, files)) - # πŸ“ scope styles to just this plot - with plt.style.context(stlyes): - display_width, display_height = display.size() - fig = plt.figure(figsize=(display_width / 100, display_height / 100)) - gs = fig.add_gridspec(num_plots, hspace=0, height_ratios=heights) - ax1 = fig.add_subplot(gs[0], zorder=1) - ax2 = None - - # πŸ“ align price tick labels for expanded chart - if(config.expand_chart()): - ax1.set_yticklabels(ax1.get_yticklabels(), ha='left') - - if config.show_volume(): - with plt.style.context(files.volume_style): - ax2 = fig.add_subplot(gs[1], zorder=0) - - return (fig, (ax1, ax2)) - - def write_to_stream(self, stream): - self.fig.savefig(stream, dpi=self.fig.dpi, pad_inches=0) - stream.seek(0) - plt.close(self.fig) + return NewPlottedChart(self.config, self.display, self.files, chart_data) diff --git a/src/drawing/mpf_plotted_chart.py b/src/drawing/mpf_plotted_chart.py new file mode 100644 index 00000000..4d43e54f --- /dev/null +++ b/src/drawing/mpf_plotted_chart.py @@ -0,0 +1,143 @@ +import matplotlib.pyplot as plt +import numpy +import mplfinance as mpf +import pandas as pd +from matplotlib.ticker import EngFormatter + + +class NewPlottedChart: + def __init__(self, config, display, files, chart_data): + self.candle_width = chart_data.candle_width + + # πŸ–ΌοΈ prep chart data frame + data_frame = pd.DataFrame(chart_data.candle_data) + data_frame = data_frame.drop([6, 7], axis=1, errors='ignore') + data_frame.columns = ['date', 'open', 'high', 'low', 'close', 'volume'] + data_frame.index = pd.DatetimeIndex(data_frame['date']) + + # 🎨 chart colours + mpf_colours = mpf.make_marketcolors( + alpha=1.0, + up='black', down='red', + edge={'up': 'black', 'down': 'red'}, # 'none', + wick={'up': 'black', 'down': 'red'}, + volume={'up': 'black', 'down': 'red'}) + + # πŸ“ create styles list + style_files = list(self.get_default_styles(config, display, files)) + + # πŸ“ setup MLF styling + mpf_style = mpf.make_mpf_style( + marketcolors=mpf_colours, + base_mpl_style=style_files, + mavcolors=['#1f77b4', '#ff7f0e', '#2ca02c']) + + # πŸ“ˆ settings for chart plot + kwargs = dict( + volume=config.show_volume(), + style=mpf_style, + tight_layout=True, + figsize=tuple(dim/100 for dim in display.size()), + xrotation=0, + datetime_format=self.date_format(data_frame), + ) + + # πŸšͺ add a line indicating entry price, if configured + entry = config.entry_price() + if entry != 0: + kwargs['hlines'] = dict(hlines=[entry], colors=['g'], linestyle='-.') + + # πŸ“ˆ create the chart plot + self.fig, ax = mpf.plot( + data_frame, + scale_width_adjustment=dict(volume=0.9, candle=0.7, lines=0.05), + update_width_config=dict(candle_linewidth=0.6), + returnfig=True, + type='candle', + # mav=(10, 20), + **kwargs + ) + + plt.subplots_adjust(left=0.0, bottom=0.0, right=1, top=1, wspace=0.1, hspace=0.0) + plt.margins(x=0) + + # πŸͺ“ make axes look nicer + for a in ax: + # a.set_adjustable('box') + a.yaxis.set_major_formatter(EngFormatter(sep='')) + a.autoscale(enable=True, axis="both", tight=True) + # margin between candles and axes + a.margins(0.05, 0.2) + a.xaxis.labelpad = 0 + # a.tick_params(pad=0, axis='both') + a.locator_params(axis='both', tight=True) + # remove labels + _ = a.set_ylabel("") + _ = a.set_xlabel("") + a.autoscale_view(True) + # a.reset_position() + + # _ = a.set_frame_on(False) + # βœ”οΈ align tick labels inside edges + if config.expand_chart(): + for ylabel in a.yaxis.get_ticklabels(): + ylabel.set_horizontalalignment('left') + for xlabel in a.xaxis.get_ticklabels(): + xlabel.set_verticalalignment('bottom') + + if config.expand_chart(): + if(len(ax) == 2): + ax[0].set_position((0, 0, 1, 1)) + ax[1].set_position((0, 0, 1, 1)) + if(len(ax) == 4): + ax[3].set_position((0, 0, 1, 0.3)) + ax[2].set_position((0, 0, 1, 0.3)) + ax[0].set_position((0, 0.3, 1, 0.7)) + ax[1].set_position((0, 0.3, 1, 0.7)) + + # self.fig.set_tight_layout(True) + # self.fig.set_constrained_layout_pads(w_pad=0, h_pad=0) + + # πŸ“‘ styles overid left to right + def get_default_styles(self, config, display, files): + yield files.base_style + yield files.default_style + + small_display = self.is_small_display(display) + if small_display: + yield files.small_screen_style + + if config.expand_chart(): + yield files.expanded_style + if small_display: + yield files.small_expanded_style + + def is_small_display(self, display): + small_display = display.size()[0] < 300 + return small_display + + # πŸ• format for date axis labels + def date_format(self, df): + candle_time_delta = df.index.values[1] - df.index.values[0] + if(candle_time_delta <= numpy.timedelta64(1, 'h')): + return '%H:%M' + elif(candle_time_delta <= numpy.timedelta64(1, 'D')): + return '%b%d' + elif(candle_time_delta <= numpy.timedelta64(4, 'W')): + return '%y/%b' + elif(candle_time_delta <= numpy.timedelta64(16, 'W')): + return '%y/%b' + else: + return '%b' + + # πŸ›Ά save plot to image stream + def write_to_stream(self, stream): + self.fig.savefig( + stream, + dpi=self.fig.dpi, + # bbox_inches='tight', + pad_inches=0.0, + transparent=True, + ) + stream.seek(0) + plt.close(self.fig) diff --git a/src/exchanges/crypto_exchanges.py b/src/exchanges/crypto_exchanges.py index 16f8e8a9..362bbb72 100644 --- a/src/exchanges/crypto_exchanges.py +++ b/src/exchanges/crypto_exchanges.py @@ -2,21 +2,22 @@ from datetime import datetime import random import collections -import matplotlib.dates as mdates from src.configuration.log_decorator import info_log from ccxt.base.errors import BadSymbol import logging + class Exchange(): CandleConfig = collections.namedtuple('CandleConfig', 'width count') candle_configs = [ - CandleConfig("5m", 60), - CandleConfig("1h", 24), - CandleConfig("1d", 60), + CandleConfig("5m", 40), + CandleConfig("1h", 40), + CandleConfig("1d", 40), ] def __init__(self, config): self.config = config + self.name = self.config.exchange_name() def fetch_history(self): configred_candle_width = self.config.candle_width() @@ -62,7 +63,7 @@ def fetch_market_data(exchange, instrument, candle_freq, num_candles, since): instrument, candle_freq, limit=num_candles, - since=since and exchange.parse8601(since)) + since=since and exchange.parse8601(since.strftime('%Y-%m-%dT%H:%M:%S.%f%z'))) except BadSymbol: logging.warning(f'"{instrument}" is not available') return [] @@ -81,8 +82,7 @@ def load_exchange(exchange_name): def make_matplotfriendly_date(element): datetime_field = element[0]/1000 datetime_utc = datetime.utcfromtimestamp(datetime_field) - datetime_num = mdates.date2num(datetime_utc) - return replace_at_index(element, 0, datetime_num) + return replace_at_index(element, 0, datetime_utc) def replace_at_index(tup, ix, val): diff --git a/src/exchanges/stock_exchanges.py b/src/exchanges/stock_exchanges.py index 1f74a88b..c83ba499 100644 --- a/src/exchanges/stock_exchanges.py +++ b/src/exchanges/stock_exchanges.py @@ -2,31 +2,40 @@ import collections import random from datetime import datetime, timedelta -import matplotlib.dates as mdates from src.configuration.log_decorator import info_log import math +CandleConfig = collections.namedtuple('CandleConfig', 'width duration fat_duration') + +candle_configs = [ + CandleConfig('1mo', timedelta(weeks=4*24), None), + CandleConfig('1wk', timedelta(weeks=60), None), + CandleConfig('3mo', timedelta(weeks=12*24), None), + CandleConfig('1m', timedelta(minutes=60), timedelta(days=4)), + CandleConfig('2m', timedelta(minutes=60), timedelta(days=4)), + CandleConfig('5m', timedelta(minutes=60), timedelta(days=4)), + CandleConfig('15m', timedelta(minutes=15*60), timedelta(days=4)), + CandleConfig('30m', timedelta(minutes=30*40), timedelta(days=4)), + CandleConfig('1h', timedelta(hours=40), timedelta(days=3)), + CandleConfig('1d', timedelta(days=40), timedelta(days=3)), +] + + class Exchange(): - CandleConfig = collections.namedtuple('CandleConfig', 'width duration') - candle_configs = [ - CandleConfig('1mo', timedelta(weeks=4*24)), - CandleConfig('1h', timedelta(hours=40)), - CandleConfig('1wk', timedelta(weeks=60)), - CandleConfig('3mo', timedelta(weeks=12*24)) - ] def __init__(self, config): self.config = config + self.name = 'yahoo finance' def fetch_history(self): instrument = self.config.stock_symbol() ticker = yfinance.Ticker(instrument) candle_config = self.select_candle_config() candle_width = candle_config.width - chart_duration = candle_config.duration + chart_duration = candle_config.fat_duration or candle_config.duration - end_date = datetime.utcnow() + end_date = self.config.chart_since() or datetime.utcnow() start_date = end_date - chart_duration history = self.get_stock_history( @@ -35,14 +44,14 @@ def fetch_history(self): start_date, end_date) - return CandleData(candle_width, history, ticker) + return CandleData(candle_width, history.tail(40), ticker) @info_log def get_stock_history(self, ticker, candle_width, start_date, end_date): return ticker.history( interval=candle_width, - start=start_date.strftime("%Y-%m-%d"), - end=end_date.strftime("%Y-%m-%d")) + start=start_date, + end=end_date) def select_candle_config(self): candle_width = self.config.candle_width() @@ -53,15 +62,16 @@ def select_candle_config(self): return candle_config def get_candle_config_matching(self, configred_candle_width): - candle_config, = ( - conf for conf in self.candle_configs - if conf.width == configred_candle_width - ) - return candle_config + if configred_candle_width not in candle_configs: + candle_config, = ( + conf for conf in candle_configs + if conf.width == configred_candle_width + ) + return candle_config def get_random_candle_config(self): - randomised_index = random.randrange(len(self.candle_configs)) - new_var = self.candle_configs[randomised_index] + randomised_index = random.randrange(len(candle_configs)) + new_var = candle_configs[randomised_index] return new_var def __repr__(self): @@ -69,9 +79,8 @@ def __repr__(self): def make_matplotfriendly_date(element): - datetime_field = element[0] - datetime_num = mdates.date2num(datetime_field) - return replace_at_index(element, 0, datetime_num) + datetime = element[0] + return replace_at_index(element, 0, datetime) def replace_at_index(tup, ix, val): @@ -97,7 +106,6 @@ def percentage_change(self): def last_close(self): all_closes = self.select_index_if_number(self.candle_data, 4) - return float(all_closes[-1]) def start_price(self): diff --git a/src/input/buttons.py b/src/input/buttons.py index d33a4dae..eabd26f2 100644 --- a/src/input/buttons.py +++ b/src/input/buttons.py @@ -3,6 +3,9 @@ class Buttons(): def __init__(self, config): + if config.output_device_name().startswith('waveshare'): + return + self.config = config # πŸ‘† map button actions self.BUTTONS = { diff --git a/src/resources/5x5_pixel.ttf b/src/resources/5x5_pixel.ttf new file mode 100644 index 00000000..5a1e33f9 Binary files /dev/null and b/src/resources/5x5_pixel.ttf differ diff --git a/src/resources/basis33.ttf b/src/resources/basis33.ttf new file mode 100644 index 00000000..b20255b2 Binary files /dev/null and b/src/resources/basis33.ttf differ diff --git a/src/resources/cg-pixel-3x5.ttf b/src/resources/cg-pixel-3x5.ttf new file mode 100644 index 00000000..96439fe7 Binary files /dev/null and b/src/resources/cg-pixel-3x5.ttf differ diff --git a/src/resources/cg-pixel-4x5.ttf b/src/resources/cg-pixel-4x5.ttf new file mode 100644 index 00000000..07b090ba Binary files /dev/null and b/src/resources/cg-pixel-4x5.ttf differ diff --git a/tests/images/APPLE 1mo defaults.png b/tests/images/APPLE 1mo defaults.png deleted file mode 100644 index 4253e8a7..00000000 Binary files a/tests/images/APPLE 1mo defaults.png and /dev/null differ diff --git a/tests/images/APPLE 3mo defaults.png b/tests/images/APPLE 3mo defaults.png deleted file mode 100644 index b1b65620..00000000 Binary files a/tests/images/APPLE 3mo defaults.png and /dev/null differ diff --git a/tests/images/BTC EXPANDED.png b/tests/images/BTC EXPANDED.png deleted file mode 100644 index 926745bb..00000000 Binary files a/tests/images/BTC EXPANDED.png and /dev/null differ diff --git a/tests/images/BTC HOLDINGS.png b/tests/images/BTC HOLDINGS.png deleted file mode 100644 index 81ce2549..00000000 Binary files a/tests/images/BTC HOLDINGS.png and /dev/null differ diff --git a/tests/images/BTC OVERLAY2.png b/tests/images/BTC OVERLAY2.png deleted file mode 100644 index 6486b3b5..00000000 Binary files a/tests/images/BTC OVERLAY2.png and /dev/null differ diff --git a/tests/images/BTC VOLUME EXPANDED.png b/tests/images/BTC VOLUME EXPANDED.png deleted file mode 100644 index 342c2dcf..00000000 Binary files a/tests/images/BTC VOLUME EXPANDED.png and /dev/null differ diff --git a/tests/images/BTC VOLUME OVERLAY2.png b/tests/images/BTC VOLUME OVERLAY2.png deleted file mode 100644 index 6134442d..00000000 Binary files a/tests/images/BTC VOLUME OVERLAY2.png and /dev/null differ diff --git a/tests/images/BTC VOLUME.png b/tests/images/BTC VOLUME.png deleted file mode 100644 index 26fb4322..00000000 Binary files a/tests/images/BTC VOLUME.png and /dev/null differ diff --git a/tests/images/GBPJPY.png b/tests/images/GBPJPY.png deleted file mode 100644 index 0ae124f2..00000000 Binary files a/tests/images/GBPJPY.png and /dev/null differ diff --git a/tests/images/bitmex BTC 1d defaults.png b/tests/images/bitmex BTC 1d defaults.png deleted file mode 100644 index 584bb7ca..00000000 Binary files a/tests/images/bitmex BTC 1d defaults.png and /dev/null differ diff --git a/tests/images/bitmex BTC 1h defaults.png b/tests/images/bitmex BTC 1h defaults.png deleted file mode 100644 index dcacdb3e..00000000 Binary files a/tests/images/bitmex BTC 1h defaults.png and /dev/null differ diff --git a/tests/images/bitmex BTC 5m defaults.png b/tests/images/bitmex BTC 5m defaults.png deleted file mode 100644 index 6aa99f4a..00000000 Binary files a/tests/images/bitmex BTC 5m defaults.png and /dev/null differ diff --git a/tests/images/bitmex ETH 1d defaults.png b/tests/images/bitmex ETH 1d defaults.png deleted file mode 100644 index 7382d0fc..00000000 Binary files a/tests/images/bitmex ETH 1d defaults.png and /dev/null differ diff --git a/tests/images/bitmex ETH 1h defaults.png b/tests/images/bitmex ETH 1h defaults.png deleted file mode 100644 index 90042f1d..00000000 Binary files a/tests/images/bitmex ETH 1h defaults.png and /dev/null differ diff --git a/tests/images/bitmex ETH 5m defaults.png b/tests/images/bitmex ETH 5m defaults.png deleted file mode 100644 index 82a259db..00000000 Binary files a/tests/images/bitmex ETH 5m defaults.png and /dev/null differ diff --git a/tests/images/cryptocom CRO 1d defaults.png b/tests/images/cryptocom CRO 1d defaults.png deleted file mode 100644 index a99d267c..00000000 Binary files a/tests/images/cryptocom CRO 1d defaults.png and /dev/null differ diff --git a/tests/images/cryptocom CRO 1h defaults.png b/tests/images/cryptocom CRO 1h defaults.png deleted file mode 100644 index f633b539..00000000 Binary files a/tests/images/cryptocom CRO 1h defaults.png and /dev/null differ diff --git a/tests/images/cryptocom CRO 5m defaults.png b/tests/images/cryptocom CRO 5m defaults.png deleted file mode 100644 index 59d89eff..00000000 Binary files a/tests/images/cryptocom CRO 5m defaults.png and /dev/null differ diff --git a/tests/images/test_264,176_APPLE 1mo defaults.png b/tests/images/test_264,176_APPLE 1mo defaults.png new file mode 100644 index 00000000..680028c6 Binary files /dev/null and b/tests/images/test_264,176_APPLE 1mo defaults.png differ diff --git a/tests/images/test_264,176_APPLE 3mo defaults.png b/tests/images/test_264,176_APPLE 3mo defaults.png new file mode 100644 index 00000000..3a087da6 Binary files /dev/null and b/tests/images/test_264,176_APPLE 3mo defaults.png differ diff --git a/tests/images/test_264,176_AUDCAD 3mo defaults with entry.png b/tests/images/test_264,176_AUDCAD 3mo defaults with entry.png new file mode 100644 index 00000000..9f49eafb Binary files /dev/null and b/tests/images/test_264,176_AUDCAD 3mo defaults with entry.png differ diff --git a/tests/images/test_264,176_BTC EXPANDED.png b/tests/images/test_264,176_BTC EXPANDED.png new file mode 100644 index 00000000..d4ae60cc Binary files /dev/null and b/tests/images/test_264,176_BTC EXPANDED.png differ diff --git a/tests/images/test_264,176_BTC HOLDINGS.png b/tests/images/test_264,176_BTC HOLDINGS.png new file mode 100644 index 00000000..e0e9af74 Binary files /dev/null and b/tests/images/test_264,176_BTC HOLDINGS.png differ diff --git a/tests/images/test_264,176_BTC OVERLAY2.png b/tests/images/test_264,176_BTC OVERLAY2.png new file mode 100644 index 00000000..b8d8c22c Binary files /dev/null and b/tests/images/test_264,176_BTC OVERLAY2.png differ diff --git a/tests/images/test_264,176_BTC VOLUME EXPANDED.png b/tests/images/test_264,176_BTC VOLUME EXPANDED.png new file mode 100644 index 00000000..de5e6830 Binary files /dev/null and b/tests/images/test_264,176_BTC VOLUME EXPANDED.png differ diff --git a/tests/images/test_264,176_BTC VOLUME OVERLAY2.png b/tests/images/test_264,176_BTC VOLUME OVERLAY2.png new file mode 100644 index 00000000..7c0c1577 Binary files /dev/null and b/tests/images/test_264,176_BTC VOLUME OVERLAY2.png differ diff --git a/tests/images/test_264,176_BTC VOLUME.png b/tests/images/test_264,176_BTC VOLUME.png new file mode 100644 index 00000000..deff6184 Binary files /dev/null and b/tests/images/test_264,176_BTC VOLUME.png differ diff --git a/tests/images/test_264,176_GBPJPY 3mo defaults with entry.png b/tests/images/test_264,176_GBPJPY 3mo defaults with entry.png new file mode 100644 index 00000000..b6980ef6 Binary files /dev/null and b/tests/images/test_264,176_GBPJPY 3mo defaults with entry.png differ diff --git a/tests/images/test_264,176_bitmex BTC 1d defaults.png b/tests/images/test_264,176_bitmex BTC 1d defaults.png new file mode 100644 index 00000000..3fef1ded Binary files /dev/null and b/tests/images/test_264,176_bitmex BTC 1d defaults.png differ diff --git a/tests/images/test_264,176_bitmex BTC 1h defaults.png b/tests/images/test_264,176_bitmex BTC 1h defaults.png new file mode 100644 index 00000000..cbf341b3 Binary files /dev/null and b/tests/images/test_264,176_bitmex BTC 1h defaults.png differ diff --git a/tests/images/test_264,176_bitmex BTC 5m defaults.png b/tests/images/test_264,176_bitmex BTC 5m defaults.png new file mode 100644 index 00000000..8609850e Binary files /dev/null and b/tests/images/test_264,176_bitmex BTC 5m defaults.png differ diff --git a/tests/images/test_264,176_bitmex ETH 1d defaults.png b/tests/images/test_264,176_bitmex ETH 1d defaults.png new file mode 100644 index 00000000..ecb92115 Binary files /dev/null and b/tests/images/test_264,176_bitmex ETH 1d defaults.png differ diff --git a/tests/images/test_264,176_bitmex ETH 1h defaults.png b/tests/images/test_264,176_bitmex ETH 1h defaults.png new file mode 100644 index 00000000..f3eff0aa Binary files /dev/null and b/tests/images/test_264,176_bitmex ETH 1h defaults.png differ diff --git a/tests/images/test_264,176_bitmex ETH 5m defaults.png b/tests/images/test_264,176_bitmex ETH 5m defaults.png new file mode 100644 index 00000000..01424897 Binary files /dev/null and b/tests/images/test_264,176_bitmex ETH 5m defaults.png differ diff --git a/tests/images/test_264,176_cryptocom CRO 1d defaults.png b/tests/images/test_264,176_cryptocom CRO 1d defaults.png new file mode 100644 index 00000000..659b659c Binary files /dev/null and b/tests/images/test_264,176_cryptocom CRO 1d defaults.png differ diff --git a/tests/images/test_264,176_cryptocom CRO 1h defaults.png b/tests/images/test_264,176_cryptocom CRO 1h defaults.png new file mode 100644 index 00000000..65c8b103 Binary files /dev/null and b/tests/images/test_264,176_cryptocom CRO 1h defaults.png differ diff --git a/tests/images/test_264,176_cryptocom CRO 5m defaults.png b/tests/images/test_264,176_cryptocom CRO 5m defaults.png new file mode 100644 index 00000000..4c240bbc Binary files /dev/null and b/tests/images/test_264,176_cryptocom CRO 5m defaults.png differ diff --git a/tests/images/test_400,300_APPLE 1mo defaults.png b/tests/images/test_400,300_APPLE 1mo defaults.png new file mode 100644 index 00000000..e6b5019f Binary files /dev/null and b/tests/images/test_400,300_APPLE 1mo defaults.png differ diff --git a/tests/images/test_400,300_APPLE 3mo defaults.png b/tests/images/test_400,300_APPLE 3mo defaults.png new file mode 100644 index 00000000..75150c7d Binary files /dev/null and b/tests/images/test_400,300_APPLE 3mo defaults.png differ diff --git a/tests/images/test_400,300_AUDCAD 3mo defaults with entry.png b/tests/images/test_400,300_AUDCAD 3mo defaults with entry.png new file mode 100644 index 00000000..9bac56a8 Binary files /dev/null and b/tests/images/test_400,300_AUDCAD 3mo defaults with entry.png differ diff --git a/tests/images/test_400,300_BTC EXPANDED.png b/tests/images/test_400,300_BTC EXPANDED.png new file mode 100644 index 00000000..7b8bf613 Binary files /dev/null and b/tests/images/test_400,300_BTC EXPANDED.png differ diff --git a/tests/images/test_400,300_BTC HOLDINGS.png b/tests/images/test_400,300_BTC HOLDINGS.png new file mode 100644 index 00000000..8abcd8e8 Binary files /dev/null and b/tests/images/test_400,300_BTC HOLDINGS.png differ diff --git a/tests/images/test_400,300_BTC OVERLAY2.png b/tests/images/test_400,300_BTC OVERLAY2.png new file mode 100644 index 00000000..1bca1eb5 Binary files /dev/null and b/tests/images/test_400,300_BTC OVERLAY2.png differ diff --git a/tests/images/test_400,300_BTC VOLUME EXPANDED.png b/tests/images/test_400,300_BTC VOLUME EXPANDED.png new file mode 100644 index 00000000..87969914 Binary files /dev/null and b/tests/images/test_400,300_BTC VOLUME EXPANDED.png differ diff --git a/tests/images/test_400,300_BTC VOLUME OVERLAY2.png b/tests/images/test_400,300_BTC VOLUME OVERLAY2.png new file mode 100644 index 00000000..e5bcbc37 Binary files /dev/null and b/tests/images/test_400,300_BTC VOLUME OVERLAY2.png differ diff --git a/tests/images/test_400,300_BTC VOLUME.png b/tests/images/test_400,300_BTC VOLUME.png new file mode 100644 index 00000000..3d6fd98b Binary files /dev/null and b/tests/images/test_400,300_BTC VOLUME.png differ diff --git a/tests/images/test_400,300_GBPJPY 3mo defaults with entry.png b/tests/images/test_400,300_GBPJPY 3mo defaults with entry.png new file mode 100644 index 00000000..e690f0ec Binary files /dev/null and b/tests/images/test_400,300_GBPJPY 3mo defaults with entry.png differ diff --git a/tests/images/test_400,300_bitmex BTC 1d defaults.png b/tests/images/test_400,300_bitmex BTC 1d defaults.png new file mode 100644 index 00000000..8297db99 Binary files /dev/null and b/tests/images/test_400,300_bitmex BTC 1d defaults.png differ diff --git a/tests/images/test_400,300_bitmex BTC 1h defaults.png b/tests/images/test_400,300_bitmex BTC 1h defaults.png new file mode 100644 index 00000000..ca7e928e Binary files /dev/null and b/tests/images/test_400,300_bitmex BTC 1h defaults.png differ diff --git a/tests/images/test_400,300_bitmex BTC 5m defaults.png b/tests/images/test_400,300_bitmex BTC 5m defaults.png new file mode 100644 index 00000000..1ddb281f Binary files /dev/null and b/tests/images/test_400,300_bitmex BTC 5m defaults.png differ diff --git a/tests/images/test_400,300_bitmex ETH 1d defaults.png b/tests/images/test_400,300_bitmex ETH 1d defaults.png new file mode 100644 index 00000000..e4b3bc61 Binary files /dev/null and b/tests/images/test_400,300_bitmex ETH 1d defaults.png differ diff --git a/tests/images/test_400,300_bitmex ETH 1h defaults.png b/tests/images/test_400,300_bitmex ETH 1h defaults.png new file mode 100644 index 00000000..03078051 Binary files /dev/null and b/tests/images/test_400,300_bitmex ETH 1h defaults.png differ diff --git a/tests/images/test_400,300_bitmex ETH 5m defaults.png b/tests/images/test_400,300_bitmex ETH 5m defaults.png new file mode 100644 index 00000000..e485c827 Binary files /dev/null and b/tests/images/test_400,300_bitmex ETH 5m defaults.png differ diff --git a/tests/images/test_400,300_cryptocom CRO 1d defaults.png b/tests/images/test_400,300_cryptocom CRO 1d defaults.png new file mode 100644 index 00000000..ff8b5e63 Binary files /dev/null and b/tests/images/test_400,300_cryptocom CRO 1d defaults.png differ diff --git a/tests/images/test_400,300_cryptocom CRO 1h defaults.png b/tests/images/test_400,300_cryptocom CRO 1h defaults.png new file mode 100644 index 00000000..68bc2dc8 Binary files /dev/null and b/tests/images/test_400,300_cryptocom CRO 1h defaults.png differ diff --git a/tests/images/test_400,300_cryptocom CRO 5m defaults.png b/tests/images/test_400,300_cryptocom CRO 5m defaults.png new file mode 100644 index 00000000..facf3daa Binary files /dev/null and b/tests/images/test_400,300_cryptocom CRO 5m defaults.png differ diff --git a/tests/images/test_640,448_APPLE 1mo defaults.png b/tests/images/test_640,448_APPLE 1mo defaults.png new file mode 100644 index 00000000..41fb3288 Binary files /dev/null and b/tests/images/test_640,448_APPLE 1mo defaults.png differ diff --git a/tests/images/test_640,448_APPLE 3mo defaults.png b/tests/images/test_640,448_APPLE 3mo defaults.png new file mode 100644 index 00000000..65a2517d Binary files /dev/null and b/tests/images/test_640,448_APPLE 3mo defaults.png differ diff --git a/tests/images/test_640,448_AUDCAD 3mo defaults with entry.png b/tests/images/test_640,448_AUDCAD 3mo defaults with entry.png new file mode 100644 index 00000000..95ad03a6 Binary files /dev/null and b/tests/images/test_640,448_AUDCAD 3mo defaults with entry.png differ diff --git a/tests/images/test_640,448_BTC EXPANDED.png b/tests/images/test_640,448_BTC EXPANDED.png new file mode 100644 index 00000000..d439e701 Binary files /dev/null and b/tests/images/test_640,448_BTC EXPANDED.png differ diff --git a/tests/images/test_640,448_BTC HOLDINGS.png b/tests/images/test_640,448_BTC HOLDINGS.png new file mode 100644 index 00000000..04eb5a3e Binary files /dev/null and b/tests/images/test_640,448_BTC HOLDINGS.png differ diff --git a/tests/images/test_640,448_BTC OVERLAY2.png b/tests/images/test_640,448_BTC OVERLAY2.png new file mode 100644 index 00000000..d8067165 Binary files /dev/null and b/tests/images/test_640,448_BTC OVERLAY2.png differ diff --git a/tests/images/test_640,448_BTC VOLUME EXPANDED.png b/tests/images/test_640,448_BTC VOLUME EXPANDED.png new file mode 100644 index 00000000..91158bd6 Binary files /dev/null and b/tests/images/test_640,448_BTC VOLUME EXPANDED.png differ diff --git a/tests/images/test_640,448_BTC VOLUME OVERLAY2.png b/tests/images/test_640,448_BTC VOLUME OVERLAY2.png new file mode 100644 index 00000000..be9c5da0 Binary files /dev/null and b/tests/images/test_640,448_BTC VOLUME OVERLAY2.png differ diff --git a/tests/images/test_640,448_BTC VOLUME.png b/tests/images/test_640,448_BTC VOLUME.png new file mode 100644 index 00000000..f3c75d0c Binary files /dev/null and b/tests/images/test_640,448_BTC VOLUME.png differ diff --git a/tests/images/test_640,448_GBPJPY 3mo defaults with entry.png b/tests/images/test_640,448_GBPJPY 3mo defaults with entry.png new file mode 100644 index 00000000..f82b0216 Binary files /dev/null and b/tests/images/test_640,448_GBPJPY 3mo defaults with entry.png differ diff --git a/tests/images/test_640,448_bitmex BTC 1d defaults.png b/tests/images/test_640,448_bitmex BTC 1d defaults.png new file mode 100644 index 00000000..293ddefe Binary files /dev/null and b/tests/images/test_640,448_bitmex BTC 1d defaults.png differ diff --git a/tests/images/test_640,448_bitmex BTC 1h defaults.png b/tests/images/test_640,448_bitmex BTC 1h defaults.png new file mode 100644 index 00000000..87aa8357 Binary files /dev/null and b/tests/images/test_640,448_bitmex BTC 1h defaults.png differ diff --git a/tests/images/test_640,448_bitmex BTC 5m defaults.png b/tests/images/test_640,448_bitmex BTC 5m defaults.png new file mode 100644 index 00000000..624467ea Binary files /dev/null and b/tests/images/test_640,448_bitmex BTC 5m defaults.png differ diff --git a/tests/images/test_640,448_bitmex ETH 1d defaults.png b/tests/images/test_640,448_bitmex ETH 1d defaults.png new file mode 100644 index 00000000..cfc029e1 Binary files /dev/null and b/tests/images/test_640,448_bitmex ETH 1d defaults.png differ diff --git a/tests/images/test_640,448_bitmex ETH 1h defaults.png b/tests/images/test_640,448_bitmex ETH 1h defaults.png new file mode 100644 index 00000000..81861c92 Binary files /dev/null and b/tests/images/test_640,448_bitmex ETH 1h defaults.png differ diff --git a/tests/images/test_640,448_bitmex ETH 5m defaults.png b/tests/images/test_640,448_bitmex ETH 5m defaults.png new file mode 100644 index 00000000..9fd8a409 Binary files /dev/null and b/tests/images/test_640,448_bitmex ETH 5m defaults.png differ diff --git a/tests/images/test_640,448_cryptocom CRO 1d defaults.png b/tests/images/test_640,448_cryptocom CRO 1d defaults.png new file mode 100644 index 00000000..82b0181a Binary files /dev/null and b/tests/images/test_640,448_cryptocom CRO 1d defaults.png differ diff --git a/tests/images/test_640,448_cryptocom CRO 1h defaults.png b/tests/images/test_640,448_cryptocom CRO 1h defaults.png new file mode 100644 index 00000000..3864f49d Binary files /dev/null and b/tests/images/test_640,448_cryptocom CRO 1h defaults.png differ diff --git a/tests/images/test_640,448_cryptocom CRO 5m defaults.png b/tests/images/test_640,448_cryptocom CRO 5m defaults.png new file mode 100644 index 00000000..25c21247 Binary files /dev/null and b/tests/images/test_640,448_cryptocom CRO 5m defaults.png differ diff --git a/tests/test_chart_rendering.py b/tests/test_chart_rendering.py index dbb2b966..4603d47e 100644 --- a/tests/test_chart_rendering.py +++ b/tests/test_chart_rendering.py @@ -1,4 +1,4 @@ -from PIL import Image, ImageChops +from PIL import Image, ImageChops, ImageOps from src.configuration.bitbot_files import use_config_dir from src.configuration.bitbot_config import load_config_ini from src.bitbot import BitBot @@ -11,118 +11,200 @@ files = use_config_dir(os.path.join(curdir, "../")) -def load_config(): - config = load_config_ini(files) - return config +# physical screen renderers for approval testing +class screen_output_renderers: + wave27b = {'output': 'waveshare.epd2in7b_V2'} + inky = {'output': 'inky'} -class disks: +# s/m/l image file renderers for automated testing +class disk_output_renderers: disk_small = {'output': 'disk', 'resolution': "264,176"} disk_med = {'output': 'disk', 'resolution': "400,300"} disk_large = {'output': 'disk', 'resolution': "640,448"} all = [disk_small, disk_med, disk_large] -class screens: - wave27b = {'output': 'waveshare.epd2in7b_V2'} - inky = {'output': 'inky'} +# basic config +config_defaults = { + 'currency': { + 'stock_symbol': '', + 'exchange': 'bitmex', + 'instrument': 'BTC/USD', + 'holdings': '0', + 'chart_since': '2021-08-22T00:00:00Z', + 'entry_price': 0, + }, + 'display': { + 'output': 'disk', + 'resolution': '400x300', + 'overlay_layout': '1', + 'expanded_chart': 'false', + 'show_volume': 'falsae', + 'candle_width': '1h', + 'rotation': '0', + 'show_ip': 'false', + 'timestamp': 'false', + }, + 'comments': { + 'up': 'moon', + 'down': 'doom', + } +} + + +# test-specific config +test_configs = { + "APPLE 1mo defaults": { + 'currency': {'stock_symbol': 'AAPL'}, + 'display': {'candle_width': '1mo'}, + }, + "APPLE 3mo defaults": { + 'currency': {'stock_symbol': 'AAPL'}, + 'display': {'candle_width': '3mo'}, + }, + "GBPJPY 3mo defaults with entry": { + 'display': {'candle_width': '3mo'}, + 'currency': { + 'stock_symbol': 'GBPJPY=X', + 'entry_price': '167', + 'chart_since': '2022-04-22T00:00:00Z', # yfinance limits to gathering 7 days of low-timeframe from the last 60 days + 'holdings': '10', + }, + 'display': {'candle_width': '5m', }, + }, + "AUDCAD 3mo defaults with entry": { + 'currency': { + 'stock_symbol': 'AUDCAD=X', + 'entry_price': '0.89332', + 'chart_since': '', # yfinance limits to gathering 7 days of low-timeframe from the last 60 days + 'holdings': '450000', + }, + 'display': {'candle_width': '1h', }, + }, + "bitmex BTC 5m defaults": { + 'display': {'candle_width': '5m'}, + }, + "bitmex BTC 1h defaults": { + 'display': {'candle_width': '1h'}, + }, + "bitmex BTC 1d defaults": { + 'display': {'candle_width': '1d'}, + }, + "BTC HOLDINGS": { + 'currency': {'holdings': "100"}, + }, + "BTC VOLUME": { + 'display': {'show_volume': 'true'}, + }, + "BTC EXPANDED": { + 'display': {'expanded_chart': 'true'}, + }, + "BTC VOLUME EXPANDED": { + 'display': {'show_volume': 'true', 'expanded_chart': 'true'}, + }, + "BTC VOLUME OVERLAY2": { + 'display': {'overlay_layout': '2', 'show_volume': 'true'}, + }, + "BTC OVERLAY2": { + 'display': {'overlay_layout': '2'}, + }, + "bitmex ETH 5m defaults": { + 'currency': {'instrument': 'ETH/USD'}, + 'display': {'candle_width': '5m'}, + }, + "bitmex ETH 1h defaults": { + 'currency': {'instrument': 'ETH/USD'}, + 'display': {'candle_width': '1h'}, + }, + "bitmex ETH 1d defaults": { + 'currency': {'instrument': 'ETH/USD'}, + 'display': {'candle_width': '1d'}, + }, + "cryptocom CRO 5m defaults": { + 'currency': {'instrument': 'CRO/USDC', 'exchange': 'cryptocom'}, + 'display': {'candle_width': '5m'}, + }, + "cryptocom CRO 1h defaults": { + 'currency': {'instrument': 'CRO/USDC', 'exchange': 'cryptocom'}, + 'display': {'candle_width': '1h'}, + }, + "cryptocom CRO 1d defaults": { + 'currency': { + 'instrument': 'CRO/USDC', + 'exchange': 'cryptocom', + }, + 'display': {'candle_width': '1d'}, + }, +} +os.makedirs('tests/images/', exist_ok=True) -# load config -test_params = [ - ("APPLE 1mo defaults", "", "", "AAPL", "1", "false", "false", "1mo", ""), - ("APPLE 3mo defaults", "", "", "AAPL", "1", "false", "false", "3mo", ""), - - ("bitmex BTC 5m defaults", "bitmex", "BTC/USD", "", "1", "false", "false", "5m", ""), - ("bitmex BTC 1h defaults", "bitmex", "BTC/USD", "", "1", "false", "false", "1h", ""), - ("bitmex BTC 1d defaults", "bitmex", "BTC/USD", "", "1", "false", "false", "1d", ""), - # BTC - ("BTC HOLDINGS", "bitmex", "BTC/USD", "", "1", "false", "false", "1d", "100"), - ("BTC VOLUME", "bitmex", "BTC/USD", "", "1", "false", "true", "1d", ""), - ("BTC EXPANDED", "bitmex", "BTC/USD", "", "1", "true", "false", "1d", ""), - ("BTC VOLUME EXPANDED", "bitmex", "BTC/USD", "", "1", "true", "true", "1d", ""), - ("BTC VOLUME OVERLAY2", "bitmex", "BTC/USD", "", "2", "false", "true", "1d", ""), - ("BTC OVERLAY2", "bitmex", "BTC/USD", "", "2", "false", "false", "1d", ""), - # ETH - ("bitmex ETH 5m defaults", "bitmex", "ETH/USD", "", "1", "false", "false", "5m", ""), - ("bitmex ETH 1h defaults", "bitmex", "ETH/USD", "", "1", "false", "false", "1h", ""), - ("bitmex ETH 1d defaults", "bitmex", "ETH/USD", "", "1", "false", "false", "1d", ""), - # CRO - ("cryptocom CRO 5m defaults", "cryptocom", "CRO/USDC", "", "1", "false", "false", "5m", ""), - ("cryptocom CRO 1h defaults", "cryptocom", "CRO/USDC", "", "1", "false", "false", "1h", ""), - ("cryptocom CRO 1d defaults", "cryptocom", "CRO/USDC", "", "1", "false", "false", "1d", ""), - # FOREX - ("GBPJPY", "", "", "GBPJPY=X", "1", "false", "false", "1mo", "100"), -] # name, exch, token, stock, overlay, expand, volume, candle_width, holdings -os.makedirs('tests/images/', exist_ok=True) +def assert_image_matches_size(new_image, expected_res): + actual_res = f"{new_image.width},{new_image.height}" + assert expected_res == actual_res, f"expected {expected_res}, was {actual_res}" + + +def assert_image_unchanged(previous_image, new_image, file_name): + diff = ImageChops.difference(new_image, previous_image) + if diff.getbbox(): + diff_file_path = '.fail.png'.join(file_name.rsplit('.png')) + diff.save(diff_file_path) + assert False, f"Image diff check: '{diff_file_path}'" class TestRenderingMeta(type): def __new__(mcs, name, bases, dict, output): - def gen_test(name, exch, token, stock, overlay, expand, volume, candle_width, holdings): + def gen_test(generatedTestName, custom_config): def test(self): - config = load_config() - image_file_name = f'tests/images/{name}.png' - config.set('currency', 'stock_symbol', stock) - config.set('currency', 'exchange', exch) - config.set('currency', 'instrument', token) - config.set('currency', 'holdings', holdings) - config.set('currency', 'entry_price', "10") - config.set('currency', 'chart_since', '2021-08-22T00:00:00Z') + config = load_config_ini(files) + config.read_dict(config_defaults) + config.read_dict(custom_config) + config.set('display', 'output', output['output']) config.set('display', 'resolution', output.get('resolution', '')) - config.set('display', 'overlay_layout', overlay) - config.set('display', 'expanded_chart', expand) - config.set('display', 'show_volume', volume) - config.set('display', 'candle_width', candle_width) - config.set('display', 'disk_file_name', image_file_name) - config.set('display', 'rotation', '0') - config.set('display', 'show_ip', 'false') - config.set('display', 'timestamp', 'false') - config.set('comments', 'up', 'moon') - config.set('comments', 'down', 'doom') - app = BitBot(config, files) - image_should_not_change_when(app.display_chart, image_file_name) + file_name = f'tests/images/{generatedTestName}.png' + config.set('display', 'disk_file_name', file_name) - if True: - os.system(f"code '{image_file_name}'") + app = BitBot(config, files) - def image_should_not_change_when(action, image_file_name): - # previous_image = Image.open(image_file_name) - action() - # new_image = Image.open(image_file_name) - # diff = ImageChops.difference(new_image, previous_image) - # if diff.getbbox(): - # diff.save(image_file_name) + previous_image = None # Image.open(file_name) + app.display_chart() + new_image = Image.open(file_name) - # assert False, f"images diff '{image_file_name}'" + assert_image_matches_size(new_image, output.get('resolution', '')) + # assert_image_unchanged(previous_image, new_image, file_name) return test - for test_param in test_params: - test_name = f"test_{output.get('resolution', output['output'].split('.')[-1])}_{test_param[0]}" - dict[test_name] = gen_test(*test_param) + for test_key in test_configs: + output_res = output['output'].split('.')[-1] + screen_res = output.get('resolution', output_res) + test_name = f"test_{screen_res}_{test_key}" + dict[test_name] = gen_test(test_name, test_configs[test_key]) + return type.__new__(mcs, name, bases, dict) -class SmallChartRenderingTests(unittest.TestCase, output=disks.disk_small, metaclass=TestRenderingMeta): +class SmallChartRenderingTests(unittest.TestCase, output=disk_output_renderers.disk_small, metaclass=TestRenderingMeta): __metaclass__ = TestRenderingMeta -class MediumChartRenderingTests(unittest.TestCase, output=disks.disk_med, metaclass=TestRenderingMeta): +class MediumChartRenderingTests(unittest.TestCase, output=disk_output_renderers.disk_med, metaclass=TestRenderingMeta): __metaclass__ = TestRenderingMeta -class LargeChartRenderingTests(unittest.TestCase, output=disks.disk_large, metaclass=TestRenderingMeta): +class LargeChartRenderingTests(unittest.TestCase, output=disk_output_renderers.disk_large, metaclass=TestRenderingMeta): __metaclass__ = TestRenderingMeta @unittest.skip("needs a waveshare display") -class Wave27bChartRenderingTests(unittest.TestCase, output=screens.wave27b, metaclass=TestRenderingMeta): +class Wave27bChartRenderingTests(unittest.TestCase, output=screen_output_renderers.wave27b, metaclass=TestRenderingMeta): __metaclass__ = TestRenderingMeta + @unittest.skip("needs an inky display") -class InkyChartRenderingTests(unittest.TestCase, output=screens.inky, metaclass=TestRenderingMeta): +class InkyChartRenderingTests(unittest.TestCase, output=screen_output_renderers.inky, metaclass=TestRenderingMeta): __metaclass__ = TestRenderingMeta diff --git a/tests/test_stock_exchange.py b/tests/test_stock_exchange.py index 2ef26729..48c3378e 100644 --- a/tests/test_stock_exchange.py +++ b/tests/test_stock_exchange.py @@ -1,16 +1,28 @@ import unittest -from src.exchanges import stock_exchanges -from src.configuration import bitbot_config +from src.configuration import bitbot_config, bitbot_files +from src.configuration.bitbot_files import use_config_dir +from src.configuration.bitbot_config import load_config_ini +from src.exchanges.stock_exchanges import Exchange, candle_configs +import os +import pathlib + # πŸͺ³ ''1h',' <- fails on weekends due to short chart duration test_params = ['1mo', '1h', '1wk', 'random'] +curdir = pathlib.Path(__file__).parent.resolve() +files = use_config_dir(os.path.join(curdir, "../")) +config_ini = load_config_ini(files) + +# πŸͺ³ ''1h',' <- fails on weekends due to short chart duration +test_params = candle_configs + class TestStockExchange(unittest.TestCase): def test_fetching_history(self): - for candle_width in test_params: - with self.subTest(msg=candle_width): - self.run_test(candle_width) + for candle_spec in test_params: + with self.subTest(msg=candle_spec.width): + self.run_test(candle_spec.width) def run_test(self, candle_width): stock = "TSLA" @@ -24,7 +36,10 @@ def run_test(self, candle_width): } } config = bitbot_config.BitBotConfig(mock_config, {}) - excange = stock_exchanges.Exchange(config) + excange = Exchange(config) + data = excange.fetch_history() num_candles = len(data.candle_data) - self.assertTrue(num_candles > 0, msg=f'got {num_candles} candles for {stock}') + + we_got_candles = num_candles > 0 + self.assertTrue(we_got_candles, msg=f'got {num_candles} candles for {stock}')