From 53f7a585a6dabb4c17c66b3fdb650035e7d87dd5 Mon Sep 17 00:00:00 2001 From: Bogdan Kovalev Date: Thu, 12 May 2022 15:23:30 +0100 Subject: [PATCH] v2.3.2 (#20) * Code clean up * Set dimensions for chart * Fix data issue on iOS * Destroy chart to avoid memory leak * Initial setup refinment * Destroy chart * Adjust price line color in dark theme * Dashed line style for HODL and no auto-sell * Dashed line style for HODL and no auto-sell * Initial setup fixes * Log fixes * Keep memos in store and fix changing state to sold * Mark asset as hold if selling fails for some reason * Mark asset as hold if selling fails for some reason * Show chart lines based on values presence * Make trade PL computable * Fix incorrect profit and commission * Update CHANGELOG.md * Move trading to assets tab * Move trading to assets tab * Add Buy Again button * Assets page visual fixes * Draw sold price on chart * Debug memo at buy * Maintain precision for quantity * Log improvement * Draw quantity on a trade card * Draw quantity on a trade card * Fix color * Fix profit line on sold asset * Add badges to toggle button * Fix trades map get * Minor dep cleanup * Fix rounding issues in setting tab * Fix rounding issues in setting tab * Update CHANGELOG.md * Update README.md * Fix precision function * 30 sec refetch interval * Add cancel buy action * Code cleanup * Averaging Down feature (#12) * Averaging down feature * Minor UI adjustments for trade card * Minor UI adjustments for trade card * Averaging down feature * Averaging down feature * Minor typo fix * Minor fixes in Settings * Improve price tracking * Info tab style adjustment * Code cleanup * Averaging down feature * Notification sensitivity adjustment * Notification sensitivity adjustment * Notification sensitivity adjustment * Add sell confirmation when Averaging down enabled * Log err minor fix * Rename take profit to profit limit * Config fields compatibility fix * Update README.md and CHANGELOG.md * Fix review comments * Survivors feature (#14) * Fix Binance attempts to number of API URLs * Loop over Binance API servers and debug how many attepts left * Loop over Binance API servers and debug how many attepts left * Pick random binance server * Remove temporary debug msg * Make chart shorter * Revert chart height * Cancel action if buy failed * Cancel action if buy failed * Show price gap on sold assets * Recommendations feature test * Catch initialization errors * Catch initialization errors * Change recommendations error to info * Add retries to Binance.ts * Print recommends scrore * Adjust error size * Adjust Recommender.ts * Adjust Recommender.ts * Fixes in Recommender.ts * Refactor Binance retry logic * Recommender fixes * Recommender fixes * Fix error * Use coin name for recommendation * Improve Recommender * Improve Recommender * Visual adjustments * Fix recommender * Visual fixes * Adjust Binance.ts logs * Adjust executor * Adjust logs * Binance fix signature issue * Binance fix signature issue * Binance update number of API servers * Rename to Survivors feature * Fixes it Binance.ts * Fix Recommender scores names * Add refresh button to survivors tab * Refetch trades only when assets tab opened * When in SELL state, do not sell while price goes up * Sync survivor scores every 6 hours with store * Visual improvements * Make tabs bar scrollable * Make tabs bar scrollable * Revert * Visual improvements * Fix non-spot trading symbols tracking is survivors * Visual fine tuning * Update README.md and CHANGELOG.md * Handle a case when price is missing * Adjust logs * Adjust logs * Make Survivors count 5 last prices * Make CoinScore count 5 last prices * Fix Survivors reset * Keep 4 prices for scores * Update CHANGELOG.md * Reduce coin score sensitivity * Reduce coin score sensitivity * Add price to limit crossing alert * Maintain scores on stable coin switch * Add stable coins autocomplete * Add coin names autocomplete * Update CHANGELOG.md * Update CHANGELOG.md * Move enum to shared types --- CHANGELOG.md | 7 +++++++ apps-script/Binance.ts | 8 +++++++- apps-script/Exchange.ts | 13 +++++++++++++ apps-script/Store.ts | 16 +++++++++++++--- apps-script/Survivors.ts | 30 ++++++++++++++--------------- apps-script/TradeResult.ts | 6 ------ apps-script/Trader.ts | 6 +++--- apps-script/TradesQueue.ts | 2 +- apps-script/api.ts | 13 ++++++++++--- apps-script/shared-lib/types.ts | 12 ++++++++++-- src/App.tsx | 3 ++- src/components/Assets.tsx | 14 +++++++++++--- src/components/Settings.tsx | 34 ++++++++++++++++++++++++++++----- 13 files changed, 120 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb3f03a..6f7efdd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# v2.3.2 + +* Changing "Stable Coin" will not reset collected "Survivors" statistics for this stable coin if you switch back. +* Added autocompletion for "Coin Name" on the "Assets" tab. +* Added suggestions for "Stable Coin" on the "Settings" tab. +* Adjusted "Survivors" scores update logic to be less sensitive. + # v2.3.1 * Fixed Reset button on Survivors tab crashes the UI #16 diff --git a/apps-script/Binance.ts b/apps-script/Binance.ts index b8169f59..3104fdfa 100644 --- a/apps-script/Binance.ts +++ b/apps-script/Binance.ts @@ -39,7 +39,13 @@ export class Binance implements IExchange { const prices: { symbol: string, price: string }[] = JSON.parse(response.getContentText()) Log.debug(`Got ${prices.length} prices`) const map: { [p: string]: number } = {} - prices.forEach(p => map[p.symbol] = +p.price) + prices.forEach(p => { + // skip symbols that are not spot trading + if (p.symbol.match(/^\w+(UP|DOWN|BEAR|BULL)\w+$/)) { + return; + } + map[p.symbol] = +p.price + }) return map } diff --git a/apps-script/Exchange.ts b/apps-script/Exchange.ts index 8340978e..ecaf7703 100644 --- a/apps-script/Exchange.ts +++ b/apps-script/Exchange.ts @@ -3,13 +3,16 @@ import {Binance, IExchange} from "./Binance"; import {Config} from "./Store"; import {ExchangeSymbol, PriceProvider, TradeResult} from "./TradeResult"; import {CacheProxy} from "./CacheProxy"; +import {StableUSDCoin} from "./shared-lib/types"; export class Exchange implements IExchange { private readonly exchange: Binance; private priceProvider: CoinStats; + private stableCoin: StableUSDCoin; constructor(config: Config) { this.exchange = new Binance(config); + this.stableCoin = config.StableCoin; switch (config.PriceProvider) { case PriceProvider.Binance: @@ -50,4 +53,14 @@ export class Exchange implements IExchange { marketSell(symbol: ExchangeSymbol, quantity: number): TradeResult { return this.exchange.marketSell(symbol, quantity); } + + getCoinNames(): string[] { + const coinNames = []; + Object.keys(this.exchange.getPrices()).forEach(coinName => { + if (coinName.endsWith(this.stableCoin)) { + coinNames.push(coinName.split(this.stableCoin)[0]); + } + }); + return coinNames; + } } diff --git a/apps-script/Store.ts b/apps-script/Store.ts index bb4857d3..90f1d7b5 100644 --- a/apps-script/Store.ts +++ b/apps-script/Store.ts @@ -1,6 +1,7 @@ import {TradeMemo} from "./TradeMemo"; -import {ExchangeSymbol, PriceProvider, StableCoin} from "./TradeResult"; +import {ExchangeSymbol, PriceProvider} from "./TradeResult"; import {CacheProxy} from "./CacheProxy"; +import {StableUSDCoin} from "./shared-lib/types"; export interface IStore { get(key: String): any @@ -63,7 +64,7 @@ export class FirebaseStore implements IStore { KEY: '', SECRET: '', BuyQuantity: 10, - PriceAsset: StableCoin.USDT, + StableCoin: StableUSDCoin.USDT, StopLimit: 0.05, ProfitLimit: 0.1, SellAtStopLimit: false, @@ -90,6 +91,11 @@ export class FirebaseStore implements IStore { delete configCache.LossLimit } + if (configCache.PriceAsset) { + configCache.StableCoin = configCache.PriceAsset + delete configCache.PriceAsset + } + CacheProxy.put("Config", JSON.stringify(configCache)) return configCache; @@ -175,7 +181,7 @@ export class FirebaseStore implements IStore { export type Config = { KEY?: string SECRET?: string - PriceAsset: string + StableCoin: StableUSDCoin BuyQuantity: number StopLimit: number ProfitLimit: number @@ -194,6 +200,10 @@ export type Config = { */ AveragingDown: boolean + /** + * @deprecated + */ + PriceAsset?: string /** * @deprecated */ diff --git a/apps-script/Survivors.ts b/apps-script/Survivors.ts index 95c0331a..3fc32567 100644 --- a/apps-script/Survivors.ts +++ b/apps-script/Survivors.ts @@ -29,13 +29,16 @@ export class Survivors implements ScoresManager { * Sorted by recommendation score. */ getScores(): CoinScore[] { - const memosJson = CacheProxy.get("RecommenderMemos"); - const memos: CoinScoreMap = memosJson ? JSON.parse(memosJson) : {}; + const scoresJson = CacheProxy.get("RecommenderMemos"); + const scores: CoinScoreMap = scoresJson ? JSON.parse(scoresJson) : {}; + const stableCoin = this.store.getConfig().StableCoin; const recommended: CoinScore[] = [] - Object.values(memos).forEach(m => { - const r = CoinScore.fromObject(m); - if (r.getScore() > 0) { - recommended.push(r); + Object.keys(scores).forEach(k => { + if (k.endsWith(stableCoin)) { + const r = CoinScore.fromObject(scores[k]); + if (r.getScore() > 0) { + recommended.push(r); + } } }) return recommended.sort((a, b) => b.getScore() - a.getScore()).slice(0, 10); @@ -44,22 +47,17 @@ export class Survivors implements ScoresManager { updateScores(): void { const scoresJson = CacheProxy.get("RecommenderMemos"); const scores: CoinScoreMap = scoresJson ? JSON.parse(scoresJson) : this.store.get("SurvivorScores") || {}; - const priceAsset = this.store.getConfig().PriceAsset; + const stableCoin = this.store.getConfig().StableCoin; const coinsRaisedAmidMarkedDown: CoinScoreMap = {}; - const updatedScores: CoinScoreMap = {}; const prices = this.exchange.getPrices(); Object.keys(prices).forEach(s => { - // skip symbols that are not spot trading - if (s.match(/^\w+(UP|DOWN|BEAR|BULL)\w+$/)) { - return; - } - const coinName = s.endsWith(priceAsset) ? s.split(priceAsset)[0] : null; + const coinName = s.endsWith(stableCoin) ? s.split(stableCoin)[0] : null; if (coinName) { const price = prices[s]; const score = CoinScore.new(coinName, scores[s]); score.pushPrice(price) score.priceGoesUp() && (coinsRaisedAmidMarkedDown[s] = score) - updatedScores[s] = score; + scores[s] = score; } }) @@ -74,11 +72,11 @@ export class Survivors implements ScoresManager { Log.info(`Updated survivors.`); } - CacheProxy.put("RecommenderMemos", JSON.stringify(updatedScores)); + CacheProxy.put("RecommenderMemos", JSON.stringify(scores)); // Sync the scores to store every 6 hours if (!CacheProxy.get("SurvivorScoresSynced")) { - this.store.set("SurvivorScores", updatedScores); + this.store.set("SurvivorScores", scores); CacheProxy.put("SurvivorScoresSynced", "true", 6 * 60 * 60); // 6 hours } } diff --git a/apps-script/TradeResult.ts b/apps-script/TradeResult.ts index e785ab77..e6a3ad71 100644 --- a/apps-script/TradeResult.ts +++ b/apps-script/TradeResult.ts @@ -3,12 +3,6 @@ export enum PriceProvider { CoinStats = "CoinStats", } -export enum StableCoin { - USDT = "USDT", - USDC = "USDC", - BUSD = "BUSD", -} - export class ExchangeSymbol { readonly quantityAsset: string readonly priceAsset: string diff --git a/apps-script/Trader.ts b/apps-script/Trader.ts index 3f0a2e41..7c8c07b3 100644 --- a/apps-script/Trader.ts +++ b/apps-script/Trader.ts @@ -80,9 +80,9 @@ export class V2Trader { const priceGoesUp = tm.priceGoesUp() if (tm.profitLimitCrossedUp(this.config.ProfitLimit)) { - Log.alert(`${symbol} crossed profit limit`) + Log.alert(`${symbol} crossed profit limit at ${tm.currentPrice}`) } else if (tm.lossLimitCrossedDown()) { - Log.alert(`${symbol}: crossed stop limit`) + Log.alert(`${symbol}: crossed stop limit at ${tm.currentPrice}`) } if (tm.currentPrice < tm.stopLimitPrice) { @@ -174,7 +174,7 @@ export class V2Trader { } private getBNBCommissionCost(commission: number): number { - const bnbPrice = this.prices["BNB" + this.config.PriceAsset]; + const bnbPrice = this.prices["BNB" + this.config.StableCoin]; return bnbPrice ? commission * bnbPrice : 0; } } diff --git a/apps-script/TradesQueue.ts b/apps-script/TradesQueue.ts index 40cad90c..96b48e00 100644 --- a/apps-script/TradesQueue.ts +++ b/apps-script/TradesQueue.ts @@ -35,7 +35,7 @@ export class TradesQueue { Object.keys(queue).forEach(coinName => { try { - const symbol = new ExchangeSymbol(coinName, config.PriceAsset); + const symbol = new ExchangeSymbol(coinName, config.StableCoin); const action = queue[coinName]; if (action === QueueAction.BUY) { const trade = store.getTrade(symbol) || new TradeMemo(new TradeResult(symbol)); diff --git a/apps-script/api.ts b/apps-script/api.ts index 6d4bf720..4ae9c365 100644 --- a/apps-script/api.ts +++ b/apps-script/api.ts @@ -17,7 +17,7 @@ function doPost(e) { return "404"; } -function catchError(fn: () => any): any { +function catchError(fn: () => T): T { try { return fn(); } catch (e) { @@ -40,7 +40,7 @@ function initialSetup(params: InitialSetupParams): string { config.SECRET = params.binanceSecretKey || config.SECRET; if (config.KEY && config.SECRET) { Log.alert("Checking if Binance is reachable"); - new Exchange(config).getFreeAsset(config.PriceAsset); + new Exchange(config).getFreeAsset(config.StableCoin); Log.alert("Connected to Binance"); // @ts-ignore Start(); @@ -121,7 +121,7 @@ function getConfig(): Config { }); } -function setConfig(config): void { +function setConfig(config): string { return catchError(() => { DefaultStore.setConfig(config); return "Config updated"; @@ -145,3 +145,10 @@ function resetSurvivors(): void { return new Survivors(DefaultStore, exchange).resetScores(); }); } + +function getCoinNames(): string[] { + return catchError(() => { + const exchange = new Exchange(DefaultStore.getConfig()); + return exchange.getCoinNames(); + }) +} diff --git a/apps-script/shared-lib/types.ts b/apps-script/shared-lib/types.ts index 57a5e76f..c688fc74 100644 --- a/apps-script/shared-lib/types.ts +++ b/apps-script/shared-lib/types.ts @@ -1,7 +1,15 @@ +export enum StableUSDCoin { + USDT = "USDT", + USDC = "USDC", + BUSD = "BUSD", + UST = "UST", + DAI = "DAI", +} + export class CoinScore { - private static readonly PRICES_MAX_CAP = 4; + private static readonly PRICES_MAX_CAP = 5; /** - * `r` is the number of times this memo was going up when 90% of marked was going down + * `r` is the number of times this memo was going up when the rest of the market wasn't. */ private r: number = 0 private p: number[] = [] diff --git a/src/App.tsx b/src/App.tsx index e2bab871..1211588e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,7 +78,8 @@ export default function App() { {fetchingData && } {fetchDataError && {fetchDataError} - Please check your Google Apps Script application is deployed and try again. + Please check your network connection and that Google Apps Script application is + deployed and try again. } {!fetchingData && initialSetup && } {!fetchingData && !initialSetup && diff --git a/src/components/Assets.tsx b/src/components/Assets.tsx index 9d1719ab..c40ff50a 100644 --- a/src/components/Assets.tsx +++ b/src/components/Assets.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import Trade from "./Trade"; import {TradeMemo, TradeState} from "../../apps-script/TradeMemo"; -import {Badge, Button, Grid, Stack, TextField, ToggleButton, ToggleButtonGroup} from "@mui/material"; +import {Autocomplete, Badge, Button, Grid, Stack, TextField, ToggleButton, ToggleButtonGroup} from "@mui/material"; import {Config} from "../../apps-script/Store"; import {useEffect} from "react"; @@ -23,6 +23,7 @@ const groupByState = (trades: { [key: string]: TradeMemo }): Map({}); + const [coinNames, setCoinNames] = React.useState([] as string[]); useEffect(() => { google.script.run.withSuccessHandler(setTrades).getTrades(); @@ -30,6 +31,10 @@ export function Assets({config}: { config: Config }) { return () => clearInterval(interval); }, []); + useEffect(() => { + google.script.run.withSuccessHandler(setCoinNames).getCoinNames(); + }, []); + const [state, setState] = React.useState(TradeState.BOUGHT); const changeState = (e, newState) => setState(newState); @@ -66,8 +71,11 @@ export function Assets({config}: { config: Config }) { - setCoinName(e.target.value)}/> + setCoinName(val)} + disableClearable={true} + renderInput={(params) => } + /> diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 43397deb..81a3f5a3 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -2,9 +2,21 @@ import * as React from 'react'; import {useEffect, useState} from 'react'; import SaveIcon from '@mui/icons-material/Save'; import {Config} from "../../apps-script/Store"; -import {Box, Button, FormControlLabel, InputAdornment, Snackbar, Stack, Switch, TextField,} from "@mui/material"; +import { + Alert, + Autocomplete, + Box, + Button, + FormControlLabel, + InputAdornment, + Snackbar, + Stack, + Switch, + TextField, +} from "@mui/material"; import {circularProgress} from "./Common"; import {PriceProvider} from "../../apps-script/TradeResult"; +import {StableUSDCoin} from "../../apps-script/shared-lib/types"; export function Settings() { const [isSaving, setIsSaving] = useState(false); @@ -13,7 +25,7 @@ export function Settings() { const [config, setConfig] = useState({ BuyQuantity: 0, StopLimit: 0, - PriceAsset: "", + StableCoin: '' as StableUSDCoin, SellAtStopLimit: false, SellAtProfitLimit: false, ProfitLimit: 0, @@ -34,6 +46,12 @@ export function Settings() { }).getConfig(), []) const onSave = () => { + if (!config.StableCoin) { + setError('Stable Coin is required'); + return + } + setError(null); + isFinite(+stopLimit) && (config.StopLimit = +stopLimit / 100); isFinite(+profitLimit) && (config.ProfitLimit = +profitLimit / 100); isFinite(+buyQuantity) && (config.BuyQuantity = Math.floor(+buyQuantity)); @@ -54,8 +72,14 @@ export function Settings() { return ( - setConfig({...config, PriceAsset: e.target.value})} + val && setConfig({...config, StableCoin: val as StableUSDCoin})} + onInputChange={(e, val) => setConfig({...config, StableCoin: val as StableUSDCoin})} + renderInput={(params) => } /> setBuyQuantity(e.target.value)} InputProps={{startAdornment: $}} @@ -103,8 +127,8 @@ export function Settings() { onClick={onSave} disabled={isSaving}>Save {isSaving && circularProgress} + {error && {error}} - {error && } ); }