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 @@
\ 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}')