Skip to content

Commit

Permalink
add visualization functions
Browse files Browse the repository at this point in the history
  • Loading branch information
asavinov committed Sep 24, 2023
1 parent 4b70b40 commit 09ad635
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 2 deletions.
239 changes: 239 additions & 0 deletions service/notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from datetime import timedelta, datetime

import asyncio

import pandas as pd
import requests

from service.App import *
Expand Down Expand Up @@ -224,5 +226,242 @@ async def generate_transaction_stats():
return profit, profit_percent, profit_descr, profit_percent_descr


async def send_diagram():
"""
Produce a line chart based on latest data and send it to the channel.
"""
symbol = App.config["symbol"]
signal = App.signal
description = App.config.get("description", "")
close_time = signal.get('close_time')

# Examples: every hour (say, 0th minute or 15th minute), every day (0th hour, 2nd hour etc.)
# If daily, then parameter is hour no., and we check that hour is this hour AND all lower params are 0 (minutes, seconds etc.)

# If hourly, then parameter is minute (or 0), and we check that current minute is 0 AND all other are 0 (but in our case we do not need this because maximum freq is 1 min)
if close_time.minute != 0:
return

#notify_frequency_minutes = 2
#if (close_time.minute % notify_frequency_minutes) != 0:
# return

#
# Prepare data to be visualized
#
freq = 'H'
nrows = 2*7*24 # 1 week 168 hours, 2 weeks 336 hours

# Get main df with high, low, close for the symbol.
df_ohlc = App.feature_df[['open', 'high', 'low', 'close']]
df_ohlc = resample_ohlc_data(df_ohlc.reset_index(), freq, nrows, buy_signal_column=None, sell_signal_column=None)

# Get transaction data.
df_t = load_all_transaction() # timestamp,price,profit,status
df_t['buy_long'] = df_t['status'].apply(lambda x: True if isinstance(x, str) and x == 'BUY' else False)
df_t['sell_long'] = df_t['status'].apply(lambda x: True if isinstance(x, str) and x == 'SELL' else False)
transactions_exist = len(df_t) > 0

if transactions_exist:
df_t = resample_transaction_data(df_t, freq, 0, 'buy_long', 'sell_long')
else:
df_t = None

# Merge because we need signals along with close price in one df
if transactions_exist:
df = df_ohlc.merge(df_t, how='left', left_on='timestamp', right_on='timestamp')
else:
df = df_ohlc

# Load score
score_exists = False

title = f"$\\bf{{{symbol}}}$"

buy_signal_threshold = App.config.get("signal_model", {}).get("parameters").get("buy_signal_threshold")
sell_signal_threshold = App.config.get("signal_model", {}).get("parameters").get("sell_signal_threshold")

fig = generate_chart(
df, title,
buy_signal_column="buy_long" if transactions_exist else None,
sell_signal_column="sell_long" if transactions_exist else None,
score_column="score" if score_exists else None,
thresholds=[buy_signal_threshold, sell_signal_threshold]
)

import io
with io.BytesIO() as buf:
fig.savefig(buf, format='png') # Convert and save in buffer
im_bytes = buf.getvalue() # Get complete content (while read returns from current position)
img_data = im_bytes

msg_txt = f""

#
# Send image
#
bot_token = App.config["telegram_bot_token"]
chat_id = App.config["telegram_chat_id"]

files = {'photo': img_data}
payload = {
'chat_id': chat_id,
'caption': msg_txt,
'parse_mode': 'markdown'
}

try:
# https://core.telegram.org/bots/api#sendphoto
url = 'https://api.telegram.org/bot' + bot_token + '/sendPhoto'
req = requests.post(url=url, data=payload, files=files)
response = req.json()
except Exception as e:
log.error(f"Error sending notification: {e}")


def resample_ohlc_data(df, freq, nrows, buy_signal_column, sell_signal_column):
"""
Resample ohlc data to lower frequency. Assumption: time in 'timestamp' column.
"""
# Aggregation functions
ohlc = {
'timestamp': 'first', # It will be in index
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
}

# These score columns are optional
if buy_signal_column:
# Buy signal if at least one buy signal was during this time interval
ohlc[buy_signal_column]: lambda x: True if not all(x == False) else False
# Alternatively, 0 if no buy signals, 1 if only 1 buy signal, 2 or -1 if more than 1 any signals (mixed interval)
if sell_signal_column:
# Sell signal if at least one sell signal was during this time interval
ohlc[sell_signal_column]: lambda x: True if not all(x == False) else False

df_out = df.resample(freq, on='timestamp').apply(ohlc)
del df_out['timestamp']
df_out.reset_index(inplace=True)

if nrows:
df_out = df_out.tail(nrows)

return df_out


def resample_transaction_data(df, freq, nrows, buy_signal_column, sell_signal_column):
"""
Given a list of transactions with arbitrary timestamps,
return a regular time series with True or False for the rows with transactions
Assumption: time in 'timestamp' column
PROBLEM: only one transaction per interval (1 hour) is possible so if we buy and then sell within one hour then we cannot represent this
Solution 1: remove
Solution 2: introduce a special symbol (like dot instead of arrows) which denotes one or more transactions - essentially error or inability to visualize
1 week 7*1440=10080 points, 5 min - 2016 points, 10 mins - 1008 points
"""
# Aggregation functions
transactions = {
'timestamp': 'first', # It will be in index
buy_signal_column: lambda x: True if not all(x == False) else False,
sell_signal_column: lambda x: True if not all(x == False) else False,
}

df_out = df.resample(freq, on='timestamp').apply(transactions)
del df_out['timestamp']
df_out.reset_index(inplace=True)

if nrows:
df_out = df_out.tail(nrows)

return df_out


def generate_chart(df, title, buy_signal_column, sell_signal_column, score_column, thresholds: list):
"""
All columns in one input df with desired length and desired freq
Visualize columns 1 (pre-defined): high, low, close
Visualize columns 1 (via parameters): buy_signal_column, sell_signal_column
Visualize columns 2: score_column (optional) - in [-1, +1]
Visualize columns 2: Threshold lines (as many as there are values in the list)
"""
from matplotlib import pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns

# List of colors: https://matplotlib.org/stable/gallery/color/named_colors.html
sns.set_style('white') # darkgrid, whitegrid, dark, white, ticks
#sns.color_palette("rocket")
#sns.set(rc={'axes.facecolor': 'gold', 'figure.facecolor': 'white'})
#sns.set(rc={'figure.facecolor': 'gold'})

fig, ax1 = plt.subplots(figsize=(12, 6))
# plt.tight_layout()

# === High, Low, Close

# Fill area between high and low
plt.fill_between(df.timestamp, df.low, df.high, step="mid", lw=0.0, facecolor='skyblue', alpha=.4) # edgecolor='red',

# Draw close price
sns.lineplot(data=df, x="timestamp", y="close", drawstyle='steps-mid', lw=.5, color='darkblue', ax=ax1)

# Buy/sell markters (list of timestamps)
# buy_df = df[df.buy_transaction]
# sell_df = df[df.sell_transaction]

# === Transactions

triangle_adj = 15
df["close_buy_adj"] = df["close"] - triangle_adj
df["close_sell_adj"] = df["close"] + triangle_adj

# markersize=6, markerfacecolor='blue'
sns.lineplot(data=df[df[buy_signal_column] == True], x="timestamp", y="close_buy_adj", lw=0, markerfacecolor="green", markersize=10, marker="^", alpha=0.6, ax=ax1)
sns.lineplot(data=df[df[sell_signal_column] == True], x="timestamp", y="close_sell_adj", lw=0, markerfacecolor="red", markersize=10, marker="v", alpha=0.6, ax=ax1)

# g2.set(yticklabels=[])
# g2.set(title='Penguins: Body Mass by Species for Gender')
ax1.set(xlabel=None) # remove the x-axis label
# g2.set(ylabel=None) # remove the y-axis label
ax1.set_ylabel('Close price', color='darkblue')
# g2.tick_params(left=False) # remove the ticks
min = df['low'].min()
max = df['high'].max()
ax1.set(ylim=(min - (max - min) * 0.05, max + (max - min) * 0.005))

ax1.xaxis.set_major_locator(mdates.DayLocator())
ax1.xaxis.set_major_formatter(mdates.DateFormatter("%d")) # "%H:%M:%S" "%d %b"
ax1.tick_params(axis="x", rotation=0)
#ax1.xaxis.grid(True)

# === Score

if score_column and score_column in df.columns:
ax2 = ax1.twinx()
# ax2.plot(x, y1, 'o-', color="red" )
sns.lineplot(data=df, x="timestamp", y=score_column, drawstyle='steps-mid', lw=.2, color="red", ax=ax2) # marker="v" "^" , markersize=12
ax2.set_ylabel('Score', color='r')
ax2.set_ylabel('Score', color='b')
ax2.set(ylim=(-0.5, +3.0))

ax2.axhline(0.0, lw=.1, color="black")

for threshold in thresholds:
ax2.axhline(threshold, lw=.1, color="red")
ax2.axhline(threshold, lw=.1, color="red")

# fig.suptitle("My figtitle", fontsize=14) # Positioned higher
# plt.title('Weekly: $\\bf{S&P 500}$', fontsize=16) # , weight='bold' or MathText
plt.title(title, fontsize=14)
# ax1.set_title('My Title')

# plt.show()

return fig


if __name__ == '__main__':
pass
7 changes: 5 additions & 2 deletions service/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ async def main_task():
return
# Signal is stored in App.signal

if "notify" in App.config["actions"]:
if "diagram" in App.config.get("actions", {}):
diagram_task = App.loop.create_task(send_diagram())

if "notify" in App.config.get("actions", {}):
notify_task = App.loop.create_task(notify_telegram())

# Now we have a list of signals and can make trade decisions using trading logic and trade
if "trade" in App.config["actions"]:
if "trade" in App.config.get("actions", {}):
trade_task = App.loop.create_task(main_trader_task())

return
Expand Down

0 comments on commit 09ad635

Please sign in to comment.