Skip to content

Commit

Permalink
v2.3.2 (#20)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bogdantimes authored May 12, 2022
1 parent 0c10ea3 commit 53f7a58
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 44 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 7 additions & 1 deletion apps-script/Binance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
13 changes: 13 additions & 0 deletions apps-script/Exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
}
16 changes: 13 additions & 3 deletions apps-script/Store.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -90,6 +91,11 @@ export class FirebaseStore implements IStore {
delete configCache.LossLimit
}

if (configCache.PriceAsset) {
configCache.StableCoin = <StableUSDCoin>configCache.PriceAsset
delete configCache.PriceAsset
}

CacheProxy.put("Config", JSON.stringify(configCache))

return configCache;
Expand Down Expand Up @@ -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
Expand All @@ -194,6 +200,10 @@ export type Config = {
*/
AveragingDown: boolean

/**
* @deprecated
*/
PriceAsset?: string
/**
* @deprecated
*/
Expand Down
30 changes: 14 additions & 16 deletions apps-script/Survivors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
})

Expand All @@ -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
}
}
Expand Down
6 changes: 0 additions & 6 deletions apps-script/TradeResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions apps-script/Trader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion apps-script/TradesQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
13 changes: 10 additions & 3 deletions apps-script/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function doPost(e) {
return "404";
}

function catchError(fn: () => any): any {
function catchError<T>(fn: () => T): T {
try {
return fn();
} catch (e) {
Expand All @@ -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();
Expand Down Expand Up @@ -121,7 +121,7 @@ function getConfig(): Config {
});
}

function setConfig(config): void {
function setConfig(config): string {
return catchError(() => {
DefaultStore.setConfig(config);
return "Config updated";
Expand All @@ -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();
})
}
12 changes: 10 additions & 2 deletions apps-script/shared-lib/types.ts
Original file line number Diff line number Diff line change
@@ -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[] = []
Expand Down
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ export default function App() {
{fetchingData && <Box sx={{width: '100%'}}><LinearProgress/></Box>}
{fetchDataError && <Alert severity="error">
<Typography variant="caption">{fetchDataError}</Typography>
<Typography variant="caption">Please check your Google Apps Script application is deployed and try again.</Typography>
<Typography variant="caption">Please check your network connection and that Google Apps Script application is
deployed and try again.</Typography>
</Alert>}
{!fetchingData && initialSetup && <InitialSetup config={config} onConnect={initialFetch}/>}
{!fetchingData && !initialSetup &&
Expand Down
14 changes: 11 additions & 3 deletions src/components/Assets.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -23,13 +23,18 @@ const groupByState = (trades: { [key: string]: TradeMemo }): Map<TradeState, Tra

export function Assets({config}: { config: Config }) {
const [trades, setTrades] = React.useState<{ [k: string]: TradeMemo }>({});
const [coinNames, setCoinNames] = React.useState([] as string[]);

useEffect(() => {
google.script.run.withSuccessHandler(setTrades).getTrades();
const interval = setInterval(google.script.run.withSuccessHandler(setTrades).getTrades, 30000); // 30 seconds
return () => clearInterval(interval);
}, []);

useEffect(() => {
google.script.run.withSuccessHandler(setCoinNames).getCoinNames();
}, []);

const [state, setState] = React.useState<TradeState>(TradeState.BOUGHT);
const changeState = (e, newState) => setState(newState);

Expand Down Expand Up @@ -66,8 +71,11 @@ export function Assets({config}: { config: Config }) {
</Grid>
<Grid item>
<Stack sx={sx} direction={"row"} spacing={2}>
<TextField fullWidth={true} label="Coin name" value={coinName}
onChange={(e) => setCoinName(e.target.value)}/>
<Autocomplete value={coinName} fullWidth={true} options={coinNames}
onChange={(e, val) => setCoinName(val)}
disableClearable={true}
renderInput={(params) => <TextField {...params} label={"Coin Name"}/>}
/>
<Button variant="contained" onClick={buy}>Buy</Button>
</Stack>
</Grid>
Expand Down
34 changes: 29 additions & 5 deletions src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -13,7 +25,7 @@ export function Settings() {
const [config, setConfig] = useState<Config>({
BuyQuantity: 0,
StopLimit: 0,
PriceAsset: "",
StableCoin: '' as StableUSDCoin,
SellAtStopLimit: false,
SellAtProfitLimit: false,
ProfitLimit: 0,
Expand All @@ -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));
Expand All @@ -54,8 +72,14 @@ export function Settings() {
return (
<Box sx={{justifyContent: 'center', display: 'flex', '& .MuiTextField-root': {width: '25ch'}}}>
<Stack spacing={2}>
<TextField value={config.PriceAsset} label={"Stable Coin"}
onChange={e => setConfig({...config, PriceAsset: e.target.value})}
<Autocomplete
freeSolo
value={config.StableCoin}
inputValue={config.StableCoin}
options={Object.values(StableUSDCoin)}
onChange={(e, val) => val && setConfig({...config, StableCoin: val as StableUSDCoin})}
onInputChange={(e, val) => setConfig({...config, StableCoin: val as StableUSDCoin})}
renderInput={(params) => <TextField {...params} label={"Stable Coin"}/>}
/>
<TextField value={buyQuantity} label={"Buy Quantity"} onChange={e => setBuyQuantity(e.target.value)}
InputProps={{startAdornment: <InputAdornment position="start">$</InputAdornment>}}
Expand Down Expand Up @@ -103,8 +127,8 @@ export function Settings() {
onClick={onSave} disabled={isSaving}>Save</Button>
{isSaving && circularProgress}
</Box>
{error && <Alert severity="error">{error}</Alert>}
</Stack>
{error && <Snackbar open={!!error} message={error}/>}
</Box>
);
}

0 comments on commit 53f7a58

Please sign in to comment.