From 843733123f1def470c04db56aaaef48f578dc849 Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Wed, 13 Sep 2023 02:49:21 +0100 Subject: [PATCH 1/9] Fix interest computation --- api/account/assets.py | 5 ++--- api/apis.py | 2 -- web/src/state/bots/actions.js | 38 ++++++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/api/account/assets.py b/api/account/assets.py index 5eeddd657..68ff2e5e3 100644 --- a/api/account/assets.py +++ b/api/account/assets.py @@ -247,7 +247,7 @@ async def retrieve_gainers_losers(self, market_asset="USDT"): ) def match_series_dates( - self, dates, balance_date, i: int = 0, count=0 + self, dates, balance_date, i: int = 0 ) -> int | None: if i == len(dates): return None @@ -298,7 +298,7 @@ async def get_balance_series(self, end_date, start_date): lte_tp_id = ObjectId.from_datetime(obj_end_date) params["_id"]["$lte"] = lte_tp_id - balance_series = list(self.db.balances.find(params).sort([("_id", -1)])) + balance_series = list(self.db.balances.find(params).sort([("time", -1)])) # btc candlestick data series params = CandlestickParams( @@ -314,7 +314,6 @@ async def get_balance_series(self, end_date, start_date): balances_series_diff = [] balances_series_dates = [] balance_btc_diff = [] - balance_series.sort(key=lambda item: item["_id"], reverse=False) for index, item in enumerate(balance_series): btc_index = self.match_series_dates(dates, item["time"], index) diff --git a/api/apis.py b/api/apis.py index dda4418d8..a99433613 100644 --- a/api/apis.py +++ b/api/apis.py @@ -10,8 +10,6 @@ class BinanceApi: """ Binance API URLs - - To test: https://binance.github.io/binance-api-swagger/ """ diff --git a/web/src/state/bots/actions.js b/web/src/state/bots/actions.js index 2a6777686..006fa0211 100644 --- a/web/src/state/bots/actions.js +++ b/web/src/state/bots/actions.js @@ -80,6 +80,24 @@ export function getProfit(base_price, current_price, strategy = "long") { return 0; } +/** + * Calculate interests based on hourly interest rate + * @param {bot} bot object + * @returns {float} + */ +function getInterestsShortMargin(bot) { + const timeDelta = bot.deal.margin_short_sell_timestamp - bot.deal.margin_short_buy_back_timestamp; + const durationHours = (timeDelta / 1000) / 3600 + const interests = parseFloat(bot.deal.hourly_interest_rate) * durationHours; + const closeTotal = bot.deal.margin_short_buy_back_price; + const openTotal = bot.deal.margin_short_sell_price; + return { + interests: interests, + openTotal: openTotal, + closeTotal: closeTotal, + } +} + /** * This function calculates the profit (not including commissions/fees) * for a single bot, namely the BotForm and TestBotForm components @@ -105,27 +123,27 @@ export function computeSingleBotProfit(bot, realTimeCurrPrice = null) { } else if (bot.deal.margin_short_sell_price > 0) { // Completed margin short if (bot.deal.margin_short_buy_back_price > 0) { - const currentPrice = bot.deal.margin_short_buy_back_price; - const marginSellPrice = bot.deal.margin_short_sell_price; - const interests = (+bot.deal.hourly_interest_rate) * (+bot.deal.margin_short_loan_principal) + + const { interests, openTotal, closeTotal} = getInterestsShortMargin(bot); let profitChange = parseFloat( - ((currentPrice - marginSellPrice - interests) / marginSellPrice) * 100 - ) * -1; + (((openTotal - closeTotal) / openTotal) - interests) * 100 + ); return +profitChange.toFixed(2); } else { - const currentPrice = + // Not completed margin_sho + const closePrice = bot.deal.margin_short_buy_back_price > 0 ? bot.deal.margin_short_buy_back_price : realTimeCurrPrice || bot.deal.current_price; - if (currentPrice === 0) { + if (closePrice === 0) { return 0; } - const marginSellPrice = bot.deal.margin_short_sell_price; + const { interests, openTotal } = getInterestsShortMargin(bot); let profitChange = parseFloat( - ((currentPrice - marginSellPrice) / marginSellPrice) * 100 - ) * -1; + (((openTotal - closePrice) / openTotal) - interests) * 100 + ); return +profitChange.toFixed(2); } } else { From 2b625c1863e53bd626a7f7b29a3167986b43368c Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Thu, 14 Sep 2023 02:58:32 +0100 Subject: [PATCH 2/9] Fix timestamp --- web/src/components/BotCard.jsx | 4 ++-- web/src/pages/dashboard/Dashboard.jsx | 6 ++++-- web/src/state/bots/actions.js | 6 +++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/web/src/components/BotCard.jsx b/web/src/components/BotCard.jsx index 3fbd747e7..2b2cb7135 100644 --- a/web/src/components/BotCard.jsx +++ b/web/src/components/BotCard.jsx @@ -128,11 +128,11 @@ export default function BotCard({ -

Bought @

+

Open @

- {x.deal?.buy_price && x.deal.buy_price.toFixed(6)} + {(x.deal?.buy_price && x.deal.buy_price.toFixed(6)) || (x.deal?.buy_total_qty)}

diff --git a/web/src/pages/dashboard/Dashboard.jsx b/web/src/pages/dashboard/Dashboard.jsx index 77aaec4f1..ca1ed8b9c 100644 --- a/web/src/pages/dashboard/Dashboard.jsx +++ b/web/src/pages/dashboard/Dashboard.jsx @@ -2,7 +2,7 @@ import produce from "immer"; import React from "react"; import { connect } from "react-redux"; import { Card, CardBody, CardFooter, CardTitle } from "reactstrap"; -import { Col, Row } from "react-bootstrap"; +import { Col, Row, Container } from "react-bootstrap"; import GainersLosers from "../../components/GainersLosers"; import { loading } from "../../containers/spinner/actions"; import { getBalanceRaw, getEstimate } from "../../state/balances/actions"; @@ -224,6 +224,7 @@ class Dashboard extends React.Component { <> + @@ -335,6 +336,7 @@ class Dashboard extends React.Component { + {this.props.benchmarkData && ( @@ -385,7 +387,7 @@ const mapStateToProps = (s) => { let percentageRevenue = 0; let revenue = 0; if (benchmarkData && balanceEstimate) { - revenue = (balanceEstimate.total_fiat - benchmarkData.usdt[benchmarkData.usdt.length - 1]); + revenue = (balanceEstimate.total_fiat - benchmarkData.usdt[0]); percentageRevenue = (revenue / balanceEstimate.total_fiat) * 100; } diff --git a/web/src/state/bots/actions.js b/web/src/state/bots/actions.js index 006fa0211..54f49c660 100644 --- a/web/src/state/bots/actions.js +++ b/web/src/state/bots/actions.js @@ -86,7 +86,11 @@ export function getProfit(base_price, current_price, strategy = "long") { * @returns {float} */ function getInterestsShortMargin(bot) { - const timeDelta = bot.deal.margin_short_sell_timestamp - bot.deal.margin_short_buy_back_timestamp; + let closeTimestamp = bot.deal.margin_short_buy_back_timestamp; + if (closeTimestamp === 0) { + closeTimestamp = new Date().getTime() + } + const timeDelta = closeTimestamp - bot.deal.margin_short_sell_timestamp; const durationHours = (timeDelta / 1000) / 3600 const interests = parseFloat(bot.deal.hourly_interest_rate) * durationHours; const closeTotal = bot.deal.margin_short_buy_back_price; From 480d893b345b7b16259ee9a881d7643a122d259f Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Thu, 14 Sep 2023 04:18:52 +0100 Subject: [PATCH 3/9] Custom grid for dashboard items --- .github/workflows/pr.yml | 1 - api/market_updates.py | 33 +-- .../assets/scss/paper-dashboard/_misc.scss | 11 + .../scss/paper-dashboard/_variables.scss | 5 +- web/src/pages/dashboard/Dashboard.jsx | 209 +++++++++--------- 5 files changed, 134 insertions(+), 125 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 154008515..38386e04e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -86,7 +86,6 @@ jobs: -e MONGO_AUTH_PASSWORD=${{ env.MONGO_AUTH_PASSWORD }} \ -e PYTHONUNBUFFERED=TRUE \ -e ENV=ci -d binbot_streaming - docker logs binbot_streaming - name: Tag image if: ${{ github.actor != 'dependabot[bot]' }} run: | diff --git a/api/market_updates.py b/api/market_updates.py index 04cf7a502..f616c71b7 100644 --- a/api/market_updates.py +++ b/api/market_updates.py @@ -12,7 +12,6 @@ ) - logging.Formatter.converter = time.gmtime # date time in GMT/UTC logging.basicConfig( level=logging.INFO, @@ -21,26 +20,30 @@ datefmt="%Y-%m-%d %H:%M:%S", ) -if os.getenv("ENV") != "ci": - scheduler = BackgroundScheduler() - assets = Assets() +def cronjobs(): + assets = Assets() assets.store_balance() - # scheduler.add_job( - # func=assets.store_balance, - # trigger="cron", - # timezone="Europe/London", - # hour=1, - # minute=0, - # id='store_balance' - # ) - # scheduler.start() - # atexit.register(lambda: scheduler.shutdown(wait=False)) + logging.log("Cron jobs completed") + + +if os.getenv("ENV") != "ci": + scheduler = BackgroundScheduler() + scheduler.add_job( + func=cronjobs, + trigger="cron", + timezone="Europe/London", + hour=1, + minute=0, + id="store_balance", + ) + scheduler.start() + atexit.register(lambda: scheduler.shutdown(wait=False)) try: mu = StreamingController() mu.get_klines() - + except WebSocketException as e: if isinstance(e, WebSocketConnectionClosedException): logging.error("Lost websocket connection") diff --git a/web/src/assets/scss/paper-dashboard/_misc.scss b/web/src/assets/scss/paper-dashboard/_misc.scss index c061fcab3..f9a32c1ac 100644 --- a/web/src/assets/scss/paper-dashboard/_misc.scss +++ b/web/src/assets/scss/paper-dashboard/_misc.scss @@ -173,4 +173,15 @@ a[data-toggle="collapse"][aria-expanded="true"] .caret, .redux-toastr { position: absolute; +} + +// Dashboard grid +#dashboard-grid { + display: grid; + column-gap: 1rem; + grid-template-columns: repeat(2, 3fr); + + @media screen and (min-width: 991px) { + grid-template-columns: 1fr; + } } \ No newline at end of file diff --git a/web/src/assets/scss/paper-dashboard/_variables.scss b/web/src/assets/scss/paper-dashboard/_variables.scss index 2787d8e33..ade6f66d9 100644 --- a/web/src/assets/scss/paper-dashboard/_variables.scss +++ b/web/src/assets/scss/paper-dashboard/_variables.scss @@ -116,7 +116,7 @@ $bg-danger: lighten($danger-color, 7%) !default; $bg-brown: lighten($brown-color, 7%) !default; $bg-purple: lighten($purple-color, 7%) !default; -// brand Colors +// brand Colors $brand-primary: $primary-color !default; $brand-info: $info-color !default; $brand-success: $success-color !default; @@ -126,7 +126,7 @@ $brand-inverse: $black-color !default; $link-disabled-color: #666666 !default; $dark-color: #212120 !default; -// light colors +// light colors $light-blue: rgba($primary-color, 0.2); $light-azure: rgba($info-color, 0.2); $light-green: rgba($success-color, 0.2); @@ -390,4 +390,3 @@ $zindex-notification: 1070 !default; // must be higher than a modal background ( $zindex-modal: 1080 !default; // must be higher than a modal background (1050) $zindex-loader-bg: 1085 !default; $zindex-loader: 1090 !default; - diff --git a/web/src/pages/dashboard/Dashboard.jsx b/web/src/pages/dashboard/Dashboard.jsx index ca1ed8b9c..19c0f8494 100644 --- a/web/src/pages/dashboard/Dashboard.jsx +++ b/web/src/pages/dashboard/Dashboard.jsx @@ -1,8 +1,9 @@ import produce from "immer"; +import moment from "moment"; import React from "react"; +import { Col, Row } from "react-bootstrap"; import { connect } from "react-redux"; import { Card, CardBody, CardFooter, CardTitle } from "reactstrap"; -import { Col, Row, Container } from "react-bootstrap"; import GainersLosers from "../../components/GainersLosers"; import { loading } from "../../containers/spinner/actions"; import { getBalanceRaw, getEstimate } from "../../state/balances/actions"; @@ -10,8 +11,7 @@ import { checkValue, listCssColors, roundDecimals } from "../../validations"; import { NetWorthChart } from "./NetWorthChart"; import { PortfolioBenchmarkChart } from "./PortfolioBenchmarkChart"; import { ProfitLossBars } from "./ProfitLossBars"; -import { getGainersLosers, getBenchmarkData, getBenchmarkUsdt } from "./saga"; -import moment from "moment"; +import { getBenchmarkData, getBenchmarkUsdt, getGainersLosers } from "./saga"; class Dashboard extends React.Component { constructor(props) { @@ -223,120 +223,112 @@ class Dashboard extends React.Component { {!load ? ( <> - - - - - - - - - {balanceEstimate && ( -
- - -
- -
- - -

- Total Balance -

- - {roundDecimals( - balanceEstimate.total_fiat, - 2 - )}{" "} - {balanceEstimate.asset} -
-
- -
-
- )} - -
-
- {balanceEstimate && ( - -
- - -

Left to allocate:

- - -

- {balanceEstimate.fiat_left}{" "} - {balanceEstimate.asset} -

- -
-
- )} -
- -
- - - - - - -
- -
- - + +
+ + + + + {balanceEstimate && (
-

- Profit & Loss -

- 0 - ? "text-success card-title numbers" - : "text-danger card-title numbers" - } - > -

- {typeof this.props.percentageRevenue === - "number" && - `${this.props.percentageRevenue.toFixed( + + +

+ +
+ + +

+ Total Balance +

+ + {roundDecimals( + balanceEstimate.total_fiat, 2 - )}% `} -

-
-

+ )}{" "} + {balanceEstimate.asset} +
+ + +

- -
-
+ )} + + + + {balanceEstimate && (
-

- (Current - Last balance) -

+

Left to allocate:

- {typeof this.props.revenue === "number" && - this.props.revenue.toFixed(4)}{" "} - USDT + {balanceEstimate.fiat_left}{" "} + {balanceEstimate.asset}

-
- - - + )} + + + + + +
+ +
+ + +
+

+ Profit & Loss +

+ 0 + ? "text-success card-title numbers" + : "text-danger card-title numbers" + } + > +

+ {typeof this.props.percentageRevenue === + "number" && + `${this.props.percentageRevenue.toFixed( + 2 + )}% `} +

+
+

+

+ +
+
+ +
+ + +

+ (Current - Last balance) +

+ + +

+ {typeof this.props.revenue === "number" && + this.props.revenue.toFixed(4)}{" "} + USDT +

+ +
+
+
+
{this.props.benchmarkData && ( @@ -383,11 +375,16 @@ const mapStateToProps = (s) => { const { data: balanceEstimate } = s.estimateReducer; const { data: balance_raw } = s.balanceRawReducer; const { data: gainersLosersData } = s.gainersLosersReducer; - const { data: benchmarkData, btcPrices, usdtBalanceSeries, dates } = s.btcBenchmarkReducer; + const { + data: benchmarkData, + btcPrices, + usdtBalanceSeries, + dates, + } = s.btcBenchmarkReducer; let percentageRevenue = 0; let revenue = 0; if (benchmarkData && balanceEstimate) { - revenue = (balanceEstimate.total_fiat - benchmarkData.usdt[0]); + revenue = balanceEstimate.total_fiat - benchmarkData.usdt[0]; percentageRevenue = (revenue / balanceEstimate.total_fiat) * 100; } @@ -399,7 +396,7 @@ const mapStateToProps = (s) => { benchmarkData: { usdt: usdtBalanceSeries, btc: btcPrices, - dates: dates + dates: dates, }, percentageRevenue: percentageRevenue, revenue: revenue, From 0036d9da1095a671696148f6ccc6baebcb92080b Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Sun, 24 Sep 2023 03:32:52 +0100 Subject: [PATCH 4/9] Transfer dust cron job --- api/account/assets.py | 34 +++++++++++++++++++++++++++++++++- api/account/routes.py | 8 ++++++++ api/apis.py | 8 ++++++++ api/market_updates.py | 22 ++++++++++++++-------- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/api/account/assets.py b/api/account/assets.py index 68ff2e5e3..0d4432253 100644 --- a/api/account/assets.py +++ b/api/account/assets.py @@ -8,7 +8,7 @@ from charts.models import CandlestickParams from charts.models import Candlestick from db import setup_db -from tools.handle_error import InvalidSymbol, json_response, json_response_message +from tools.handle_error import InvalidSymbol, json_response, json_response_error, json_response_message from tools.round_numbers import round_numbers @@ -336,3 +336,35 @@ async def get_balance_series(self, end_date, start_date): } ) return resp + + def clean_balance_assets(self): + """ + Check if there are many small assets (0.000.. BTC) + if there are more than 5 (number of bots) + transfer to BNB + """ + balance = self.get_raw_balance() + data = json.loads(balance.body)["data"] + assets = [] + for item in data: + if item["asset"] not in ["USDT", "NFT", "BNB"]: + assets.append(item["asset"]) + + if len(assets) > 5: + self.transfer_dust(assets) + resp = json_response_message("Sucessfully cleaned balance.") + else: + resp = json_response_error("Amount of assets in balance is low. Transfer not performed.") + + return resp + + def disable_isolated_accounts(self): + """ + Check and disable isolated accounts + """ + info = self.signed_request(url=self.isolated_account_url, payload={}) + for item in info["assets"]: + if float(item["liquidatePrice"]) == 0: + self.disable_isolated_margin_account(item["symbol"]) + resp = json_response_message("Sucessfully finished disabling isolated margin accounts.") + return resp \ No newline at end of file diff --git a/api/account/routes.py b/api/account/routes.py index 891d910bb..68f7e3bcb 100644 --- a/api/account/routes.py +++ b/api/account/routes.py @@ -79,3 +79,11 @@ async def get_balance_series(): today = datetime.now() month_ago = today - timedelta(30) 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() + +@account_blueprint.get("/disable-isolated", response_model=BalanceSeriesResponse, tags=["assets"]) +async def disable_isolated(): + return await Assets().disable_isolated_accounts() diff --git a/api/apis.py b/api/apis.py index a99433613..a4063bc3c 100644 --- a/api/apis.py +++ b/api/apis.py @@ -147,6 +147,14 @@ def get_isolated_balance_total(self): raise IsolateBalanceError("Hit symbol 24hr restriction or not available (requires transfer in)") return assets + def transfer_dust(self, assets): + """ + Transform small balances to BNB + """ + assets = ",".join(assets) + response = self.signed_request(url=self.dust_transfer_url, method="POST", payload={"asset": assets}) + return response + class BinbotApi(BinanceApi): """ API endpoints on this project itself diff --git a/api/market_updates.py b/api/market_updates.py index f616c71b7..47a9f1fa0 100644 --- a/api/market_updates.py +++ b/api/market_updates.py @@ -4,6 +4,7 @@ import time from apscheduler.schedulers.background import BackgroundScheduler +from api.account.routes import disable_isolated from streaming.streaming_controller import StreamingController from account.assets import Assets from websocket import ( @@ -20,23 +21,28 @@ datefmt="%Y-%m-%d %H:%M:%S", ) - -def cronjobs(): - assets = Assets() - assets.store_balance() - logging.log("Cron jobs completed") - - if os.getenv("ENV") != "ci": scheduler = BackgroundScheduler() + assets = Assets() + scheduler.add_job( - func=cronjobs, + func=assets.store_balance(), trigger="cron", timezone="Europe/London", hour=1, minute=0, id="store_balance", ) + + scheduler.add_job( + func=assets.disable_isolated_accounts(), + trigger="cron", + timezone="Europe/London", + hour=2, + minute=0, + id="disable_isolated_accounts", + ) + scheduler.start() atexit.register(lambda: scheduler.shutdown(wait=False)) From e22858ee430992d05809486f5881bf183aded503 Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Thu, 28 Sep 2023 02:16:48 +0100 Subject: [PATCH 5/9] Add account cleaning jobs --- api/account/assets.py | 21 ++++++++++++--------- api/deals/margin.py | 19 ++++++++----------- api/deals/spot.py | 5 ++--- api/market_updates.py | 15 ++++++++++++--- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/api/account/assets.py b/api/account/assets.py index 0d4432253..8514ebb87 100644 --- a/api/account/assets.py +++ b/api/account/assets.py @@ -337,28 +337,27 @@ async def get_balance_series(self, end_date, start_date): ) return resp - def clean_balance_assets(self): + async def clean_balance_assets(self): """ Check if there are many small assets (0.000.. BTC) if there are more than 5 (number of bots) transfer to BNB """ - balance = self.get_raw_balance() - data = json.loads(balance.body)["data"] + data = self.signed_request(url=self.account_url) assets = [] - for item in data: - if item["asset"] not in ["USDT", "NFT", "BNB"]: - assets.append(item["asset"]) + for item in data["balances"]: + if item["asset"] not in ["USDT", "NFT", "BNB"] and float(item["free"]) > 0: + assets.append(item["free"]) if len(assets) > 5: self.transfer_dust(assets) resp = json_response_message("Sucessfully cleaned balance.") else: - resp = json_response_error("Amount of assets in balance is low. Transfer not performed.") + resp = json_response_error("Amount of assets in balance is low. Transfer not needed.") return resp - def disable_isolated_accounts(self): + async def disable_isolated_accounts(self): """ Check and disable isolated accounts """ @@ -366,5 +365,9 @@ def disable_isolated_accounts(self): for item in info["assets"]: if float(item["liquidatePrice"]) == 0: self.disable_isolated_margin_account(item["symbol"]) - resp = json_response_message("Sucessfully finished disabling isolated margin accounts.") + msg = "Sucessfully finished disabling isolated margin accounts." + else: + msg = "Disabling isolated margin account not required yet." + + resp = json_response_message(msg) return resp \ No newline at end of file diff --git a/api/deals/margin.py b/api/deals/margin.py index 207f6fcbd..81f74b200 100644 --- a/api/deals/margin.py +++ b/api/deals/margin.py @@ -20,6 +20,8 @@ class MarginShortError(Exception): pass +# To be removed one day when commission endpoint found that provides this value +ESTIMATED_COMMISSIONS_RATE = 0.0075 class MarginDeal(BaseDeal): def __init__(self, bot, db_collection_name: str) -> None: @@ -77,13 +79,12 @@ def compute_margin_buy_back( ): return None - qty = float(self.isolated_balance[0]["baseAsset"]["borrowed"]) + float( - self.isolated_balance[0]["baseAsset"]["interest"] - ) + 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 + qty = round_numbers_ceiling(qty, self.qty_precision) - return round_numbers_ceiling(qty, self.qty_precision), float( - self.isolated_balance[0]["baseAsset"]["free"] - ) + free = float(self.isolated_balance[0]["baseAsset"]["free"]) + + return qty, free def get_remaining_assets(self) -> tuple[float, float]: """ @@ -706,11 +707,7 @@ def execute_stop_loss(self): try: quote, base = self.get_remaining_assets() price = self.matching_engine(self.active_bot.pair, True, qty) - # No need to round? - # qty = round_numbers( - # float(quote) / float(price), self.qty_precision - # ) - # If still qty = 0, it means everything is clear + if qty == 0: return diff --git a/api/deals/spot.py b/api/deals/spot.py index 80b3feba8..e85dea629 100644 --- a/api/deals/spot.py +++ b/api/deals/spot.py @@ -29,11 +29,10 @@ def __init__(self, bot, db_collection_name: str) -> None: def switch_margin_short(self, close_price): msg = "Resetting bot for margin_short strategy..." - print(msg) self.update_deal_logs(msg) + self.save_bot_streaming() - margin_deal = MarginDeal(self.active_bot, db_collection_name="bots") - margin_deal.margin_short_base_order() + self.active_bot = MarginDeal(self.active_bot, db_collection_name="bots").margin_short_base_order() self.save_bot_streaming() diff --git a/api/market_updates.py b/api/market_updates.py index 47a9f1fa0..1a363d123 100644 --- a/api/market_updates.py +++ b/api/market_updates.py @@ -4,7 +4,7 @@ import time from apscheduler.schedulers.background import BackgroundScheduler -from api.account.routes import disable_isolated +from account.routes import disable_isolated from streaming.streaming_controller import StreamingController from account.assets import Assets from websocket import ( @@ -26,7 +26,7 @@ assets = Assets() scheduler.add_job( - func=assets.store_balance(), + func=assets.store_balance, trigger="cron", timezone="Europe/London", hour=1, @@ -35,7 +35,7 @@ ) scheduler.add_job( - func=assets.disable_isolated_accounts(), + func=assets.disable_isolated_accounts, trigger="cron", timezone="Europe/London", hour=2, @@ -43,6 +43,15 @@ id="disable_isolated_accounts", ) + scheduler.add_job( + func=assets.clean_balance_assets, + trigger="cron", + timezone="Europe/London", + hour=3, + minute=0, + id="clean_balance_assets", + ) + scheduler.start() atexit.register(lambda: scheduler.shutdown(wait=False)) From 0a79714f2e5590f6872abb8396020b58b504df2d Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Tue, 3 Oct 2023 02:36:14 +0100 Subject: [PATCH 6/9] Fix scheduler --- api/deals/margin.py | 3 +-- api/market_updates.py | 22 ++++++++++++---------- web/src/pages/dashboard/reducer.js | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/api/deals/margin.py b/api/deals/margin.py index 81f74b200..ff8632c53 100644 --- a/api/deals/margin.py +++ b/api/deals/margin.py @@ -311,14 +311,13 @@ def retry_repayment(self, query_loan, buy_back_fiat): self.save_bot_streaming() self.terminate_margin_short(buy_back_fiat) except Exception as error: - print(error) try: self.transfer_spot_to_isolated_margin( asset=self.active_bot.balance_to_use, symbol=self.active_bot.pair, amount=total_base_qty, ) - self.retry_repayment(query_loan, buy_back_fiat) + # self.retry_repayment(query_loan, buy_back_fiat) except Exception as error: print(error) self._append_errors( diff --git a/api/market_updates.py b/api/market_updates.py index 1a363d123..be8448cc1 100644 --- a/api/market_updates.py +++ b/api/market_updates.py @@ -3,8 +3,7 @@ import logging import time -from apscheduler.schedulers.background import BackgroundScheduler -from account.routes import disable_isolated +from apscheduler.schedulers.background import BlockingScheduler from streaming.streaming_controller import StreamingController from account.assets import Assets from websocket import ( @@ -22,7 +21,8 @@ ) if os.getenv("ENV") != "ci": - scheduler = BackgroundScheduler() + + scheduler = BlockingScheduler() assets = Assets() scheduler.add_job( @@ -30,16 +30,16 @@ trigger="cron", timezone="Europe/London", hour=1, - minute=0, + minute=1, id="store_balance", ) - + scheduler.add_job( func=assets.disable_isolated_accounts, trigger="cron", timezone="Europe/London", hour=2, - minute=0, + minute=1, id="disable_isolated_accounts", ) @@ -48,16 +48,14 @@ trigger="cron", timezone="Europe/London", hour=3, - minute=0, + minute=1, id="clean_balance_assets", ) - scheduler.start() - atexit.register(lambda: scheduler.shutdown(wait=False)) - try: mu = StreamingController() mu.get_klines() + scheduler.start() except WebSocketException as e: if isinstance(e, WebSocketConnectionClosedException): @@ -66,8 +64,12 @@ mu.get_klines() else: logging.error(f"Websocket exception: {e}") + + atexit.register(lambda: scheduler.shutdown(wait=False)) 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/web/src/pages/dashboard/reducer.js b/web/src/pages/dashboard/reducer.js index 94789a2da..e8653301e 100644 --- a/web/src/pages/dashboard/reducer.js +++ b/web/src/pages/dashboard/reducer.js @@ -66,7 +66,7 @@ const btcBenchmarkReducer = produce((draft, action) => { } }); // Match dates with diff series - action.data.dates.shift() + action.data.dates.pop() draft.dates = action.data.dates; draft.data = action.data } From 1951357355bb792c959ff9c54c3dcff032d9a38f Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Sun, 8 Oct 2023 06:03:08 +0100 Subject: [PATCH 7/9] Fix wallet asset clean endpoint --- api/account/assets.py | 2 +- api/apis.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/account/assets.py b/api/account/assets.py index 8514ebb87..9ae284dcc 100644 --- a/api/account/assets.py +++ b/api/account/assets.py @@ -347,7 +347,7 @@ async def clean_balance_assets(self): assets = [] for item in data["balances"]: if item["asset"] not in ["USDT", "NFT", "BNB"] and float(item["free"]) > 0: - assets.append(item["free"]) + assets.append(item["asset"]) if len(assets) > 5: self.transfer_dust(assets) diff --git a/api/apis.py b/api/apis.py index a4063bc3c..b300fd276 100644 --- a/api/apis.py +++ b/api/apis.py @@ -1,3 +1,4 @@ +from typing import List import hashlib import hmac import os @@ -147,12 +148,12 @@ def get_isolated_balance_total(self): raise IsolateBalanceError("Hit symbol 24hr restriction or not available (requires transfer in)") return assets - def transfer_dust(self, assets): + def transfer_dust(self, assets: List[str]): """ Transform small balances to BNB """ - assets = ",".join(assets) - response = self.signed_request(url=self.dust_transfer_url, method="POST", payload={"asset": assets}) + list_assets = ",".join(assets) + response = self.signed_request(url=self.dust_transfer_url, method="POST", payload={"asset": list_assets}) return response class BinbotApi(BinanceApi): From e99d92aedd71d4f7663d15357ad2ec18b90792c1 Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Sun, 8 Oct 2023 07:15:46 +0100 Subject: [PATCH 8/9] One click liquidation endpoint --- api/account/assets.py | 68 +++++++++++++++++++++++++++++++++++++++++-- api/account/routes.py | 4 +++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/api/account/assets.py b/api/account/assets.py index 9ae284dcc..becc4eff2 100644 --- a/api/account/assets.py +++ b/api/account/assets.py @@ -9,7 +9,8 @@ from charts.models import Candlestick from db import setup_db from tools.handle_error import InvalidSymbol, json_response, json_response_error, json_response_message -from tools.round_numbers import round_numbers +from tools.round_numbers import round_numbers, round_numbers_ceiling, supress_notation +from binance.exceptions import BinanceAPIException class Assets(Account): @@ -370,4 +371,67 @@ async def disable_isolated_accounts(self): msg = "Disabling isolated margin account not required yet." resp = json_response_message(msg) - return resp \ No newline at end of file + return resp + + def one_click_liquidation(self, asset, balance="USDT", json=True): + """ + Emulate Binance Dashboard + One click liquidation function + """ + pair = asset + balance + isolated_balance = self.get_isolated_balance(pair) + # Check margin account balance first + balance = float(isolated_balance[0]["quoteAsset"]["free"]) + qty_precision = self.get_qty_precision(pair) + if balance > 0: + # repay + repay_amount = float( + isolated_balance[0]["baseAsset"]["borrowed"] + ) + float(isolated_balance[0]["baseAsset"]["interest"]) + # 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": asset, "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) + try: + self.repay_margin_loan( + asset=asset, + symbol=pair, + amount=repay_amount, + isIsolated="TRUE", + ) + # get new balance + isolated_balance = self.get_isolated_balance(pair) + print(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=asset, + symbol=pair, + amount=isolated_balance[0]["quoteAsset"]["free"], + ) + if float(isolated_balance[0]["baseAsset"]["free"]) != 0: + self.transfer_isolated_margin_to_spot( + asset=asset, + symbol=pair, + amount=isolated_balance[0]["baseAsset"]["free"], + ) + + self.disable_isolated_margin_account(symbol=pair) + return json_response_message(f"Successfully liquidated {pair}") + + except BinanceAPIException 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) diff --git a/api/account/routes.py b/api/account/routes.py index 68f7e3bcb..321328342 100644 --- a/api/account/routes.py +++ b/api/account/routes.py @@ -87,3 +87,7 @@ async def clean_balance(): @account_blueprint.get("/disable-isolated", response_model=BalanceSeriesResponse, tags=["assets"]) async def disable_isolated(): return await Assets().disable_isolated_accounts() + +@account_blueprint.get("/one-click-liquidation/{asset}", response_model=BalanceSeriesResponse, tags=["assets"]) +def one_click_liquidation(asset): + return Assets().one_click_liquidation(asset, json=True) From 0b57092179b2948e45f300caee1aae80c6406b06 Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Sun, 8 Oct 2023 19:26:18 +0100 Subject: [PATCH 9/9] Ignore minor dependabot updates --- .github/dependabot.yml | 6 ++++++ api/deals/margin.py | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9a6a14aa3..d5f635b15 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,9 @@ updates: commit-message: # Prefix all commit messages with "npm" prefix: "npm" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] - package-ecosystem: "pip" directory: "/" @@ -18,3 +21,6 @@ updates: prefix: "pip prod" prefix-development: "pip dev" include: "scope" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] diff --git a/api/deals/margin.py b/api/deals/margin.py index ff8632c53..439936614 100644 --- a/api/deals/margin.py +++ b/api/deals/margin.py @@ -317,7 +317,6 @@ def retry_repayment(self, query_loan, buy_back_fiat): symbol=self.active_bot.pair, amount=total_base_qty, ) - # self.retry_repayment(query_loan, buy_back_fiat) except Exception as error: print(error) self._append_errors( @@ -377,7 +376,7 @@ def terminate_margin_short(self, buy_back_fiat: bool = True): pass except BinanceErrors as error: if error.code == -3041: - self.retry_repayment(query_loan, buy_back_fiat) + self.active_bot.errors.append(error.message) pass except Exception as error: self.update_deal_logs(error)