diff --git a/.github/workflows/kubot-app.yml b/.github/workflows/kubot-app.yml index bad607f..29c12e6 100644 --- a/.github/workflows/kubot-app.yml +++ b/.github/workflows/kubot-app.yml @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.13 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.13 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.gitignore b/.gitignore index a384f3c..d398542 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ config/config venv +certs .idea .pytest_cache __pycache__ .vscode/settings.json kubot_* -*.html \ No newline at end of file +*.html diff --git a/Dockerfile b/Dockerfile index 7f560c3..a5b6fcc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM python:3.8-alpine as base +FROM python:3.13-alpine as base RUN pip install --upgrade pip -RUN apk --update add gcc musl-dev postgresql-dev +RUN apk --update add gcc musl-dev postgresql-dev git COPY . /app WORKDIR /app diff --git a/Makefile b/Makefile index 3fa3229..d9d84d7 100644 --- a/Makefile +++ b/Makefile @@ -27,10 +27,10 @@ build-dev: ## build kubot development docker image docker build --target development --tag ${image}:${version} . docker image prune -f -compose-dev: ## compose and start kubot suite in development mode +compose-dev: gen-certs ## compose and start kubot suite in development mode KUBOT_IMAGE=${image} KUBOT_VERSION=${version} docker-compose up -d -development: ## start kubot database and gui +development: gen-certs ## start kubot database and gui KUBOT_IMAGE=${image} KUBOT_VERSION=${version} docker-compose up -d postgres grafana run-d: ## run kubot docker image detached @@ -44,9 +44,13 @@ run: ## run kubot docker image attached venv: ## bootstrap python3 venv @test -d "venv" || python3 -m venv venv +gen-certs: ## generate grafana certs + @mkdir -p certs + @openssl req -x509 -newkey rsa:2048 -keyout certs/grafana.key -out certs/grafana.crt -days 365 -nodes -subj "/CN=localhost" + install: venv ## install kubot dependencies @source venv/bin/activate; \ - pip install --upgrade pip setuptools==57.5.0; \ + pip install --upgrade pip setuptools==75.6.0; \ LIBRARY_PATH=$LIBRARY_PATH:/opt/homebrew/opt/openssl/lib pip install -r requirements.txt; test: venv ## run pytest suite diff --git a/config/config.demo b/config/config.demo index 474399c..a572d8e 100644 --- a/config/config.demo +++ b/config/config.demo @@ -10,6 +10,9 @@ minimum_rate = 0.0005 charge = 0.00005 interval = 300 currencies = [{"currency": "USDT", "term": 28, "reserved_amount": 10}] +symbols = ["ETH-USDT", "ETH-BTC"] +category_currency = ["DUAL"] +mode = DUAL [pushover] user_key = user_key diff --git a/docker-compose.yml b/docker-compose.yml index 046c31c..bd3c918 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,8 +33,13 @@ services: restart: on-failure:10 depends_on: - postgres + environment: + - GF_SERVER_PROTOCOL=https + - GF_SERVER_CERT_FILE=/etc/grafana/certs/grafana.crt + - GF_SERVER_CERT_KEY=/etc/grafana/certs/grafana.key volumes: - grafana-storage:/var/lib/grafana + - ./certs:/etc/grafana/certs:ro - ./provisioning:/etc/grafana/provisioning ports: - 3000:3000 diff --git a/provisioning/dashboards/Kubot/kubot-dual-investment-dashboard.json b/provisioning/dashboards/Kubot/kubot-dual-investment-dashboard.json new file mode 100644 index 0000000..db53a90 --- /dev/null +++ b/provisioning/dashboards/Kubot/kubot-dual-investment-dashboard.json @@ -0,0 +1,740 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "description": "Total Interest\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "decimals": 2, + "displayName": "USDT", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 8, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P0689FA73A582FCBA" + }, + "editorMode": "code", + "format": "table", + "hide": false, + "rawQuery": true, + "rawSql": "WITH daily_sum_usdt AS (\n SELECT\n ((item ->> 'createdAt')::numeric) as createdAt,\n ((item ->> 'amount')::numeric) AS amount\n FROM\n ledgerassets t,\n LATERAL json_array_elements(t.ledger -> 'items') AS item\n WHERE\n item ->> 'direction' = 'in'\n AND item ->> 'currency' = 'USDT'\n GROUP BY\n createdAt, amount\n),\nsummarized_amounts AS (\n SELECT\n createdAt as \"time\",\n\n amount - LEAD(amount) OVER (ORDER BY createdAt DESC) AS diff_with_next_day\n FROM\n daily_sum_usdt\n ORDER BY\n time DESC\n)\n\nselect sum(diff_with_next_day) as sum_usdt from summarized_amounts;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P0689FA73A582FCBA" + }, + "editorMode": "code", + "format": "table", + "hide": false, + "rawQuery": true, + "rawSql": "WITH daily_sum AS (\n SELECT\n to_timestamp(((item ->> 'createdAt')::numeric) / 1000) as createdAt,\n ((item ->> 'amount')::numeric) AS amount\n FROM\n ledgerassets t,\n LATERAL json_array_elements(t.ledger -> 'items') AS item\n WHERE\n item ->> 'direction' = 'in'\n AND item ->> 'currency' = 'ETH'\n GROUP BY\n createdAt, amount\n),\nsummarized_amounts_eth AS (\n SELECT\n createdAt as \"time\",\n ((amount - LEAD(amount) OVER (ORDER BY createdAt DESC)) * l2.lastTradedPrice) AS diff_with_next_day\n FROM\n daily_sum\n JOIN LATERAL (select ((symbol->>'lastTradedPrice')::float) as lastTradedPrice from symbolassets as l2 where symbol->>'symbolCode' = 'ETH-USDT' order by ABS(EXTRACT(EPOCH FROM (daily_sum.createdAt - l2.time))) asc limit 1) as l2 on true\n GROUP BY daily_sum.createdAt, daily_sum.amount, l2.lastTradedPrice\n order by\n daily_sum.createdAt DESC\n)\nselect sum(diff_with_next_day) as sum_eth from summarized_amounts_eth;", + "refId": "B", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Total Interest", + "transformations": [ + { + "id": "concatenate", + "options": {} + }, + { + "id": "calculateField", + "options": { + "alias": "interest", + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "sum_usdt" + } + }, + "operator": "+", + "right": { + "matcher": { + "id": "byName", + "options": "sum_eth" + } + } + }, + "cumulative": { + "field": "sum_usdt", + "reducer": "sum" + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": true + } + } + ], + "type": "gauge" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "description": "Profit ETH\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 1, + "spanNulls": false + }, + "decimals": 2, + "displayName": "USDT", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 20, + "x": 4, + "y": 0 + }, + "id": 7, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.86, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P0689FA73A582FCBA" + }, + "editorMode": "code", + "format": "table", + "hide": false, + "rawQuery": true, + "rawSql": "WITH daily_sum AS (\n SELECT\n to_timestamp(((item ->> 'createdAt')::numeric) / 1000) as createdAt,\n ((item ->> 'amount')::numeric) AS amount\n FROM\n ledgerassets t,\n LATERAL json_array_elements(t.ledger -> 'items') AS item\n WHERE\n item ->> 'direction' = 'in'\n AND item ->> 'currency' = 'ETH'\n GROUP BY\n createdAt, amount\n)\n\nSELECT\n createdAt as \"time\",\n ((amount - LEAD(amount) OVER (ORDER BY createdAt DESC)) * l2.lastTradedPrice) AS diff_with_next_day\nFROM\n daily_sum\nJOIN LATERAL (select ((symbol->>'lastTradedPrice')::float) as lastTradedPrice from symbolassets as l2 where symbol->>'symbolCode' = 'ETH-USDT' order by ABS(EXTRACT(EPOCH FROM (daily_sum.createdAt - l2.time))) asc limit 1) as l2 on true\nGROUP BY daily_sum.createdAt, daily_sum.amount, l2.lastTradedPrice\norder by\n daily_sum.createdAt DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Profit ETH", + "type": "state-timeline" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "description": "Profit USDT\n", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "axisPlacement": "auto", + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 1, + "spanNulls": false + }, + "decimals": 2, + "displayName": "USDT", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 20, + "x": 4, + "y": 5 + }, + "id": 6, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "mergeValues": true, + "rowHeight": 0.86, + "showValue": "auto", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P0689FA73A582FCBA" + }, + "editorMode": "code", + "format": "table", + "hide": false, + "rawQuery": true, + "rawSql": "WITH daily_sum AS (\n SELECT\n ((item ->> 'createdAt')::numeric) as createdAt,\n ((item ->> 'amount')::numeric) AS amount\n FROM\n ledgerassets t,\n LATERAL json_array_elements(t.ledger -> 'items') AS item\n WHERE\n item ->> 'direction' = 'in'\n AND item ->> 'currency' = 'USDT'\n GROUP BY\n createdAt, amount\n)\n\nSELECT\n createdAt as \"time\",\n\n amount - LEAD(amount) OVER (ORDER BY createdAt DESC) AS diff_with_next_day\nFROM\n daily_sum\nORDER BY\n time DESC;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Profit USDT", + "type": "state-timeline" + }, + { + "datasource": { + "type": "postgres", + "uid": "P0689FA73A582FCBA" + }, + "description": "Interest over ETH and USDT", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "right", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyUSD" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "amount ETH" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "left" + }, + { + "id": "displayName", + "value": "Amount ETH" + }, + { + "id": "custom.axisLabel", + "value": "ETH" + }, + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": 5 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "amount USDT" + }, + "properties": [ + { + "id": "displayName", + "value": "Amount USDT" + }, + { + "id": "custom.axisLabel", + "value": "USDT" + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "P0689FA73A582FCBA" + }, + "editorMode": "code", + "format": "time_series", + "group": [], + "metricColumn": "none", + "rawQuery": true, + "rawSql": "SELECT\n time as \"time\",\n ((item->>'amount')::float) as amount,\n ((item->>'currency')) as currency\nFROM\n ledgerassets t,\n LATERAL json_array_elements(t.ledger -> 'items') AS item\nWHERE\n (item ->> 'direction' = 'in')\nGROUP BY\n item ->> 'id', amount, time, currency\nORDER BY 1;\n", + "refId": "A", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "column" + } + ] + ], + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + }, + "timeColumn": "time", + "where": [ + { + "name": "$__timeFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "title": "Profit USDT / ETH", + "type": "timeseries" + }, + { + "datasource": { + "type": "postgres", + "uid": "P0689FA73A582FCBA" + }, + "description": "ETH-USDT Last Traded Price", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "displayName": "ETH-USDT", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "ETH-USDT" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 10 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "P0689FA73A582FCBA" + }, + "editorMode": "code", + "format": "time_series", + "group": [], + "metricColumn": "none", + "rawQuery": true, + "rawSql": "SELECT\n time AS \"time\",\n ((symbol->>'lastTradedPrice')::float) as lastTradedPrice\nFROM symbolassets\nWHERE (symbol->>'symbol') = 'ETH-USDT'\nORDER BY 1", + "refId": "A", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "column" + } + ], + [ + { + "params": [ + "value" + ], + "type": "column" + } + ] + ], + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + }, + "table": "symbolassets", + "timeColumn": "time", + "timeColumnType": "timestamp", + "where": [ + { + "name": "$__timeFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "title": "Last Traded Price ETH - USDT", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-40d", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Dual Investment", + "uid": "heg0onYNk", + "version": 40 +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f67c291..629c856 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,23 @@ attrs==20.3.0 certifi==2020.6.20 chardet==3.0.4 +charset-normalizer==3.4.0 contextlib2==0.6.0.post1 idna==2.10 iniconfig==1.1.1 -kucoin-python==1.0.6 +kucoin-python @ git+https://github.com/desytech/kucoin-python-sdk.git@master markdown2==2.3.10 packaging==20.7 peewee==3.14.0 -pluggy==0.13.1 -psycopg2-binary==2.8.6 -py==1.9.0 +pluggy==1.6.0 +psycopg2-binary==2.9.10 +pushover @ git+https://github.com/Wyattjoh/pushover@5852545c5b9cf2717e1eafc4c8b134a08b0994da pyarmor==6.6.0 +Pygments==2.19.2 pyparsing==2.4.7 -pytest==6.1.2 -python-pushover==0.4 -requests==2.24.0 +pytest==8.4.1 +requests==2.32.3 schema==0.7.2 toml==0.10.2 -urllib3==1.25.11 +urllib3==2.2.3 websockets==8.1 diff --git a/src/config/config.py b/src/config/config.py index 0e61ab6..c7196fe 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -4,7 +4,9 @@ from configparser import ConfigParser, ExtendedInterpolation import const -from schemas.config import currencies as currencies_schema +from schemas.config import currencies as currencies_schema, symbols as symbols_schema, modes as modes_schema, \ + category_currency as category_currency_schema +from schemas.static import Modes def property_wrapper(default=None): @@ -96,5 +98,22 @@ def slack_api_token(self): def slack_channel(self): return self.__config['slack'].get('channel') + @property + @property_wrapper(default=Modes.LENDING) + def mode(self): + modes = Modes(self.__config['bot'].get('mode')) + return modes_schema.validate(modes) + + @property + @property_wrapper(default=[]) + def symbols(self): + symbols = json.loads(self.__config['bot'].get('symbols')) + return symbols_schema.validate(symbols) + @property + @property_wrapper(default=[]) + def category_currency(self): + category_currency = json.loads(self.__config['bot'].get('category_currency')) + return category_currency_schema.validate(category_currency) +'' config = Config() diff --git a/src/database/models/category.py b/src/database/models/category.py new file mode 100644 index 0000000..4bb2a07 --- /dev/null +++ b/src/database/models/category.py @@ -0,0 +1,8 @@ +from playhouse.postgres_ext import * +from datetime import datetime +from database.models.base import BaseModel + +class CategoryCurrency(BaseModel): + time = DateTimeField(default=datetime.utcnow) + category = CharField() + items = JSONField() \ No newline at end of file diff --git a/src/database/models/ledger.py b/src/database/models/ledger.py new file mode 100644 index 0000000..ef3a15f --- /dev/null +++ b/src/database/models/ledger.py @@ -0,0 +1,7 @@ +from playhouse.postgres_ext import * +from datetime import datetime +from database.models.base import BaseModel + +class LedgerAssets(BaseModel): + time = DateTimeField(default=datetime.utcnow) + ledger = JSONField() \ No newline at end of file diff --git a/src/database/models/symbols.py b/src/database/models/symbols.py new file mode 100644 index 0000000..67571b2 --- /dev/null +++ b/src/database/models/symbols.py @@ -0,0 +1,7 @@ +from playhouse.postgres_ext import * +from datetime import datetime +from database.models.base import BaseModel + +class SymbolAssets(BaseModel): + time = DateTimeField(default=datetime.utcnow) + symbol = JSONField() \ No newline at end of file diff --git a/src/kubot.py b/src/kubot.py index a618f6a..6a297bf 100644 --- a/src/kubot.py +++ b/src/kubot.py @@ -4,11 +4,15 @@ import const from database.models.base import db +from database.models.category import CategoryCurrency from database.models.market import FundingMarket from database.models.activeorder import ActiveLendOrder from database.models.assets import LendingAssets +from database.models.ledger import LedgerAssets +from database.models.symbols import SymbolAssets from kucoin.client import Margin, User from config.config import config +from schemas.static import Modes from logger import Logger from datetime import datetime, timedelta from notification.pushovernotifier import PushoverNotifier @@ -20,9 +24,10 @@ class Scheduler(object): - def __init__(self, notifiers, currencies): + def __init__(self, notifiers, currencies, mode): self.__client = Margin(config.api_key, config.api_secret, config.api_passphrase) self.__user = User(config.api_key, config.api_secret, config.api_passphrase) + self.__user_public = User(config.api_key, config.api_secret, config.api_passphrase, "https://www.kucoin.com") self.__notifiers = notifiers self.__currencies = currencies self.__scheduler = sched.scheduler(time.time, time.sleep) @@ -39,26 +44,35 @@ def cleanup_database(self): FundingMarket.delete().where(FundingMarket.time < time_delta).execute() ActiveLendOrder.delete().where(ActiveLendOrder.time < time_delta).execute() LendingAssets.delete().where(LendingAssets.time < time_delta).execute() + LedgerAssets.delete().where(LedgerAssets.time < time_delta).execute() + SymbolAssets.delete().where(SymbolAssets.time < time_delta).execute() + CategoryCurrency.delete().where(CategoryCurrency.time < time_delta).execute() def schedule_checks(self, interval): self.__scheduler.enter(interval, 1, self.schedule_checks, argument=(interval,)) self.cleanup_database() - for currency in self.__currencies: - try: - min_int_rate = self.get_min_daily_interest_rate(currency) - min_int_rate_charge = float(format(min_int_rate + config.charge, '.5f')) - if min_int_rate_charge <= config.minimum_rate: - self.__minimum_rate = config.minimum_rate - elif self.__minimum_rate == const.DEFAULT_MIN_RATE or abs(min_int_rate_charge - self.__minimum_rate) >= config.correction: - self.__minimum_rate = min_int_rate_charge - self.get_lending_assets(currency) - self.check_active_loans(min_int_rate, currency) - self.lend_loans(min_int_rate, currency) - self.check_active_lendings(currency) - except (socket.timeout, requests.exceptions.Timeout) as e: - Logger().logger.error("Currency: %s, Transport Exception occurred: %s", currency.name, e) - except Exception as e: - Logger().logger.error("Currency: %s, Generic Error occurred: %s", currency.name, e) + try: + match config.mode: + case Modes.LENDING: + for currency in self.__currencies: + min_int_rate = self.get_min_daily_interest_rate(currency) + min_int_rate_charge = float(format(min_int_rate + config.charge, '.5f')) + if min_int_rate_charge <= config.minimum_rate: + self.__minimum_rate = config.minimum_rate + elif self.__minimum_rate == const.DEFAULT_MIN_RATE or abs(min_int_rate_charge - self.__minimum_rate) >= config.correction: + self.__minimum_rate = min_int_rate_charge + self.get_lending_assets(currency) + self.check_active_loans(min_int_rate, currency) + self.lend_loans(min_int_rate, currency) + self.check_active_lendings(currency) + case Modes.DUAL: + self.check_ledger() + self.check_symbol_trigger() + self.check_dual_investments() + except (socket.timeout, requests.exceptions.Timeout) as e: + Logger().logger.error("Transport Exception occurred: %s", e) + except Exception as e: + Logger().logger.error("Generic Error occurred: %s", e) def get_lending_assets(self, currency): asset = self.__client.get_lend_record(currency=currency.name) @@ -117,6 +131,28 @@ def get_min_daily_interest_rate(self, currency): else: return config.default_interest + def check_ledger(self): + current_page = 1 + page_size = 50 + ledger = self.__user.get_account_ledger(pageSize=page_size, currentPage=current_page) + for page in range(current_page + 1, ledger['totalPage'] + 1): + result = self.__user.get_account_ledger(pageSize=page_size, currentPage=page) + ledger['items'].extend(result['items']) + ledger_asset = LedgerAssets(ledger=ledger) + Logger().logger.info('%s rows saved into the ledger table', ledger_asset.save()) + + def check_symbol_trigger(self): + for symbol in config.symbols: + result = self.__user_public.get_symbol_ticks(symbols=symbol)[0] + symbol_asset = SymbolAssets(symbol=result) + Logger().logger.info('%s rows saved into the %s symbols table', symbol_asset.save(), symbol) + + def check_dual_investments(self): + for category in config.category_currency: + result = self.__user_public.get_category_currency(category=category) + category_currency = CategoryCurrency(category=category, items=result) + Logger().logger.info('%s rows saved into %s category currency table', category_currency.save(), category) + def check_active_lendings(self, currency): current_page = 1 page_size = 50 @@ -155,16 +191,18 @@ def try_add_notifier(notifier, current_notifiers): def main(): Logger().logger.info("Starting Kubot Version {} - " - "Config: Correction: {}, Default Interest: {}, Minimum Rate: {}, Charge: {}" + "Config: Mode: {}, Correction: {}, Default Interest: {}, Minimum Rate: {}, Charge: {}, Symbols: {}" .format(get_version(), + config.mode.value, convert_float_to_percentage(config.correction), convert_float_to_percentage(config.default_interest), 'disabled' if config.minimum_rate == const.DEFAULT_MIN_RATE else convert_float_to_percentage(config.minimum_rate), - convert_float_to_percentage(config.charge))) + convert_float_to_percentage(config.charge), + config.symbols)) # initialize database with db: - db.create_tables([FundingMarket, ActiveLendOrder, LendingAssets]) + db.create_tables([FundingMarket, ActiveLendOrder, LendingAssets, LedgerAssets, SymbolAssets, CategoryCurrency]) # initialize notifier systems notifiers = [] @@ -177,7 +215,7 @@ def main(): currencies = [Currency(currency) for currency in config.currencies] # start main scheduler process - Scheduler(notifiers=notifiers, currencies=currencies) + Scheduler(notifiers=notifiers, currencies=currencies, mode=config.mode) if __name__ == "__main__": diff --git a/src/notification/pushovernotifier.py b/src/notification/pushovernotifier.py index c155d7d..7ce0d2b 100644 --- a/src/notification/pushovernotifier.py +++ b/src/notification/pushovernotifier.py @@ -1,7 +1,7 @@ import re from notification.notify import Notifier -from pushover import Client +from pushover import Pushover from logger import Logger REGEX_PUSHOVER_KEYS = r'^\w{30,30}$' @@ -11,7 +11,8 @@ class PushoverNotifier(Notifier): def __init__(self, config): self.user_key = config.user_key self.api_token = config.api_token - self.client = Client(self.user_key, api_token=self.api_token) + self.client = Pushover(self.api_token) + self.client.user(self.user_key) @staticmethod def is_valid_config(config): @@ -27,7 +28,9 @@ def api(self): def send_message(self, message, title=None): try: - self.api.send_message(message, title=title) + msg = self.api.msg(message) + msg.set("title", title) + self.api.send(msg) except Exception as e: Logger().logger.error("Pushover send message error: %s", e) diff --git a/src/schemas/config.py b/src/schemas/config.py index 09b6682..e80059f 100644 --- a/src/schemas/config.py +++ b/src/schemas/config.py @@ -1,9 +1,22 @@ from schema import Schema, Or, And +from .static import Modes, CategoryCurrencyEnum currencies = Schema([ { - 'currency': Or("USDT"), + 'currency': Or("USDT", "USDC"), 'term': Or(7, 14, 28), 'reserved_amount': And(int, lambda a: a >= 0) } ]) + +symbols = Schema([ + Or("ETH-USDT", "ETH-BTC") +]) + +modes = Schema( + Or(Modes.DUAL, Modes.LENDING) +) + +category_currency = Schema([ + Or(CategoryCurrencyEnum.DUAL.value) +]) diff --git a/src/schemas/static.py b/src/schemas/static.py new file mode 100644 index 0000000..4b5e78d --- /dev/null +++ b/src/schemas/static.py @@ -0,0 +1,8 @@ +from enum import Enum + +class Modes(Enum): + LENDING="LENDING" + DUAL="DUAL" + +class CategoryCurrencyEnum(Enum): + DUAL="DUAL" \ No newline at end of file