示例#1
0
    def get_action(self, bot, update, args):
        # TODO: Do this in every plugin
        keywords = utl.get_kw(args)
        arg_list = utl.del_kw(args)

        # TODO: Do this in most other plugins
        if not arg_list:
            if not keywords.get(Keyword.INLINE):
                update.message.reply_text(text=f"Usage:\n{self.get_usage()}",
                                          parse_mode=ParseMode.MARKDOWN)
            return

        vs_cur = str()

        if "-" in arg_list[0]:
            pair = arg_list[0].split("-", 1)
            vs_cur = pair[1].upper()
            coin = pair[0].upper()
        else:
            coin = arg_list[0].upper()

        exchange = str()
        if len(arg_list) > 1:
            exchange = arg_list[1]

        try:
            response = APICache.get_cg_coins_list()
        except Exception as e:
            return self.handle_error(e, update)

        coin_id = str()
        coin_name = str()
        for entry in response:
            if entry["symbol"].upper() == coin:
                coin_id = entry["id"]
                coin_name = entry["name"]
                break

        if RateLimit.limit_reached(update):
            return

        cg = CoinGecko()
        msg = str()

        if exchange:
            try:
                result = cg.get_coin_by_id(coin_id)
            except Exception as e:
                return self.handle_error(e, update)

            if result:
                vs_list = list()

                if vs_cur:
                    vs_list = vs_cur.split(",")

                for ticker_len in result["tickers"]:
                    if ticker_len["market"]["name"].upper() == exchange.upper(
                    ):
                        if ticker_len["base"] != coin:
                            base_coin = ticker_len["base"]
                        else:
                            base_coin = ticker_len["target"]

                        if vs_list:
                            if base_coin in vs_list:
                                price = utl.format(ticker_len["last"],
                                                   force_length=True)
                                msg += f"`{base_coin}: {price}`\n"
                        else:
                            price = utl.format(ticker_len["last"],
                                               force_length=True)
                            msg += f"`{base_coin}: {price}`\n"
        else:
            if not vs_cur:
                if coin == "BTC":
                    vs_cur = "ETH,USD,EUR"
                elif coin == "ETH":
                    vs_cur = "BTC,USD,EUR"
                else:
                    vs_cur = "BTC,ETH,USD,EUR"

            try:
                result = cg.get_simple_price(coin_id, vs_cur)
            except Exception as e:
                return self.handle_error(e, update)

            if result:
                for symbol, price in next(iter(result.values())).items():
                    if symbol in utl.get_fiat_list():
                        if decimal.Decimal(
                                str(price)).as_tuple().exponent > -3:
                            price = utl.format(price,
                                               decimals=2,
                                               force_length=True)
                        else:
                            price = utl.format(price, force_length=True)
                    else:
                        price = utl.format(price, force_length=True)

                    msg += f"`{symbol.upper()}: {price}`\n"

        if msg:
            if exchange:
                ticker_len = 0
                for line in msg.split("\n"):
                    length = len(line[:line.find(":")])
                    if ticker_len < length:
                        ticker_len = length

                message = str()
                for line in msg.split("\n"):
                    if line:
                        lst = line.split(" ")
                        index = ticker_len + 2 + len(lst[1]) - len(lst[0])
                        price = "{1:>{0}}".format(index, lst[1])
                        message += f"{lst[0]}{price}\n"

                msg = f"`{coin} ({coin_name}) on {exchange.capitalize()}`\n\n" + message
            else:
                msg = f"`{coin} ({coin_name})`\n\n" + msg

            # Add link to source of data (CoinGecko)
            msg += f"\n[Details on CoinGecko]({self.CG_URL}{coin_id})"
        else:
            msg = f"{emo.ERROR} Can't retrieve data for *{coin}*"

        if keywords.get(Keyword.INLINE):
            return msg

        self.send_msg(msg, update, keywords)
示例#2
0
    def get_action(self, bot, update, args):
        keywords = utl.get_kw(args)
        arg_list = utl.del_kw(args)

        if not arg_list:
            if not keywords.get(Keyword.INLINE):
                update.message.reply_text(text=f"Usage:\n{self.get_usage()}",
                                          parse_mode=ParseMode.MARKDOWN)
            return

        time_frame = 72
        resolution = "HOUR"
        base_coin = "BTC"

        # Coin or pair
        if "-" in arg_list[0]:
            pair = arg_list[0].split("-", 1)
            base_coin = pair[1].upper()
            coin = pair[0].upper()
        else:
            coin = arg_list[0].upper()

        if coin == "BTC" and base_coin == "BTC":
            base_coin = "USD"

        if coin == base_coin:
            update.message.reply_text(
                text=f"{emo.ERROR} Can't compare *{coin}* to itself",
                parse_mode=ParseMode.MARKDOWN)
            return

        if RateLimit.limit_reached(update):
            return

        cmc_thread = threading.Thread(target=self._get_cmc_coin_id,
                                      args=[coin])
        cmc_thread.start()

        # Time frame
        if len(arg_list) > 1:
            if arg_list[1].lower().endswith(
                    "m") and arg_list[1][:-1].isnumeric():
                resolution = "MINUTE"
                time_frame = arg_list[1][:-1]
            elif arg_list[1].lower().endswith(
                    "h") and arg_list[1][:-1].isnumeric():
                resolution = "HOUR"
                time_frame = arg_list[1][:-1]
            elif arg_list[1].lower().endswith(
                    "d") and arg_list[1][:-1].isnumeric():
                resolution = "DAY"
                time_frame = arg_list[1][:-1]
            else:
                update.message.reply_text(
                    text=f"{emo.ERROR} Argument *{arg_list[1]}* is invalid",
                    parse_mode=ParseMode.MARKDOWN)
                return

        try:
            if resolution == "MINUTE":
                ohlcv = CryptoCompare().get_historical_ohlcv_minute(
                    coin, base_coin, time_frame)
            elif resolution == "HOUR":
                ohlcv = CryptoCompare().get_historical_ohlcv_hourly(
                    coin, base_coin, time_frame)
            elif resolution == "DAY":
                ohlcv = CryptoCompare().get_historical_ohlcv_daily(
                    coin, base_coin, time_frame)
            else:
                ohlcv = CryptoCompare().get_historical_ohlcv_hourly(
                    coin, base_coin, time_frame)
        except Exception as e:
            return self.handle_error(e, update)

        if ohlcv["Response"] == "Error":
            if ohlcv["Message"] == "limit is larger than max value.":
                update.message.reply_text(
                    text=f"{emo.ERROR} Time frame can't be larger "
                    f"then *{con.CG_DATA_LIMIT}* data points",
                    parse_mode=ParseMode.MARKDOWN)
                return
            elif ohlcv["Message"].startswith(
                    "There is no data for the symbol"):
                ohlcv = None
            else:
                update.message.reply_text(
                    text=f"{emo.ERROR} CoinGecko ERROR: {ohlcv['Message']}",
                    parse_mode=ParseMode.MARKDOWN)
                return

        ohlcv = ohlcv["Data"]

        if ohlcv:
            try:
                o = [value["open"] for value in ohlcv]
                h = [value["high"] for value in ohlcv]
                l = [value["low"] for value in ohlcv]
                c = [value["close"] for value in ohlcv]
                t = [value["time"] for value in ohlcv]
            except:
                return self.handle_error(f"No OHLC data for {coin}", update)

        if not ohlcv or utl.all_same(o, h, l, c):
            if base_coin != "BTC" and base_coin != "USD":
                update.message.reply_text(
                    text=f"{emo.ERROR} Base currency for "
                    f"*{coin}* can only be *BTC* or *USD*",
                    parse_mode=ParseMode.MARKDOWN)
                return

            # Time frame
            if len(arg_list) > 1:
                if resolution != "DAY":
                    update.message.reply_text(
                        text=f"{emo.ERROR} Timeframe for *{coin}* "
                        f"can only be specified in days",
                        parse_mode=ParseMode.MARKDOWN)
                    return
                else:
                    time_frame = int(time_frame)
            else:
                time_frame = 60  # Days

            try:
                cp_ohlc = APICache.get_cp_coin_list()
            except Exception as e:
                return self.handle_error(e, update)

            for c in cp_ohlc:
                if c["symbol"] == coin:
                    # Current datetime in seconds
                    t_now = time.time()
                    # Convert chart time span to seconds
                    time_frame = time_frame * 24 * 60 * 60
                    # Start datetime for chart in seconds
                    t_start = t_now - int(time_frame)

                    try:
                        ohlcv = CoinPaprika().get_historical_ohlc(
                            c["id"],
                            int(t_start),
                            end=int(t_now),
                            quote=base_coin.lower(),
                            limit=366)
                    except Exception as e:
                        return self.handle_error(e, update)

                    cp_api = True
                    break

            if not ohlcv:
                update.message.reply_text(
                    text=f"{emo.ERROR} No OHLC data for *{coin}* "
                    f"available or time frame too big",
                    parse_mode=ParseMode.MARKDOWN)
                return

            try:
                o = [value["open"] for value in ohlcv]
                h = [value["high"] for value in ohlcv]
                l = [value["low"] for value in ohlcv]
                c = [value["close"] for value in ohlcv]
                t = [
                    time.mktime(dau.parse(value["time_close"]).timetuple())
                    for value in ohlcv
                ]
            except:
                return self.handle_error(f"No OHLC data for {coin}", update)

        margin_l = 140
        tickformat = "0.8f"

        max_value = max(h)
        if max_value > 0.9:
            if max_value > 999:
                margin_l = 120
                tickformat = "0,.0f"
            else:
                margin_l = 125
                tickformat = "0.2f"

        try:
            fig = fif.create_candlestick(o, h, l, c, pd.to_datetime(t,
                                                                    unit='s'))
        except Exception as e:
            return self.handle_error(e, update)

        fig['layout']['yaxis'].update(tickformat=tickformat,
                                      tickprefix="   ",
                                      ticksuffix=f"  ")

        fig['layout'].update(title=dict(text=coin, font=dict(size=26)),
                             yaxis=dict(title=dict(text=base_coin,
                                                   font=dict(size=18)), ))

        fig['layout'].update(shapes=[{
            "type": "line",
            "xref": "paper",
            "yref": "y",
            "x0": 0,
            "x1": 1,
            "y0": c[len(c) - 1],
            "y1": c[len(c) - 1],
            "line": {
                "color": "rgb(50, 171, 96)",
                "width": 1,
                "dash": "dot"
            }
        }])

        fig['layout'].update(paper_bgcolor='rgb(233,233,233)',
                             plot_bgcolor='rgb(233,233,233)',
                             autosize=False,
                             width=800,
                             height=600,
                             margin=go.layout.Margin(l=margin_l,
                                                     r=50,
                                                     b=85,
                                                     t=100,
                                                     pad=4))

        cmc_thread.join()

        fig['layout'].update(images=[
            dict(source=f"{con.CMC_LOGO_URL_PARTIAL}{self.cmc_coin_id}.png",
                 opacity=0.8,
                 xref="paper",
                 yref="paper",
                 x=1.05,
                 y=1,
                 sizex=0.2,
                 sizey=0.2,
                 xanchor="right",
                 yanchor="bottom")
        ])

        self.send_photo(
            io.BufferedReader(BytesIO(pio.to_image(fig, format='jpeg'))),
            update, keywords)
示例#3
0
    def get_action(self, bot, update, args):
        keywords = utl.get_kw(args)
        arg_list = utl.del_kw(args)

        if not arg_list:
            if not keywords.get(Keyword.INLINE):
                update.message.reply_text(text=f"Usage:\n{self.get_usage()}",
                                          parse_mode=ParseMode.MARKDOWN)
            return

        if RateLimit.limit_reached(update):
            return

        coin = arg_list[0].upper()
        data = None
        cgid = None

        try:
            response = APICache.get_cg_coins_list()
        except Exception as e:
            return self.handle_error(e, update)

        # Get coin ID and data
        for entry in response:
            if entry["symbol"].upper() == coin:
                try:
                    data = CoinGecko().get_coin_by_id(entry["id"])
                except Exception as e:
                    return self.handle_error(e, update)

                cgid = entry["id"]
                break

        if not data:
            update.message.reply_text(text=f"{emo.ERROR} No data for *{coin}*",
                                      parse_mode=ParseMode.MARKDOWN)
            return

        name = data["name"]
        symbol = data["symbol"].upper()

        rank_mc = data["market_cap_rank"]
        rank_cg = data["coingecko_rank"]

        cs = int(float(data['market_data']['circulating_supply']))
        sup_c = f"{utl.format(cs)} {symbol}"

        if data["market_data"]["total_supply"]:
            ts = int(float(data["market_data"]["total_supply"]))
            sup_t = f"{utl.format(ts)} {symbol}"
        else:
            sup_t = "N/A"

        usd = data["market_data"]["current_price"]["usd"]
        eur = data["market_data"]["current_price"]["eur"]
        btc = data["market_data"]["current_price"]["btc"]
        eth = data["market_data"]["current_price"]["eth"]

        p_usd = utl.format(usd, force_length=True)
        p_eur = utl.format(eur, force_length=True, template=p_usd)
        p_btc = utl.format(btc, force_length=True, template=p_usd)
        p_eth = utl.format(eth, force_length=True, template=p_usd)

        p_usd = "{:>12}".format(p_usd)
        p_eur = "{:>12}".format(p_eur)
        p_btc = "{:>12}".format(p_btc)
        p_eth = "{:>12}".format(p_eth)

        # Do not display BTC or ETH price if coin is BTC or ETH
        btc_str = "" if coin == "BTC" else f"BTC {p_btc}\n"
        eth_str = "" if coin == "ETH" else f"ETH {p_eth}\n"

        v_24h = utl.format(
            int(float(data["market_data"]["total_volume"]["usd"])))
        m_cap = utl.format(int(float(
            data["market_data"]["market_cap"]["usd"])))

        if data["market_data"]["price_change_percentage_1h_in_currency"]:
            c_1h = data["market_data"][
                "price_change_percentage_1h_in_currency"]["usd"]
            c1h = utl.format(float(c_1h), decimals=2, force_length=True)
            h1 = "{:>10}".format(f"{c1h}%")
        else:
            h1 = "{:>10}".format("N/A")

        if data["market_data"]["price_change_percentage_24h_in_currency"]:
            c_1d = data["market_data"][
                "price_change_percentage_24h_in_currency"]["usd"]
            c1d = utl.format(float(c_1d), decimals=2, force_length=True)
            d1 = "{:>10}".format(f"{c1d}%")
        else:
            d1 = "{:>10}".format("N/A")

        if data["market_data"]["price_change_percentage_7d_in_currency"]:
            c_1w = data["market_data"][
                "price_change_percentage_7d_in_currency"]["usd"]
            c1w = utl.format(float(c_1w), decimals=2, force_length=True)
            w1 = "{:>10}".format(f"{c1w}%")
        else:
            w1 = "{:>10}".format("N/A")

        if data["market_data"]["price_change_percentage_30d_in_currency"]:
            c_1m = data["market_data"][
                "price_change_percentage_30d_in_currency"]["usd"]
            c1m = utl.format(float(c_1m), decimals=2, force_length=True)
            m1 = "{:>10}".format(f"{c1m}%")
        else:
            m1 = "{:>10}".format("N/A")

        if data["market_data"]["price_change_percentage_1y_in_currency"]:
            c_1y = data["market_data"][
                "price_change_percentage_1y_in_currency"]["usd"]
            c1y = utl.format(float(c_1y), decimals=2, force_length=True)
            y1 = "{:>10}".format(f"{c1y}%")
        else:
            y1 = "{:>10}".format("N/A")

        msg = f"`" \
              f"{name} ({symbol})\n\n" \
              f"USD {p_usd}\n" \
              f"EUR {p_eur}\n" \
              f"{btc_str}" \
              f"{eth_str}\n" \
              f"Hour  {h1}\n" \
              f"Day   {d1}\n" \
              f"Week  {w1}\n" \
              f"Month {m1}\n" \
              f"Year  {y1}\n\n" \
              f"Market Cap Rank: {rank_mc}\n" \
              f"Coin Gecko Rank: {rank_cg}\n\n" \
              f"Volume 24h: {v_24h} USD\n" \
              f"Market Cap: {m_cap} USD\n" \
              f"Circ. Supp: {sup_c}\n" \
              f"Total Supp: {sup_t}\n\n" \
              f"`" \
              f"Stats on [CoinGecko](https://www.coingecko.com/en/coins/{cgid}) & " \
              f"[Coinlib](https://coinlib.io/coin/{coin}/{coin})"

        if keywords.get(Keyword.INLINE):
            return msg

        self.send_msg(msg, update, keywords)
示例#4
0
    def get_action(self, bot, update, args):
        keywords = utl.get_kw(args)
        arg_list = utl.del_kw(args)

        if not arg_list:
            if not keywords.get(Keyword.INLINE):
                update.message.reply_text(
                    text=f"Usage:\n{self.get_usage()}",
                    parse_mode=ParseMode.MARKDOWN)
            return

        if RateLimit.limit_reached(update):
            return

        vs_cur = "usd"

        if "-" in arg_list[0]:
            pair = arg_list[0].split("-", 1)
            vs_cur = pair[1].lower()
            coin = pair[0].upper()
        else:
            coin = arg_list[0].upper()

        ath_date = str()
        ath_price = str()
        cur_price = str()
        ath_change = str()

        # Get coin ID
        try:
            response = APICache.get_cg_coins_list()
        except Exception as e:
            return self.handle_error(e, update)

        for entry in response:
            if entry["symbol"].lower() == coin.lower():
                try:
                    coin_info = CoinGecko().get_coin_by_id(entry["id"])
                except Exception as e:
                    return self.handle_error(e, update)

                cur_price = coin_info["market_data"]["current_price"]
                ath_price = coin_info["market_data"]["ath"]
                ath_date = coin_info["market_data"]["ath_date"]
                ath_change = coin_info["market_data"]["ath_change_percentage"]
                break

        msg = str()

        for c in vs_cur.split(","):
            if c in ath_price:
                ath_p = format(ath_price[c])
                cur_p = format(cur_price[c], template=ath_p)
                change = format(ath_change[c], decimals=2)

                date_time = ath_date[c]
                date_ath = date_time[:10]
                date_list = date_ath.split("-")
                y = int(date_list[0])
                m = int(date_list[1])
                d = int(date_list[2])

                ath = date(y, m, d)
                now = datetime.date.today()

                ath_p_str = f"Price ATH: {ath_p} {c.upper()}\n"
                cur_p_str = f"Price NOW: {cur_p.rjust(len(ath_p))} {c.upper()}\n"

                msg += f"`" \
                       f"{date_ath} ({(now - ath).days} days ago)\n" \
                       f"{ath_p_str}" \
                       f"{cur_p_str}" \
                       f"Change: {change}%\n\n" \
                       f"`"

        if msg:
            msg = f"`All-Time High for {coin}`\n\n {msg}"
        else:
            msg = f"{emo.ERROR} Can't retrieve data for *{coin}*"

        if keywords.get(Keyword.INLINE):
            return msg

        self.send_msg(msg, update, keywords)
示例#5
0
    def get_action(self, bot, update, args):
        keywords = utl.get_kw(args)
        arg_list = utl.del_kw(args)

        if arg_list:
            t = arg_list[0].lower()
            if not t == "hour" and not t == "day":
                if not keywords.get(Keyword.INLINE):
                    update.message.reply_text(
                        text=
                        f"{emo.ERROR} First argument has to be `day` or `hour`",
                        parse_mode=ParseMode.MARKDOWN)
                return

        if len(arg_list) > 1:
            entries = arg_list[1]
            if not entries.isnumeric():
                if not keywords.get(Keyword.INLINE):
                    update.message.reply_text(
                        text=f"{emo.ERROR} Second argument (# of positions "
                        f"to display) has to be a number",
                        parse_mode=ParseMode.MARKDOWN)
                return

        if len(arg_list) > 2:
            entries = arg_list[2]
            if not entries.isnumeric():
                if not keywords.get(Keyword.INLINE):
                    update.message.reply_text(
                        text=f"{emo.ERROR} Third argument (min. volume) "
                        f"has to be a number",
                        parse_mode=ParseMode.MARKDOWN)
                return

        if RateLimit.limit_reached(update):
            return

        period = CoinData.HOUR
        volume = None
        entries = 10

        if arg_list:
            # Period
            if arg_list[0].lower() == "hour":
                period = CoinData.HOUR
            elif arg_list[0].lower() == "day":
                period = CoinData.DAY
            else:
                period = CoinData.HOUR

            # Entries
            if len(arg_list) > 1 and arg_list[1].isnumeric():
                entries = int(arg_list[1])

            # Volume
            if len(arg_list) > 2 and arg_list[2].isnumeric():
                volume = int(arg_list[2])

        try:
            best = CoinData().get_movers(CoinData.BEST,
                                         period=period,
                                         entries=entries,
                                         volume=volume)
        except Exception as e:
            return self.handle_error(e, update)

        if not best:
            if not keywords.get(Keyword.INLINE):
                update.message.reply_text(
                    text=f"{emo.INFO} No matching data found",
                    parse_mode=ParseMode.MARKDOWN)
            return

        msg = str()

        for coin in best:
            name = coin["Name"]
            symbol = coin["Symbol"]
            desc = f"{name} ({symbol})"

            if len(desc) > self.DESC_LEN:
                desc = f"{desc[:self.DESC_LEN-3]}..."

            if period == CoinData.HOUR:
                change = coin["Change_1h"]
            else:
                change = coin["Change_24h"]

            change = utl.format(change, decimals=2, force_length=True)
            change = "{1:>{0}}".format(self.DESC_LEN + 9 - len(desc), change)
            msg += f"`{desc}{change}%`\n"

        vol = str()
        if volume:
            vol = f" (vol > {utl.format(volume)})"

        msg = f"`Best movers 1{period.lower()[:1]}{vol}\n\n`{msg}"

        if keywords.get(Keyword.INLINE):
            return msg

        self.send_msg(msg, update, keywords)
示例#6
0
    def get_action(self, bot, update, args):
        keywords = utl.get_kw(args)
        arg_list = utl.del_kw(args)

        if not arg_list:
            if not keywords.get(Keyword.INLINE):
                update.message.reply_text(text=f"Usage:\n{self.get_usage()}",
                                          parse_mode=ParseMode.MARKDOWN)
            return

        time_frame = 3  # Days
        base_coin = "BTC"

        if "-" in arg_list[0]:
            pair = arg_list[0].split("-", 1)
            base_coin = pair[1].upper()
            coin = pair[0].upper()
        else:
            coin = arg_list[0].upper()

        if coin == "BTC" and base_coin == "BTC":
            base_coin = "USD"

        if coin == base_coin:
            update.message.reply_text(
                text=f"{emo.ERROR} Can't compare *{coin}* to itself",
                parse_mode=ParseMode.MARKDOWN)
            return

        if len(arg_list) > 1 and arg_list[1].isnumeric():
            time_frame = arg_list[1]

        if RateLimit.limit_reached(update):
            return

        cg_thread = threading.Thread(target=self._get_cg_coin_id, args=[coin])
        cmc_thread = threading.Thread(target=self._get_cmc_coin_id,
                                      args=[coin])

        cg_thread.start()
        cmc_thread.start()

        cg_thread.join()

        if not self.cg_coin_id:
            update.message.reply_text(
                text=f"{emo.ERROR} Can't retrieve data for *{coin}*",
                parse_mode=ParseMode.MARKDOWN)
            return

        try:
            market = CoinGecko().get_coin_market_chart_by_id(
                self.cg_coin_id, base_coin.lower(), time_frame)
        except Exception as e:
            return self.handle_error(e, update)

        # Volume
        df_volume = DataFrame(market["total_volumes"],
                              columns=["DateTime", "Volume"])
        df_volume["DateTime"] = pd.to_datetime(df_volume["DateTime"],
                                               unit="ms")
        volume = go.Scatter(x=df_volume.get("DateTime"),
                            y=df_volume.get("Volume"),
                            name="Volume")

        # Price
        df_price = DataFrame(market["prices"], columns=["DateTime", "Price"])
        df_price["DateTime"] = pd.to_datetime(df_price["DateTime"], unit="ms")
        price = go.Scatter(x=df_price.get("DateTime"),
                           y=df_price.get("Price"),
                           yaxis="y2",
                           name="Price",
                           line=dict(color=("rgb(22, 96, 167)"), width=2))

        cmc_thread.join()

        if not self.cmc_coin_id:
            update.message.reply_text(
                text=f"{emo.ERROR} Can't retrieve data for *{coin}*",
                parse_mode=ParseMode.MARKDOWN)
            return

        margin_l = 140
        tickformat = "0.8f"

        max_value = df_price["Price"].max()
        if max_value > 0.9:
            if max_value > 999:
                margin_l = 110
                tickformat = "0,.0f"
            else:
                margin_l = 115
                tickformat = "0.2f"

        layout = go.Layout(
            images=[
                dict(
                    source=f"{con.CMC_LOGO_URL_PARTIAL}{self.cmc_coin_id}.png",
                    opacity=0.8,
                    xref="paper",
                    yref="paper",
                    x=1.05,
                    y=1,
                    sizex=0.2,
                    sizey=0.2,
                    xanchor="right",
                    yanchor="bottom")
            ],
            paper_bgcolor='rgb(233,233,233)',
            plot_bgcolor='rgb(233,233,233)',
            autosize=False,
            width=800,
            height=600,
            margin=go.layout.Margin(l=margin_l, r=50, b=85, t=100, pad=4),
            yaxis=dict(domain=[0, 0.20]),
            yaxis2=dict(title=dict(text=base_coin, font=dict(size=18)),
                        domain=[0.25, 1],
                        tickprefix="   ",
                        ticksuffix=f"  "),
            title=dict(text=coin, font=dict(size=26)),
            legend=dict(orientation="h",
                        yanchor="top",
                        xanchor="center",
                        y=1.05,
                        x=0.45),
            shapes=[{
                "type": "line",
                "xref": "paper",
                "yref": "y2",
                "x0": 0,
                "x1": 1,
                "y0": market["prices"][len(market["prices"]) - 1][1],
                "y1": market["prices"][len(market["prices"]) - 1][1],
                "line": {
                    "color": "rgb(50, 171, 96)",
                    "width": 1,
                    "dash": "dot"
                }
            }],
        )

        try:
            fig = go.Figure(data=[price, volume], layout=layout)
        except Exception as e:
            return self.handle_error(e, update)

        fig["layout"]["yaxis2"].update(tickformat=tickformat)

        self.send_photo(
            io.BufferedReader(BytesIO(pio.to_image(fig, format='jpeg'))),
            update, keywords)