diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 38386e04e..da07ed430 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -97,6 +97,35 @@ jobs: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} docker push carloswufei/binbot_streaming + deploy_cronjobs: + name: Deploy cronjobs + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + - name: Build image + run: docker build --tag binbot_cronjobs -f Dockerfile.cronjobs . + - name: Test run script + run: | + docker run --network host --name binbot_cronjobs \ + -e MONGO_HOSTNAME=${{ env.MONGO_HOSTNAME }} \ + -e MONGO_PORT=${{ env.MONGO_PORT }} \ + -e MONGO_APP_DATABASE=${{ env.MONGO_APP_DATABASE }} \ + -e MONGO_AUTH_USERNAME=${{ env.MONGO_AUTH_USERNAME }} \ + -e MONGO_AUTH_PASSWORD=${{ env.MONGO_AUTH_PASSWORD }} \ + -e PYTHONUNBUFFERED=TRUE \ + -e ENV=ci -d binbot_cronjobs + - name: Tag image + if: ${{ github.actor != 'dependabot[bot]' }} + run: | + docker commit binbot_cronjobs carloswufei/binbot_cronjobs & + docker tag binbot_cronjobs carloswufei/binbot_cronjobs + - name: Push to Docker Hub + if: ${{ github.actor != 'dependabot[bot]' }} + run: | + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + docker push carloswufei/binbot_cronjobs + python_tests: name: Python code tests diff --git a/.vscode/launch.json b/.vscode/launch.json index ed344a7da..74c69efee 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,14 @@ "request": "launch", "program": "${workspaceFolder}/api/market_updates.py", "console": "internalConsole", + "justMyCode": false + }, + { + "name": "Python: cronjobs", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/api/cronjobs.py", + "console": "internalConsole", "justMyCode": true }, { @@ -56,14 +64,6 @@ "program": "binquant/consumer/__init__.py", "console": "internalConsole", "justMyCode": true - }, - { - "name": "Python: Test Producer", - "type": "python", - "request": "launch", - "program": "binquant/kafka_producer.py", - "console": "internalConsole", - "justMyCode": true - }, + } ] } diff --git a/Dockerfile.cronjobs b/Dockerfile.cronjobs new file mode 100644 index 000000000..f17f93f78 --- /dev/null +++ b/Dockerfile.cronjobs @@ -0,0 +1,9 @@ +FROM ubuntu:latest +RUN apt-get update && apt-get install -y --no-install-recommends python3-pip build-essential python3-dev python-setuptools +COPY api api +WORKDIR api +RUN pip3 install pipenv --no-cache-dir --upgrade +RUN pipenv install --system --deploy --ignore-pipfile --clear +ENTRYPOINT ["python3", "-u", "cronjobs.py"] + +STOPSIGNAL SIGTERM diff --git a/api/account/account.py b/api/account/account.py index 03cb3560d..eefae30ea 100644 --- a/api/account/account.py +++ b/api/account/account.py @@ -220,15 +220,6 @@ def min_notional_by_symbol(self, symbol, min_notional_limit="minNotional"): ) return min_notional_filter[min_notional_limit] - def get_qty_precision(self, pair): - lot_size_by_symbol = self.lot_size_by_symbol(pair, "stepSize") - qty_precision = -( - Decimal(str(lot_size_by_symbol)) - .as_tuple() - .exponent - ) - return qty_precision - def get_one_balance(self, symbol="BTC"): # Response after request data = self.bb_request(url=self.bb_balance_url) diff --git a/api/account/assets.py b/api/account/assets.py index 258ef125f..fd5f12ac2 100644 --- a/api/account/assets.py +++ b/api/account/assets.py @@ -9,14 +9,13 @@ from db import setup_db from tools.handle_error import json_response, json_response_error, json_response_message from tools.round_numbers import round_numbers -from tools.exceptions import BinanceErrors, InvalidSymbol +from tools.exceptions import BinanceErrors, InvalidSymbol, MarginLoanNotFound from deals.base import BaseDeal class Assets(BaseDeal): def __init__(self): self.db = setup_db() self.usd_balance = 0 - self.isolated_balance = None def get_raw_balance(self, asset=None): """ @@ -338,7 +337,7 @@ async def get_balance_series(self, end_date, start_date): ) return resp - async def clean_balance_assets(self): + def clean_balance_assets(self): """ Check if there are many small assets (0.000.. BTC) if there are more than 5 (number of bots) @@ -358,37 +357,42 @@ async def clean_balance_assets(self): return resp - async def disable_isolated_accounts(self): + def disable_isolated_accounts(self, symbol=None): """ Check and disable isolated accounts """ info = self.signed_request(url=self.isolated_account_url, payload={}) + msg = "Disabling isolated margin account not required yet." for item in info["assets"]: + # Liquidate price = 0 guarantees there is no loan unpaid if float(item["liquidatePrice"]) == 0: + if float(item["baseAsset"]["free"]) > 0: + self.transfer_isolated_margin_to_spot(asset=item["baseAsset"]["asset"], symbol=item["symbol"], amount=float(item["baseAsset"]["free"])) + + if float(item["quoteAsset"]["free"]) > 0: + self.transfer_isolated_margin_to_spot(asset=item["quoteAsset"]["asset"], symbol=item["symbol"], amount=float(item["quoteAsset"]["free"])) + self.disable_isolated_margin_account(item["symbol"]) msg = "Sucessfully finished disabling isolated margin accounts." - else: - msg = "Disabling isolated margin account not required yet." - resp = json_response_message(msg) - return resp + return json_response_message(msg) def one_click_liquidation(self, pair: str): """ Emulate Binance Dashboard One click liquidation function + + This endpoint is different than the margin_liquidation function + in that it contains some clean up functionality in the cases + where there are are still funds in the isolated pair """ - - try: - self.margin_liquidation(pair) + try: + self.symbol = pair + self.margin_liquidation(pair, self.qty_precision) + return json_response_message(f"Successfully liquidated {pair}") + except MarginLoanNotFound as error: + return json_response_message(f"{error}. Successfully cleared isolated pair {pair}") except BinanceErrors as error: - if error.code == -3041: - # Most likely not enough funds to pay back - # Get fiat (USDT) to pay back - return json_response_error(error.message) - if error.code == -3015: - # false alarm - pass - if error.code == -3051: - return json_response_error(error.message) + return json_response_error(f"Error liquidating {pair}: {error.message}") + diff --git a/api/account/routes.py b/api/account/routes.py index 2be11881e..627e4288a 100644 --- a/api/account/routes.py +++ b/api/account/routes.py @@ -81,13 +81,13 @@ async def get_balance_series(): return await Assets().get_balance_series(start_date=datetime.timestamp(month_ago), end_date=datetime.timestamp(today)) @account_blueprint.get("/clean", response_model=BalanceSeriesResponse, tags=["assets"]) -async def clean_balance(): - return await Assets().clean_balance_assets() +def clean_balance(): + return Assets().clean_balance_assets() @account_blueprint.get("/disable-isolated", response_model=BalanceSeriesResponse, tags=["assets"]) -async def disable_isolated(): - return await Assets().disable_isolated_accounts() +def disable_isolated(): + return Assets().disable_isolated_accounts() -@account_blueprint.get("/one-click-liquidation/{asset}", response_model=BalanceSeriesResponse, tags=["assets"]) +@account_blueprint.get("/one-click-liquidation/{asset}", tags=["assets"]) def one_click_liquidation(asset): return Assets().one_click_liquidation(asset) diff --git a/api/apis.py b/api/apis.py index 10ab667fd..322815c82 100644 --- a/api/apis.py +++ b/api/apis.py @@ -53,6 +53,7 @@ class BinanceApi: margin_repay_url = f"{BASE}/sapi/v1/margin/repay" isolated_hourly_interest = f"{BASE}/sapi/v1/margin/next-hourly-interest-rate" margin_order = f"{BASE}/sapi/v1/margin/order" + max_borrow_url = f"{BASE}/sapi/v1/margin/maxBorrowable" def signed_request(self, url, method="GET", payload={}, params={}): """ @@ -95,6 +96,12 @@ def enable_isolated_margin_account(self, symbol): return self.signed_request(self.isolated_account_url, method="POST", payload={"symbol": symbol}) def disable_isolated_margin_account(self, symbol): + """ + Very high weight, use as little as possible + + There is a cronjob that disables all margin isolated accounts everyday + check market_updates + """ return self.signed_request(self.isolated_account_url, method="DELETE", payload={"symbol": symbol}) def transfer_isolated_margin_to_spot(self, asset, symbol, amount): @@ -111,6 +118,9 @@ def create_margin_loan(self, asset, symbol, amount, isIsolated=True): return self.signed_request(self.loan_record_url, method="POST", payload={"asset": asset, "symbol": symbol, "amount": amount, "isIsolated": isIsolated}) + def get_max_borrow(self, asset, isolated_symbol:str | None = None): + return self.signed_request(self.max_borrow_url, payload={"asset": asset, "isolatedSymbol": isolated_symbol }) + def get_margin_loan_details(self, asset: str, isolatedSymbol: str): return self.signed_request(self.loan_record_url, payload={"asset": asset, "isolatedSymbol": isolatedSymbol}) diff --git a/api/bots/controllers.py b/api/bots/controllers.py index 34c38d4c5..193acfafc 100644 --- a/api/bots/controllers.py +++ b/api/bots/controllers.py @@ -6,10 +6,9 @@ from fastapi.exceptions import RequestValidationError from account.account import Account -from deals.margin import MarginShortError from deals.controllers import CreateDealController from tools.enum_definitions import BinbotEnums -from tools.exceptions import OpenDealError, NotEnoughFunds, QuantityTooLow, IsolateBalanceError +from tools.exceptions import BinanceErrors, BinbotErrors, QuantityTooLow from tools.handle_error import ( handle_binance_errors, json_response, @@ -117,6 +116,14 @@ def create(self, data): bot = data.dict() bot["id"] = str(ObjectId()) + # if bot["strategy"] == "margin_short": + # asset = bot["pair"].replace(bot["balance_to_use"], "") + # # price = self.matching_engine(bot["pair"], True) + # total = float(bot["base_order_size"]) + # check_max_borrow = self.get_max_borrow(asset, isolated_symbol=bot["pair"]) + # if total > check_max_borrow["borrowLimit"]: + # return json_response_error(f"Max margin account borrow limit reached") + self.db_collection.insert_one(bot) resp = json_response( { @@ -184,14 +191,11 @@ def activate(self, botId: str): bot, db_collection=self.db_collection.name ).open_deal() return json_response_message("Successfully activated bot!") - except OpenDealError as error: - return json_response_error(error.args[0]) - except NotEnoughFunds as e: - return json_response_error(e.args[0]) - except MarginShortError as error: - message = str("Unable to create margin_short bot: ".join(error.args)) - return json_response_error(message) - except IsolateBalanceError as error: + except BinanceErrors as error: + self.post_errors_by_id(botId, error.message) + return json_response_error(error.message) + except BinbotErrors as error: + self.post_errors_by_id(botId, error.message) return json_response_error(error.message) except Exception as error: resp = json_response_error(f"Unable to activate bot: {error}") @@ -355,3 +359,20 @@ def put_archive(self, botId): resp = json_response({"message": f"Failed to archive bot {error}"}) return resp + + def post_errors_by_id(self, bot_id: str, reported_error: str): + """ + Directly post errors to Bot + which should show in the BotForm page in Web + """ + try: + self.db_collection.update_one( + {"id": bot_id}, {"$push": {"errors": reported_error}} + ) + return json_response( + {"message": "Successfully submitted bot errors", "botId": bot_id} + ) + except Exception as error: + resp = json_response({"message": f"Failed to submit bot errors: {error}"}) + + return resp diff --git a/api/bots/routes.py b/api/bots/routes.py index 1f10812b3..db1efce08 100644 --- a/api/bots/routes.py +++ b/api/bots/routes.py @@ -63,3 +63,8 @@ def deactivate(id: str): @bot_blueprint.put("/bot/archive/{id}", tags=["bots"]) def archive(id: str): return Bot(collection_name="bots").put_archive(id) + + +@bot_blueprint.post("/bot/errors/{bot_id}", tags=["bots"]) +def bot_errors(bot_id: str, bot_errors: str): + return Bot(collection_name="bots").post_errors_by_id(bot_id, bot_errors) diff --git a/api/bots/schemas.py b/api/bots/schemas.py index 6aa81c8da..a5186912f 100644 --- a/api/bots/schemas.py +++ b/api/bots/schemas.py @@ -1,5 +1,5 @@ from time import time -from typing import Literal +from typing import List, Literal from bson.objectid import ObjectId from tools.enum_definitions import Status @@ -40,7 +40,7 @@ class BotSchema(BaseModel): created_at: float = time() * 1000 deal: DealSchema = Field(default_factory=DealSchema) dynamic_trailling: bool = False - errors: list[str] = [] + errors: list[str] = [] # Event logs locked_so_funds: float = 0 # funds locked by Safety orders mode: str = "manual" # Manual is triggered by the terminal dashboard, autotrade by research app name: str = "Default bot" @@ -90,12 +90,18 @@ def check_trailling(cls, v: str | bool): return False return True + @validator("errors") + def check_errors_format(cls, v: list[str]): + if isinstance(v, list): + return [] + return [] + class Config: use_enum_values = True arbitrary_types_allowed = True json_encoders = {ObjectId: str} schema_extra = { - "description": "Most fields are optional. Deal field is generated internally, orders are filled up by Binance and", + "description": "Most fields are optional. Deal field is generated internally, orders are filled up by Binance", "example": { "pair": "BNBUSDT", "balance_size_to_use": 0, diff --git a/api/charts/models.py b/api/charts/models.py index b7e693c01..97ae39ce6 100644 --- a/api/charts/models.py +++ b/api/charts/models.py @@ -315,6 +315,48 @@ def bollinguer_bands(self, df, dates): return ma_100, ma_25, ma_7 + def macd(self, df): + """ + Moving Average Convergence Divergence (MACD) indicator + https://www.alpharithms.com/calculate-macd-python-272222/ + """ + + k = df[4].ewm(span=12, adjust=False, min_periods=12).mean() + # Get the 12-day EMA of the closing price + d = df[4].ewm(span=26, adjust=False, min_periods=26).mean() + # Subtract the 26-day EMA from the 12-Day EMA to get the MACD + macd = k - d + # Get the 9-Day EMA of the MACD for the Trigger line + # Get the 9-Day EMA of the MACD for the Trigger line + macd_s = macd.ewm(span=9, adjust=False, min_periods=9).mean() + + return macd, macd_s + + def rsi(self, df): + """ + Relative Strength Index (RSI) indicator + https://www.qmr.ai/relative-strength-index-rsi-in-python/ + """ + + change = df[4].astype(float).diff() + change.dropna(inplace=True) + # Create two copies of the Closing price Series + change_up = change.copy() + change_down = change.copy() + + change_up[change_up<0] = 0 + change_down[change_down>0] = 0 + + # Verify that we did not make any mistakes + change.equals(change_up+change_down) + + # Calculate the rolling average of average up and average down + avg_up = change_up.rolling(14).mean() + avg_down = change_down.rolling(14).mean().abs() + + rsi = 100 * avg_up / (avg_up + avg_down) + return rsi + def get(self, symbol, interval="15m", limit=500, start_time: float | None=None, end_time: float | None=None, stats = False): """ Get candlestick graph data @@ -351,6 +393,8 @@ def get(self, symbol, interval="15m", limit=500, start_time: float | None=None, trace = self.candlestick_trace(df, dates) ma_100, ma_25, ma_7 = self.bollinguer_bands(df, dates) + macd, macd_signal = self.macd(df) + rsi = self.rsi(df) if stats: high_price = max(df[2]) @@ -372,18 +416,23 @@ def get(self, symbol, interval="15m", limit=500, start_time: float | None=None, close_price_r = df[4].astype(float).corr(btc_df[4].astype(float)) volume_r = df[5].astype(float).corr(btc_df[5].astype(float)) + close_price_cov = df[4].astype(float).cov(btc_df[4].astype(float)) + # collection of correlations p_btc = { "open_price": open_price_r, "high_price": high_price_r, "low_price": low_price_r, "close_price": close_price_r, - "volume": volume_r + "volume": volume_r, } resp = json_response( { "trace": [trace, ma_100, ma_25, ma_7], + "macd": macd, + "macd_signal": macd_signal, + "rsi": rsi, "interval": interval, "volumes": volumes, "amplitude": amplitude, diff --git a/api/cronjobs.py b/api/cronjobs.py new file mode 100644 index 000000000..71a54ac15 --- /dev/null +++ b/api/cronjobs.py @@ -0,0 +1,52 @@ +import atexit +import logging +import time +import asyncio + +from apscheduler.schedulers.blocking import BlockingScheduler +from account.assets import Assets + + +logging.Formatter.converter = time.gmtime # date time in GMT/UTC +logging.basicConfig( + level=logging.INFO, + filename=None, + format="%(asctime)s.%(msecs)03d UTC %(levelname)s %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) + +def main(): + + scheduler = BlockingScheduler() + assets = Assets() + timezone = "Europe/London" + + scheduler.add_job( + func=assets.store_balance, + trigger="cron", + timezone=timezone, + hour=1, + minute=1, + id="store_balance", + ) + scheduler.add_job( + func=assets.disable_isolated_accounts, + trigger="cron", + timezone=timezone, + hour=2, + minute=1, + id="disable_isolated_accounts", + ) + scheduler.add_job( + func=assets.clean_balance_assets, + trigger="cron", + timezone=timezone, + hour=3, + minute=1, + id="clean_balance_assets", + ) + scheduler.start() + + +if __name__ == "__main__": + main() diff --git a/api/deals/base.py b/api/deals/base.py index f5aad4d1c..4be1c9345 100644 --- a/api/deals/base.py +++ b/api/deals/base.py @@ -1,3 +1,4 @@ +from typing import List import uuid import requests import numpy @@ -10,9 +11,10 @@ from pymongo import ReturnDocument from tools.round_numbers import round_numbers, supress_notation from tools.handle_error import handle_binance_errors, encode_json -from tools.exceptions import QuantityTooLow +from tools.exceptions import BinanceErrors, MarginLoanNotFound from scipy.stats import linregress from tools.round_numbers import round_numbers_ceiling +from tools.enum_definitions import Status # To be removed one day when commission endpoint found that provides this value @@ -31,9 +33,8 @@ class BaseDeal(OrderController): def __init__(self, bot, db_collection_name): self.active_bot = BotSchema.parse_obj(bot) - self.isolated_balance = None - self.qty_precision = None - super().__init__() + self.symbol = self.active_bot.pair + super().__init__(symbol=self.active_bot.pair) self.db_collection = self.db[db_collection_name] def __repr__(self) -> str: @@ -45,6 +46,12 @@ def __repr__(self) -> str: def generate_id(self): return uuid.uuid4().hex + @property + def isolated_balance(self): + if not hasattr(self, "_isolated_balance"): + self._isolated_balance = self.get_isolated_balance(self.symbol) + return self._isolated_balance + def compute_qty(self, pair): """ Helper function to compute buy_price. @@ -59,7 +66,7 @@ def compute_qty(self, pair): return qty def compute_margin_buy_back( - self, pair: str, qty_precision + self, pair: str ): """ Same as compute_qty but with isolated margin balance @@ -69,9 +76,6 @@ def compute_margin_buy_back( Decimals have to be rounded up to avoid leaving "leftover" interests """ - if not self.isolated_balance: - self.isolated_balance = self.get_isolated_balance(pair) - if ( self.isolated_balance[0]["quoteAsset"]["free"] == 0 or self.isolated_balance[0]["baseAsset"]["borrowed"] == 0 @@ -80,12 +84,6 @@ def compute_margin_buy_back( qty = float(self.isolated_balance[0]["baseAsset"]["borrowed"]) + float(self.isolated_balance[0]["baseAsset"]["interest"]) + float(self.isolated_balance[0]["baseAsset"]["borrowed"]) * ESTIMATED_COMMISSIONS_RATE - # Save API calls - self.qty_precision = qty_precision - - if not self.qty_precision: - self.qty_precision = self.get_qty_precision(pair) - qty = round_numbers_ceiling(qty, self.qty_precision) free = float(self.isolated_balance[0]["baseAsset"]["free"]) @@ -195,7 +193,7 @@ def save_bot_streaming(self): if "_id" in bot: bot.pop("_id") - bot = self.db_collection.find_one_and_update( + response = self.db_collection.find_one_and_update( {"id": self.active_bot.id}, { "$set": bot, @@ -207,7 +205,32 @@ def save_bot_streaming(self): self.update_deal_logs(f"Failed to save bot during streaming updates: {error}") raise StreamingSaveError(error) - return bot + return response + + def create_new_bot_streaming(self): + """ + MongoDB query to save bot using Pydantic + + This function differs from usual save query in that + it returns the saved bot, thus called streaming, it's + specifically for streaming saves + """ + + try: + + bot = encode_json(self.active_bot) + if "_id" in bot: + bot.pop("_id") + + bot_response = self.db_collection.insert_one( + bot, + ) + + except Exception as error: + self.update_deal_logs(f"Failed to save bot during streaming updates: {error}") + raise StreamingSaveError(error) + + return bot_response.inserted_id def dynamic_take_profit(self, current_bot, close_price): @@ -282,67 +305,97 @@ def dynamic_take_profit(self, current_bot, close_price): def margin_liquidation(self, pair: str, qty_precision=None): """ - Emulate Binance Dashboard - One click liquidation function + Emulate Binance Dashboard One click liquidation function + + Args: + - pair: a.k.a symbol, quote asset + base asset + - qty_precision: to round numbers for Binance API. Passed optionally to + reduce number of requests to avoid rate limit. """ - isolated_balance = self.get_isolated_balance(pair) + isolated_balance = self.isolated_balance base = isolated_balance[0]["baseAsset"]["asset"] quote = isolated_balance[0]["quoteAsset"]["asset"] # Check margin account balance first - balance = float(isolated_balance[0]["quoteAsset"]["free"]) - borrowed_amount = float(isolated_balance[0]["baseAsset"]["free"]) + borrowed_amount = float(isolated_balance[0]["baseAsset"]["borrowed"]) + free = float(isolated_balance[0]["baseAsset"]["free"]) buy_margin_response = None - if not qty_precision: - qty_precision = self.get_qty_precision(pair) if borrowed_amount > 0: - # repay - repay_amount, free = self.compute_margin_buy_back(pair, qty_precision) - if repay_amount == 0 or not repay_amount: - raise QuantityTooLow(f"Liquidation amount {repay_amount}") - # Check if there is a loan - # Binance may reject loans if they don't have asset - # or binbot errors may transfer funds but no loan is created - query_loan = self.signed_request( - url=self.loan_record_url, - payload={"asset": base, "isolatedSymbol": pair}, - ) - if query_loan["total"] > 0 and repay_amount > 0: - # Only supress trailling 0s, so that everything is paid - repay_amount = round_numbers_ceiling(repay_amount, qty_precision) - - if free == 0 or free <= repay_amount: + # repay_amount contains total borrowed_amount + interests + commissions for buying back + # borrow amount is only the loan + repay_amount, free = self.compute_margin_buy_back(pair) + repay_amount = round_numbers_ceiling(repay_amount, qty_precision) + + if free == 0 or free < repay_amount: + try: + # lot_size_by_symbol = self.lot_size_by_symbol(pair, "stepSize") + qty = round_numbers_ceiling(repay_amount - free, qty_precision) buy_margin_response = self.buy_margin_order( symbol=pair, - qty=supress_notation(repay_amount, qty_precision), + qty=qty, ) - repay_amount = float(buy_margin_response["origQty"]) + repay_amount, free = self.compute_margin_buy_back(pair) + except BinanceErrors as error: + if error.code == -3041: + # Not enough funds in isolated pair + # transfer from wallet + transfer_diff_qty = round_numbers_ceiling(repay_amount - free) + available_balance = self.get_one_balance(quote) + amount_to_transfer = 15 # Min amount + if available_balance < 15: + amount_to_transfer = available_balance + self.transfer_spot_to_isolated_margin( + asset=quote, + symbol=pair, + amount=amount_to_transfer, + ) + buy_margin_response = self.buy_margin_order(pair, supress_notation(transfer_diff_qty, qty_precision)) + repay_amount, free = self.compute_margin_buy_back(pair) + pass + if error.code == -2010 or error.code == -1013: + # There is already money in the base asset + qty = round_numbers_ceiling(repay_amount - free, qty_precision) + price = float(self.matching_engine(pair, True, qty)) + usdt_notional = price * qty + if usdt_notional < 15: + qty = round_numbers_ceiling(15 / price) + + buy_margin_response = self.buy_margin_order(pair, supress_notation(qty, qty_precision)) + repay_amount, free = self.compute_margin_buy_back(pair) + pass + + self.repay_margin_loan( + asset=base, + symbol=pair, + amount=repay_amount, + isIsolated="TRUE", + ) - if free >= repay_amount: - self.repay_margin_loan( - asset=base, - symbol=pair, - amount=repay_amount, - isIsolated="TRUE", - ) + # get new balance + isolated_balance = self.get_isolated_balance(pair) - # get new balance - isolated_balance = self.get_isolated_balance(pair) - logging.info(f"Transfering leftover isolated assets back to Spot") - if float(isolated_balance[0]["quoteAsset"]["free"]) != 0: - # transfer back to SPOT account - self.transfer_isolated_margin_to_spot( - asset=quote, - symbol=pair, - amount=isolated_balance[0]["quoteAsset"]["free"], - ) - if float(isolated_balance[0]["baseAsset"]["free"]) != 0: - self.transfer_isolated_margin_to_spot( - asset=base, - symbol=pair, - amount=isolated_balance[0]["baseAsset"]["free"], - ) - - self.disable_isolated_margin_account(symbol=pair) - return buy_margin_response + if float(isolated_balance[0]["quoteAsset"]["free"]) != 0: + # transfer back to SPOT account + self.transfer_isolated_margin_to_spot( + asset=quote, + symbol=pair, + amount=isolated_balance[0]["quoteAsset"]["free"], + ) + if float(isolated_balance[0]["baseAsset"]["free"]) != 0: + self.transfer_isolated_margin_to_spot( + asset=base, + symbol=pair, + amount=isolated_balance[0]["baseAsset"]["free"], + ) + + if borrowed_amount == 0: + # Funds are transferred back by now, + # disabling pair should be done by cronjob, + # therefore no reason not to complete the bot + if hasattr(self, "active_bot"): + self.active_bot.status = Status.completed + + raise MarginLoanNotFound("Isolated margin loan already liquidated") + + return buy_margin_response diff --git a/api/deals/controllers.py b/api/deals/controllers.py index 84edec953..3c0e34a34 100644 --- a/api/deals/controllers.py +++ b/api/deals/controllers.py @@ -8,7 +8,6 @@ from pymongo import ReturnDocument from tools.enum_definitions import Status from tools.exceptions import ( - ShortStrategyError, TakeProfitError, ) from tools.handle_error import ( @@ -38,7 +37,6 @@ class CreateDealController(BaseDeal): def __init__(self, bot, db_collection="paper_trading"): # Inherit from parent class super().__init__(bot, db_collection) - self.active_bot = BotSchema.parse_obj(bot) def get_one_balance(self, symbol="BTC"): # Response after request @@ -90,7 +88,11 @@ def base_order(self): pair, supress_notation(price, self.price_precision), qty, "BUY" ) else: - res = self.buy_order(symbol=pair, qty=qty, price=supress_notation(price, self.price_precision)) + res = self.buy_order( + symbol=pair, + qty=qty, + price=supress_notation(price, self.price_precision), + ) order_data = OrderSchema( timestamp=res["transactTime"], @@ -280,7 +282,11 @@ def update_take_profit(self, order_id): print("Old take profit order cancelled") qty = round_numbers(self.get_one_balance(asset), self.qty_precision) - res = self.sell_order(symbol=self.active_bot.pair, qty=qty, price=supress_notation(new_tp_price, self.price_precision)) + res = self.sell_order( + symbol=self.active_bot.pair, + qty=qty, + price=supress_notation(new_tp_price, self.price_precision), + ) # New take profit order successfully created order = handle_binance_errors(res) @@ -320,25 +326,12 @@ def update_take_profit(self, order_id): else: self.update_deal_logs("Error: Bot does not contain a base order deal") - def open_deal(self): """ Mandatory deals section - If base order deal is not executed, bot is not activated """ - # Short strategy checks - if self.active_bot.strategy == "short": - if ( - not hasattr(self.active_bot, "short_buy_price") - or float(self.active_bot.short_buy_price) == 0 - ): - raise ShortStrategyError( - "Short strategy requires short_buy_price to be set, or it will never trigger" - ) - else: - pass - # If there is already a base order do not execute base_order_deal = next( ( @@ -394,13 +387,10 @@ def open_deal(self): # Keep trailling_stop_loss_price up to date in case of failure to update in autotrade # if we don't do this, the trailling stop loss will trigger - if ( - self.active_bot.deal - and ( - self.active_bot.deal.trailling_stop_loss_price > 0 - or self.active_bot.deal.trailling_stop_loss_price - < self.active_bot.deal.buy_price - ) + if self.active_bot.deal and ( + self.active_bot.deal.trailling_stop_loss_price > 0 + or self.active_bot.deal.trailling_stop_loss_price + < self.active_bot.deal.buy_price ): take_profit_price = float(self.active_bot.deal.buy_price) * ( 1 + (float(self.active_bot.take_profit) / 100) diff --git a/api/deals/margin.py b/api/deals/margin.py index a2af24011..71d8fddc4 100644 --- a/api/deals/margin.py +++ b/api/deals/margin.py @@ -10,7 +10,7 @@ from deals.base import BaseDeal from deals.schema import MarginOrderSchema from pydantic import ValidationError -from tools.exceptions import QuantityTooLow, BinanceErrors +from tools.exceptions import BinbotErrors, MarginLoanNotFound, QuantityTooLow, BinanceErrors from tools.round_numbers import round_numbers, supress_notation, round_numbers_ceiling @@ -22,7 +22,6 @@ class MarginDeal(BaseDeal): def __init__(self, bot, db_collection_name: str) -> None: # Inherit from parent class super().__init__(bot, db_collection_name) - self.isolated_balance = self.get_isolated_balance(self.active_bot.pair) def _append_errors(self, error): """ @@ -124,18 +123,6 @@ def terminate_failed_transactions(self): symbol=self.active_bot.pair, amount=qty, ) - try: - self.disable_isolated_margin_account(symbol=self.active_bot.pair) - except BinanceErrors as error: - if error.code == -1003: - self._append_errors( - "Isolated margin account can't be disabled within 24hrs, please disable manually" - ) - all_errors = ". ".join(self.active_bot.errors) - # save error so it's available in the bot logs - self.save_bot_streaming() - # raise error so that it returns to the JSON response - raise MarginShortError(all_errors) def init_margin_short(self, initial_price): """ @@ -153,7 +140,7 @@ def init_margin_short(self, initial_price): try: self.enable_isolated_margin_account(symbol=self.active_bot.pair) except BinanceErrors as error: - if error.code == -11001: + if error.code == -11001 or error.code == -3052: # Isolated margin account needs to be activated with a transfer self.transfer_spot_to_isolated_margin( asset=self.active_bot.balance_to_use, @@ -161,24 +148,14 @@ def init_margin_short(self, initial_price): amount="1", ) + transfer_qty = float(self.active_bot.base_order_size) + # Given USDT amount we want to buy, # how much can we buy? qty = round_numbers_ceiling( (float(self.active_bot.base_order_size) / float(initial_price)), self.qty_precision, ) - if qty == 0: - raise QuantityTooLow("Margin short quantity is too low") - - # transfer quantity is base order size (what we want to invest) + stop loss to cover losses - stop_loss_price_inc = float(initial_price) * ( - 1 + (self.active_bot.stop_loss / 100) - ) - final_qty = float(stop_loss_price_inc * qty) - transfer_qty = round_numbers_ceiling( - final_qty, - self.qty_precision, - ) # For leftover values # or transfers to activate isolated pair @@ -200,42 +177,32 @@ def init_margin_short(self, initial_price): raise MarginShortError("Isolated margin not available") asset = self.active_bot.pair.replace(self.active_bot.balance_to_use, "") - try: - self.create_margin_loan( - asset=asset, symbol=self.active_bot.pair, amount=qty - ) - loan_details = self.get_margin_loan_details( - asset=asset, isolatedSymbol=self.active_bot.pair - ) + self.create_margin_loan( + asset=asset, symbol=self.active_bot.pair, amount=qty + ) + loan_details = self.get_margin_loan_details( + asset=asset, isolatedSymbol=self.active_bot.pair + ) - self.active_bot.deal.margin_short_loan_timestamp = loan_details["rows"][0][ - "timestamp" - ] - self.active_bot.deal.margin_short_loan_principal = loan_details["rows"][0][ - "principal" - ] - self.active_bot.deal.margin_loan_id = loan_details["rows"][0]["txId"] - self.active_bot.deal.margin_short_base_order = qty - - # Estimate interest to add to total cost - asset = self.active_bot.pair.replace(self.active_bot.balance_to_use, "") - # This interest rate is much more accurate than any of the others - hourly_fees = self.signed_request( - url=self.isolated_hourly_interest, - payload={"assets": asset, "isIsolated": "TRUE"}, - ) - self.active_bot.deal.hourly_interest_rate = float( - hourly_fees[0]["nextHourlyInterestRate"] - ) + self.active_bot.deal.margin_short_loan_timestamp = loan_details["rows"][0][ + "timestamp" + ] + self.active_bot.deal.margin_short_loan_principal = loan_details["rows"][0][ + "principal" + ] + self.active_bot.deal.margin_loan_id = loan_details["rows"][0]["txId"] + self.active_bot.deal.margin_short_base_order = qty - except BinanceErrors as error: - if error.args[1] == -3045: - msg = "Binance doesn't have any money to lend" - self._append_errors(msg) - self.terminate_failed_transactions() - raise MarginShortError(msg) - except Exception as error: - logging.error(error) + # Estimate interest to add to total cost + asset = self.active_bot.pair.replace(self.active_bot.balance_to_use, "") + # This interest rate is much more accurate than any of the others + hourly_fees = self.signed_request( + url=self.isolated_hourly_interest, + payload={"assets": asset, "isIsolated": "TRUE"}, + ) + self.active_bot.deal.hourly_interest_rate = float( + hourly_fees[0]["nextHourlyInterestRate"] + ) return @@ -381,18 +348,6 @@ def terminate_margin_short(self, buy_back_fiat: bool = True): self.active_bot.errors.append(error_msg) return - # Disable isolated pair to avoid reaching the 15 pair limit - # this is not always possible, sometimes there are small quantities - # that can't be cleaned out completely, need to do it manually - # this is ok, since this is not a hard requirement to complete the deal - try: - self.disable_isolated_margin_account(symbol=self.active_bot.pair) - except BinanceErrors as error: - logging.error(error) - if error.code == -3051: - self._append_errors(error.message) - pass - completion_msg = f"{self.active_bot.pair} ISOLATED margin funds transferred back to SPOT." self.active_bot.status = Status.completed self.active_bot.errors.append(completion_msg) @@ -415,10 +370,15 @@ def margin_short_base_order(self): if self.db_collection.name == "bots": self.init_margin_short(initial_price) - order_res = self.sell_margin_order( - symbol=self.active_bot.pair, - qty=self.active_bot.deal.margin_short_base_order, - ) + try: + order_res = self.sell_margin_order( + symbol=self.active_bot.pair, + qty=self.active_bot.deal.margin_short_base_order, + ) + except BinanceErrors as error: + if error.code == -3052: + print(error) + return else: # Simulate Margin sell # qty doesn't matter in paper bots @@ -525,7 +485,15 @@ def streaming_updates(self, close_price: str): logging.info( f"Executing margin_short stop_loss reversal after hitting stop_loss_price {self.active_bot.deal.stop_loss_price}" ) - self.execute_stop_loss() + try: + self.execute_stop_loss() + except MarginLoanNotFound as error: + self.update_deal_logs( + f"{error.message}" + ) + self.active_bot.status = Status.error + return + if ( hasattr(self.active_bot, "margin_short_reversal") and self.active_bot.margin_short_reversal @@ -601,14 +569,6 @@ def execute_stop_loss(self): self.update_deal_logs(error.message) return - except Exception as error: - self.update_deal_logs( - f"Error trying to open new stop_limit order {error}" - ) - # Continue in case of error to execute long bot - # to avoid losses - return - stop_loss_order = MarginOrderSchema( timestamp=res["transactTime"], deal_type="stop_loss", @@ -652,9 +612,7 @@ def execute_take_profit(self, price=None): - Buy back asset sold """ - qty = 1 if self.db_collection.name == "bots": - qty, free = self.compute_margin_buy_back(self.active_bot.pair, self.qty_precision) self.cancel_open_orders("take_profit") # Margin buy (buy back) @@ -724,6 +682,9 @@ def switch_to_long_bot(self, current_price): 3. Create deal """ self.update_deal_logs("Resetting bot for long strategy...") + new_id = self.create_new_bot_streaming() + self.active_bot.id = new_id + # Reset bot to prepare for new activation base_order = next( ( diff --git a/api/market_updates.py b/api/market_updates.py index be8448cc1..8eaf5bbc1 100644 --- a/api/market_updates.py +++ b/api/market_updates.py @@ -3,13 +3,10 @@ import logging import time -from apscheduler.schedulers.background import BlockingScheduler +from apscheduler.schedulers.background import BackgroundScheduler from streaming.streaming_controller import StreamingController from account.assets import Assets -from websocket import ( - WebSocketException, - WebSocketConnectionClosedException, -) +from websocket import WebSocketConnectionClosedException logging.Formatter.converter = time.gmtime # date time in GMT/UTC @@ -20,56 +17,17 @@ datefmt="%Y-%m-%d %H:%M:%S", ) -if os.getenv("ENV") != "ci": - - scheduler = BlockingScheduler() - assets = Assets() - - scheduler.add_job( - func=assets.store_balance, - trigger="cron", - timezone="Europe/London", - hour=1, - minute=1, - id="store_balance", - ) - - scheduler.add_job( - func=assets.disable_isolated_accounts, - trigger="cron", - timezone="Europe/London", - hour=2, - minute=1, - id="disable_isolated_accounts", - ) - - scheduler.add_job( - func=assets.clean_balance_assets, - trigger="cron", - timezone="Europe/London", - hour=3, - minute=1, - id="clean_balance_assets", - ) - try: + mu = StreamingController() mu.get_klines() - scheduler.start() -except WebSocketException as e: - if isinstance(e, WebSocketConnectionClosedException): - logging.error("Lost websocket connection") - mu = StreamingController() - mu.get_klines() - else: - logging.error(f"Websocket exception: {e}") - - atexit.register(lambda: scheduler.shutdown(wait=False)) +except WebSocketConnectionClosedException as e: + logging.error("Lost websocket connection") + mu = StreamingController() + mu.get_klines() except Exception as error: logging.error(f"Streaming controller error: {error}") mu = StreamingController() mu.get_klines() - - atexit.register(lambda: scheduler.shutdown(wait=False)) diff --git a/api/orders/controller.py b/api/orders/controller.py index 7e56573d7..f0e049e1a 100644 --- a/api/orders/controller.py +++ b/api/orders/controller.py @@ -8,22 +8,31 @@ poll_percentage = 0 class OrderController(Account): - def __init__(self) -> None: + def __init__(self, symbol) -> None: super().__init__() # Always GTC and limit orders # limit/market orders will be decided by matching_engine # PRICE_FILTER decimals - self.price_precision = -1 * ( - Decimal(str(self.price_filter_by_symbol(self.active_bot.pair, "tickSize"))) + self.symbol = symbol + pass + + @property + def price_precision(self): + self._price_precision = -1 * ( + Decimal(str(self.price_filter_by_symbol(self.symbol, "tickSize"))) .as_tuple() .exponent ) - self.qty_precision = -1 * ( - Decimal(str(self.lot_size_by_symbol(self.active_bot.pair, "stepSize"))) + return self._price_precision + + @property + def qty_precision(self): + self._qty_precision = -1 * ( + Decimal(str(self.lot_size_by_symbol(self.symbol, "stepSize"))) .as_tuple() .exponent ) - pass + return self._qty_precision def sell_order(self, symbol, qty, price=None): """ diff --git a/api/orders/routes.py b/api/orders/routes.py index d6e88b7a6..1aac54204 100644 --- a/api/orders/routes.py +++ b/api/orders/routes.py @@ -8,20 +8,26 @@ @order_blueprint.post("/buy", tags=["orders"]) def create_buy_order(item: OrderParams): - return OrderController().buy_order(item) + return OrderController(symbol=item.pair).buy_order(item) @order_blueprint.post("/sell", tags=["orders"]) def create_sell_order(item: OrderParams): - return OrderController().sell_order(item) + return OrderController(symbol=item.pair).sell_order(item) @order_blueprint.get("/open", tags=["orders"]) def get_open_orders(): - return OrderController().get_open_orders() + # Symbol not required + return OrderController(symbol=None).get_open_orders() @order_blueprint.delete("/close/{symbol}/{orderid}", tags=["orders"]) def delete_order(symbol, orderid): - return OrderController().delete_order(symbol, orderid) + return OrderController(symbol=symbol).delete_order(symbol, orderid) + + +@order_blueprint.get("/margin/sell/{symbol}/{qty}", tags=["orders"]) +def margin_sell(symbol, qty): + return OrderController(symbol=symbol).sell_margin_order(symbol, qty) \ No newline at end of file diff --git a/api/research/controller.py b/api/research/controller.py index e03520c37..a85982940 100644 --- a/api/research/controller.py +++ b/api/research/controller.py @@ -1,11 +1,12 @@ from db import setup_db from datetime import datetime from time import sleep -from tools.handle_error import json_response, json_response_error +from tools.handle_error import json_response, json_response_error, json_response_message from pymongo.errors import DuplicateKeyError from apis import ThreeCommasApi from tools.round_numbers import round_numbers from pymongo import ASCENDING +from fastapi.encoders import jsonable_encoder class Controller: """ @@ -134,4 +135,50 @@ def get_3commas_signals(self): return json_response({"message": "Successfully retrieved profitable 3commas signals", "data": signals}) + """ + Get pairs that binbot-research signals are subscribed to + receive cryptodata + + To merge with blacklist + """ + def get_subscribed_symbols(self): + query_result = self.db.subscribed_symbols.find({}).sort("pair", ASCENDING) + all_symbols = list(query_result) + return json_response( + {"message": "Successfully retrieved blacklist", "data": all_symbols} + ) + + def delete_all_subscribed_symbols(self): + query_result = self.db.subscribed_symbols.delete_many({}) + + return json_response( + {"message": "Successfully deleted all symbols", "data": { + "total": 0 + }} + ) + def bulk_upsert_all(self, data): + symbols = jsonable_encoder(data) + self.db.subscribed_symbols.delete_many({}) + try: + query_result = self.db.subscribed_symbols.insert_many( + symbols, + ) + return json_response( + {"message": "Successfully created new susbcribed list", "data": { + "total": len(query_result.inserted_ids) + 1 + }} + ) + except Exception as error: + return json_response_error(f"Failed to update symbol in the subscribed list {error}") + + def edit_subscribed_symbol(self, symbol): + symbol = jsonable_encoder(symbol) + try: + self.db.subscribed_symbols.update_one( + symbol, + upsert=True, + ) + return json_response_message("Successfully update symbol in the subscribed list") + except Exception as error: + return json_response_error(f"Failed to update symbol in the subscribed list {error}") \ No newline at end of file diff --git a/api/research/routes.py b/api/research/routes.py index 2bf8269db..ec1b11948 100644 --- a/api/research/routes.py +++ b/api/research/routes.py @@ -2,7 +2,7 @@ from apis import ThreeCommasApi from research.controller import Controller -from research.schemas import BlacklistSchema, BlacklistResponse +from research.schemas import BlacklistSchema, BlacklistResponse, SubscribedSymbolsSchema research_blueprint = APIRouter() @@ -42,3 +42,17 @@ def get_blacklisted(): @research_blueprint.get("/3commas-presets", tags=["blacklist and research"]) def three_commas_presets(): return ThreeCommasApi().get_marketplace_presets() + + +@research_blueprint.get("/subscribed", tags=["blacklist and research"]) +def get_subscribed_symbols(): + return Controller().get_subscribed_symbols() + + +@research_blueprint.post("/subscribed", tags=["blacklist and research"]) +def create_subscribed_symbols(data: list[SubscribedSymbolsSchema]): + return Controller().bulk_upsert_all(data) + +@research_blueprint.put("/subscribed/{symbol}", tags=["blacklist and research"]) +def edit_subscribed_symbol(symbol: str): + return Controller().edit_subscribed_symbol(symbol) diff --git a/api/research/schemas.py b/api/research/schemas.py index bb603dc22..bc1428392 100644 --- a/api/research/schemas.py +++ b/api/research/schemas.py @@ -16,3 +16,28 @@ class Config: class BlacklistResponse(StandardResponse): data: list[BlacklistSchema] + + +""" +Database control for symbols that are used +in signals. +""" +class SubscribedSymbolsSchema(BaseModel): + _id: str + pair: str + blacklisted: bool = False + blacklisted_reason: str = "" + + class Config: + schema_extra = { + "example": { + "_id": "BNBBTC", + "pair": "BNBBTC", + "blacklisted": False, + "blacklisted_reason": "Overtraded" + } + } + + +class SubscribedSymbolsResponse(StandardResponse): + data: list[SubscribedSymbolsSchema] diff --git a/api/tools/exceptions.py b/api/tools/exceptions.py index 825855976..71007cb15 100644 --- a/api/tools/exceptions.py +++ b/api/tools/exceptions.py @@ -23,10 +23,16 @@ class NotEnoughFunds(BinanceErrors): class BinbotErrors(Exception): - pass + def __init__(self, msg, code=None): + self.message = msg + super().__init__(self.message) + return None + + def __str__(self) -> str: + return f"Binbot error: {self.message}" -class QuantityTooLow(BinanceErrors): +class QuantityTooLow(BinbotErrors): """ Raised when LOT_SIZE filter error triggers This error should happen in the least cases, @@ -37,6 +43,10 @@ class QuantityTooLow(BinanceErrors): pass +class MarginLoanNotFound(BinbotErrors): + pass + + class OpenDealError(Exception): pass @@ -61,7 +71,6 @@ class ShortStrategyError(OpenDealError): pass - class TerminateStreaming(Exception): """ This is required sometimes @@ -72,4 +81,5 @@ class TerminateStreaming(Exception): On the other hand, we want to minimize number of times this exception is raised to avoid overloading the server with reloads """ + pass diff --git a/api/tools/handle_error.py b/api/tools/handle_error.py index 2142cfac9..1a7cafd94 100644 --- a/api/tools/handle_error.py +++ b/api/tools/handle_error.py @@ -9,8 +9,13 @@ from requests.exceptions import HTTPError from fastapi.encoders import jsonable_encoder from copy import deepcopy -from tools.exceptions import BinanceErrors, BinbotErrors, InvalidSymbol, NotEnoughFunds, QuantityTooLow - +from tools.exceptions import ( + BinanceErrors, + BinbotErrors, + InvalidSymbol, + NotEnoughFunds, + QuantityTooLow, +) def post_error(msg): @@ -21,7 +26,7 @@ def post_error(msg): def json_response(content, status=200): - content = json.loads(json_util.dumps(content)) # Objectid serialization + content = json.loads(json_util.dumps(content)) # Objectid serialization response = JSONResponse( status_code=status, content=content, @@ -40,7 +45,7 @@ def json_response_error(message): return json_response(body) -def handle_binance_errors(response: Response) -> str | None: +def handle_binance_errors(response: Response) -> Response: """ Handles: - HTTP codes, not authorized, rate limits... @@ -98,8 +103,8 @@ def handle_binance_errors(response: Response) -> str | None: sleep(60) if content["code"] == -1121: - raise InvalidSymbol(f'Binance error: {content["msg"]}', content["msg"]) - + raise InvalidSymbol(f'Binance error: {content["msg"]}', content["code"]) + return content @@ -113,9 +118,9 @@ def encode_json(raw): content = deepcopy(raw) del content._id content = jsonable_encoder(content) - content["_id"]= id + content["_id"] = id else: - content = jsonable_encoder(raw) + content = jsonable_encoder(raw) return content diff --git a/docker-compose.yml b/docker-compose.yml index 0340f36f8..1b285e627 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,5 +40,19 @@ services: # - api # - db + + # crobjobs: + # build: + # context: . + # dockerfile: Dockerfile.cronjobs + # image: binbot_cronjobs + # env_file: + # - .env + # restart: on-failure + # container_name: binbot_cronjobs + # depends_on: + # # - api + # - db + volumes: mongo_data: diff --git a/web/.env b/web/.env index af98ee824..190bda841 100644 --- a/web/.env +++ b/web/.env @@ -1,3 +1,4 @@ +TSC_COMPILE_ON_ERROR=true REACT_APP_TITLE=Binbot REACT_APP_DECIMALS=8 REACT_APP_REGISTRATION=/user/register @@ -29,6 +30,7 @@ REACT_APP_TICKER=/account/ticker REACT_APP_TICKER_24="https://api.binance.com/api/v3/ticker/24hr" REACT_APP_RESEARCH_BLACKLIST=/research/blacklist REACT_APP_RESEARCH_CONTROLLER=/autotrade-settings/bots +REACT_APP_SUBSCRIBED="/research/subscribed" REACT_APP_NO_CANNIBALISM_SYMBOLS=/account/symbols/no-cannibal REACT_APP_BINANCE_INFO="https://api.binance.com/api/v3/exchangeInfo" REACT_APP_HEDGE_GBP=/account/hedge-gbp diff --git a/web/package.json b/web/package.json index cea499197..922aa7728 100644 --- a/web/package.json +++ b/web/package.json @@ -42,9 +42,18 @@ "@testing-library/jest-dom": "^5.15.1", "@testing-library/react": "^12.1.1", "@testing-library/user-event": "^14.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^20.8.6", + "@types/react": "^18.2.28", + "@types/react-dom": "^18.2.13", + "@types/react-router-dom": "^5.3.3", + "@types/redux": "^3.6.0", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", "prettier": "^2.4.3", "react-scripts": "^5.0.1", - "sass": "^1.55.0" + "sass": "^1.55.0", + "typescript": "^5.2.2" }, "eslintConfig": { "extends": [ diff --git a/web/src/components/LogsInfo.jsx b/web/src/components/LogsInfo.jsx index c17d4ddce..eca157c5b 100644 --- a/web/src/components/LogsInfo.jsx +++ b/web/src/components/LogsInfo.jsx @@ -1,14 +1,14 @@ import React from "react"; import { Card, CardBody, CardHeader, CardTitle } from "reactstrap"; -export default function LogInfo({ info }) { +export default function LogInfo({ events }) { return ( - Logs + Event logs - {info.map((item, i) => ( + {events.map((item, i) => (

{item}


diff --git a/web/src/pages/bots/BotForm.jsx b/web/src/pages/bots/BotForm.jsx index 147bbfb8d..2c512d0da 100644 --- a/web/src/pages/bots/BotForm.jsx +++ b/web/src/pages/bots/BotForm.jsx @@ -555,11 +555,7 @@ class BotForm extends React.Component { - {this.props.bot.errors.length > 0 && ( - - - - )} + ) : ( "" @@ -754,6 +750,9 @@ class BotForm extends React.Component { {this.props.balance_estimate && ( )} + {this.props.bot.errors?.length > 0 && ( + + )} diff --git a/web/src/pages/research/ControllerTab.jsx b/web/src/pages/research/ControllerTab.jsx index 4663a418d..5da737534 100644 --- a/web/src/pages/research/ControllerTab.jsx +++ b/web/src/pages/research/ControllerTab.jsx @@ -21,6 +21,7 @@ export const ControllerTab = ({ addToBlacklist, removeFromBlacklist, triggerGbpHedge, + subscribedSymbols, }) => { const [addBlacklist, setAddBlacklist] = useImmer({ reason: "", pair: "" }); const [removeBlacklist, setRemoveBlacklist] = useState(""); @@ -124,6 +125,29 @@ export const ControllerTab = ({ {" "} +
+

Subscribed

+ + {subscribedSymbols && subscribedSymbols.length > 0 && ( + + + + + + {subscribedSymbols.map((x, i) => ( + + ))} + + + + )} + diff --git a/web/src/pages/research/Research.jsx b/web/src/pages/research/Research.jsx index 80505cdd8..855bf8d43 100644 --- a/web/src/pages/research/Research.jsx +++ b/web/src/pages/research/Research.jsx @@ -5,6 +5,7 @@ import { Nav, NavItem, NavLink, TabContent, TabPane } from "reactstrap"; import { addNotification, checkValue } from "../../validations"; import { loadCandlestick, getSymbols } from "../bots/actions"; import { getBalanceRaw } from "../../state/balances/actions"; +import { getSubscribedListApi } from "./actions"; import { getBlacklist, addBlackList, @@ -31,10 +32,12 @@ class Research extends React.Component { }; } - componentDidMount = () => { + componentDidMount = async () => { this.props.getBlacklist(); this.props.getSymbols(); this.props.getBalanceRaw(); + const subscribedList = await getSubscribedListApi(); + this.setState({ subscribedList: subscribedList.data }) }; componentDidUpdate = (p, s) => { @@ -105,6 +108,7 @@ class Research extends React.Component { { this.props.addBlackList(data); @@ -136,6 +140,7 @@ const mapStateToProps = (state) => { symbols: symbols, blacklistData: blacklistData, balance_raw: balanceRaw, + }; }; @@ -145,5 +150,5 @@ export default connect(mapStateToProps, { getBlacklist, addBlackList, deleteBlackList, - getBalanceRaw + getBalanceRaw, })(Research); diff --git a/web/src/pages/research/actions.js b/web/src/pages/research/actions.js index 08a19d648..3212ff08d 100644 --- a/web/src/pages/research/actions.js +++ b/web/src/pages/research/actions.js @@ -1,3 +1,4 @@ +import request from "../../request"; import { addNotification } from "../../validations"; export const GET_RESEARCH = "GET_RESEARCH"; @@ -129,3 +130,11 @@ export function deleteBlackListFailed() { type: DELETE_BLACKLIST_ERROR } } + +/** + * Websocket subscribed list of cryptos + */ +export async function getSubscribedListApi() { + const symbols = request(process.env.REACT_APP_SUBSCRIBED) + return symbols; +} diff --git a/web/src/pages/research/saga.js b/web/src/pages/research/saga.js index 733d0410c..e98d1ad4c 100644 --- a/web/src/pages/research/saga.js +++ b/web/src/pages/research/saga.js @@ -101,3 +101,4 @@ export function* deleteBlacklistApi({ pair }) { export function* watchDeleteBlackListApi() { yield takeLatest(DELETE_BLACKLIST, deleteBlacklistApi); } + diff --git a/web/src/state/bots/actions.js b/web/src/state/bots/actions.js index 7607c6d79..df0738668 100644 --- a/web/src/state/bots/actions.js +++ b/web/src/state/bots/actions.js @@ -20,6 +20,7 @@ export const bot = { baseOrderSizeError: false, balance_to_use: "USDT", bot_profit: 0, + errors: [], mode: "manual", max_so_count: "0", maxSOCountError: false, diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 000000000..7347e2f47 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,66 @@ +{ + "compilerOptions": { + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "jsx": "preserve", /* Specify what JSX code is generated. */ + "module": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + + /* Emit */ + "declaration": false, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + + /* Interop Constraints */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": false /* Enable all strict type-checking options. */, + "alwaysStrict": false, /* Ensure 'use strict' is always emitted. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/web/yarn.lock b/web/yarn.lock index 9d5bae308..050c14ebe 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1199,6 +1199,18 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308" integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw== +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.9.1.tgz#449dfa81a57a1d755b09aa58d826c1262e4283b4" + integrity sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA== + "@eslint/eslintrc@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.0.tgz#943309d8697c52fc82c076e90c1c74fbbe69dbff" @@ -2154,6 +2166,11 @@ dependencies: "@types/node" "*" +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -2201,11 +2218,24 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jest@^29.5.5": + version "29.5.5" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.5.tgz#727204e06228fe24373df9bae76b90f3e8236a2a" + integrity sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.12": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" + integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -2221,6 +2251,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.6.tgz#ae1973dd2b1eeb1825695bb11ebfb746d27e3e93" integrity sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA== +"@types/node@^20.8.6": + version "20.8.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.6.tgz#0dbd4ebcc82ad0128df05d0e6f57e05359ee47fa" + integrity sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ== + dependencies: + undici-types "~5.25.1" + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -2258,6 +2295,13 @@ dependencies: "@types/react" "^17" +"@types/react-dom@^18.2.13": + version "18.2.13" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.13.tgz#89cd7f9ec8b28c8b6f0392b9591671fb4a9e96b7" + integrity sha512-eJIUv7rPP+EC45uNYp/ThhSpE16k22VJUknt5OLoH9tbXoi8bMhwLf5xRuWMywamNbWzhrSmU7IBJfPup1+3fw== + dependencies: + "@types/react" "*" + "@types/react-redux@^7.1.20": version "7.1.25" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" @@ -2268,6 +2312,23 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" +"@types/react-router-dom@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-transition-group@^4.4.4": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" @@ -2293,6 +2354,22 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.2.28": + version "18.2.28" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.28.tgz#86877465c0fcf751659a36c769ecedfcfacee332" + integrity sha512-ad4aa/RaaJS3hyGz0BGegdnSRXQBkd1CCYDCdNjBPg90UUpLgo+WlJqb9fMYUxtehmzF3PJaTWqRZjko6BRzBg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/redux@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@types/redux/-/redux-3.6.0.tgz#f1ebe1e5411518072e4fdfca5c76e16e74c1399a" + integrity sha512-ic+60DXHW5seNyqFvfr7Sk5cnXs+HsF9tIeIaxjOuSP5kzgDXC+AzKTYmjAfuLx4Sccm/0vjwBQj3OOkUkwOqg== + dependencies: + redux "*" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -2315,6 +2392,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== +"@types/semver@^7.5.0": + version "7.5.3" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" + integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== + "@types/serve-index@^1.9.1": version "1.9.1" resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" @@ -2401,6 +2483,23 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/eslint-plugin@^6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.5.tgz#f4024b9f63593d0c2b5bd6e4ca027e6f30934d4f" + integrity sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.7.5" + "@typescript-eslint/type-utils" "6.7.5" + "@typescript-eslint/utils" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/experimental-utils@^5.0.0": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.54.1.tgz#a45609ce43fc6b24b4c4dde215446eaad7805223" @@ -2418,6 +2517,17 @@ "@typescript-eslint/typescript-estree" "5.54.1" debug "^4.3.4" +"@typescript-eslint/parser@^6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.5.tgz#8d7ca3d1fbd9d5a58cc4d30b2aa797a760137886" + integrity sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw== + dependencies: + "@typescript-eslint/scope-manager" "6.7.5" + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/typescript-estree" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + debug "^4.3.4" + "@typescript-eslint/scope-manager@5.54.1": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.54.1.tgz#6d864b4915741c608a58ce9912edf5a02bb58735" @@ -2426,6 +2536,14 @@ "@typescript-eslint/types" "5.54.1" "@typescript-eslint/visitor-keys" "5.54.1" +"@typescript-eslint/scope-manager@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz#1cf33b991043886cd67f4f3600b8e122fc14e711" + integrity sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A== + dependencies: + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + "@typescript-eslint/type-utils@5.54.1": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.54.1.tgz#4825918ec27e55da8bb99cd07ec2a8e5f50ab748" @@ -2436,11 +2554,26 @@ debug "^4.3.4" tsutils "^3.21.0" +"@typescript-eslint/type-utils@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.5.tgz#0a65949ec16588d8956f6d967f7d9c84ddb2d72a" + integrity sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g== + dependencies: + "@typescript-eslint/typescript-estree" "6.7.5" + "@typescript-eslint/utils" "6.7.5" + debug "^4.3.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/types@5.54.1": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.54.1.tgz#29fbac29a716d0f08c62fe5de70c9b6735de215c" integrity sha512-G9+1vVazrfAfbtmCapJX8jRo2E4MDXxgm/IMOF4oGh3kq7XuK3JRkOg6y2Qu1VsTRmWETyTkWt1wxy7X7/yLkw== +"@typescript-eslint/types@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.5.tgz#4571320fb9cf669de9a95d9849f922c3af809790" + integrity sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ== + "@typescript-eslint/typescript-estree@5.54.1": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.54.1.tgz#df7b6ae05fd8fef724a87afa7e2f57fa4a599be1" @@ -2454,6 +2587,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz#4578de1a26e9f24950f029a4f00d1bfe41f15a39" + integrity sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg== + dependencies: + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/visitor-keys" "6.7.5" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/utils@5.54.1", "@typescript-eslint/utils@^5.43.0": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.54.1.tgz#7a3ee47409285387b9d4609ea7e1020d1797ec34" @@ -2468,6 +2614,19 @@ eslint-utils "^3.0.0" semver "^7.3.7" +"@typescript-eslint/utils@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.5.tgz#ab847b53d6b65e029314b8247c2336843dba81ab" + integrity sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.7.5" + "@typescript-eslint/types" "6.7.5" + "@typescript-eslint/typescript-estree" "6.7.5" + semver "^7.5.4" + "@typescript-eslint/visitor-keys@5.54.1": version "5.54.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.54.1.tgz#d7a8a0f7181d6ac748f4d47b2306e0513b98bf8b" @@ -2476,6 +2635,14 @@ "@typescript-eslint/types" "5.54.1" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.7.5": + version "6.7.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz#84c68d6ceb5b12d5246b918b84f2b79affd6c2f1" + integrity sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg== + dependencies: + "@typescript-eslint/types" "6.7.5" + eslint-visitor-keys "^3.4.1" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -4887,6 +5054,11 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint-webpack-plugin@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz#1978cdb9edc461e4b0195a20da950cf57988347c" @@ -5714,6 +5886,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + grid-index@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/grid-index/-/grid-index-1.1.0.tgz#97f8221edec1026c8377b86446a7c71e79522ea7" @@ -6007,7 +6184,7 @@ ieee754@^1.1.12: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -9213,7 +9390,7 @@ redux-saga@^1.1.3: dependencies: "@redux-saga/core" "^1.2.2" -redux@^4.0.0, redux@^4.0.4, redux@^4.2.0: +redux@*, redux@^4.0.0, redux@^4.0.4, redux@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -9648,6 +9825,13 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: dependencies: lru-cache "^6.0.0" +semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -10437,6 +10621,11 @@ tryer@^1.0.1: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + tsconfig-paths@^3.14.1: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" @@ -10569,6 +10758,11 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" +typescript@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -10589,6 +10783,11 @@ uncontrollable@^7.2.1: invariant "^2.2.4" react-lifecycles-compat "^3.0.4" +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"