-
-
Notifications
You must be signed in to change notification settings - Fork 134
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #124 from hummingbot/feat/config_generator
(feat) position builder | config generator
- Loading branch information
Showing
3 changed files
with
267 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
from math import exp | ||
import streamlit as st | ||
from plotly.subplots import make_subplots | ||
import plotly.graph_objects as go | ||
from decimal import Decimal | ||
import yaml | ||
|
||
from utils.st_utils import initialize_st_page | ||
from hummingbot.smart_components.utils.distributions import Distributions | ||
|
||
# Initialize the Streamlit page | ||
initialize_st_page(title="Position Generator", icon="🔭", initial_sidebar_state="collapsed") | ||
|
||
# Page content | ||
st.text("This tool will help you analyze and generate a position config.") | ||
st.write("---") | ||
|
||
# Layout in columns | ||
col_quote, col_tp_sl, col_levels, col_spread_dist, col_amount_dist = st.columns([1, 1, 1, 2, 2]) | ||
|
||
|
||
def normalize(values): | ||
total = sum(values) | ||
return [val / total for val in values] | ||
|
||
|
||
def convert_to_yaml(spreads, order_amounts): | ||
data = { | ||
'dca_spreads': [float(spread)/100 for spread in spreads], | ||
'dca_amounts': [float(amount) for amount in order_amounts] | ||
} | ||
return yaml.dump(data, default_flow_style=False) | ||
|
||
|
||
with col_quote: | ||
total_amount_quote = st.number_input("Total amount of quote", value=1000) | ||
|
||
with col_tp_sl: | ||
tp = st.number_input("Take Profit (%)", min_value=0.0, max_value=100.0, value=2.0, step=0.1) | ||
sl = st.number_input("Stop Loss (%)", min_value=0.0, max_value=100.0, value=8.0, step=0.1) | ||
|
||
with col_levels: | ||
n_levels = st.number_input("Number of Levels", min_value=1, value=5) | ||
|
||
|
||
def distribution_inputs(column, dist_type_name): | ||
if dist_type_name == "Spread": | ||
dist_type = column.selectbox( | ||
f"Type of {dist_type_name} Distribution", | ||
("GeoCustom", "Geometric", "Fibonacci", "Manual", "Logarithmic", "Arithmetic"), | ||
key=f"{dist_type_name.lower()}_dist_type", | ||
# Set the default value | ||
) | ||
else: | ||
dist_type = column.selectbox( | ||
f"Type of {dist_type_name} Distribution", | ||
("Geometric", "Fibonacci", "Manual", "Logarithmic", "Arithmetic"), | ||
key=f"{dist_type_name.lower()}_dist_type", | ||
# Set the default value | ||
) | ||
base, scaling_factor, step, ratio, manual_values = None, None, None, None, None | ||
|
||
if dist_type != "Manual": | ||
start = column.number_input(f"{dist_type_name} Start Value", value=1.0, key=f"{dist_type_name.lower()}_start") | ||
if dist_type == "Logarithmic": | ||
base = column.number_input(f"{dist_type_name} Log Base", value=exp(1), key=f"{dist_type_name.lower()}_base") | ||
scaling_factor = column.number_input(f"{dist_type_name} Scaling Factor", value=2.0, key=f"{dist_type_name.lower()}_scaling") | ||
elif dist_type == "Arithmetic": | ||
step = column.number_input(f"{dist_type_name} Step", value=0.1, key=f"{dist_type_name.lower()}_step") | ||
elif dist_type == "Geometric": | ||
ratio = column.number_input(f"{dist_type_name} Ratio", value=2.0, key=f"{dist_type_name.lower()}_ratio") | ||
elif dist_type == "GeoCustom": | ||
ratio = column.number_input(f"{dist_type_name} Ratio", value=2.0, key=f"{dist_type_name.lower()}_ratio") | ||
else: | ||
manual_values = [column.number_input(f"{dist_type_name} for level {i+1}", value=1.0, key=f"{dist_type_name.lower()}_{i}") for i in range(n_levels)] | ||
start = None # As start is not relevant for Manual type | ||
|
||
return dist_type, start, base, scaling_factor, step, ratio, manual_values | ||
|
||
|
||
# Spread and Amount Distributions | ||
spread_dist_type, spread_start, spread_base, spread_scaling, spread_step, spread_ratio, manual_spreads = distribution_inputs(col_spread_dist, "Spread") | ||
amount_dist_type, amount_start, amount_base, amount_scaling, amount_step, amount_ratio, manual_amounts = distribution_inputs(col_amount_dist, "Amount") | ||
|
||
|
||
def get_distribution(dist_type, n_levels, start, base=None, scaling_factor=None, step=None, ratio=None, manual_values=None): | ||
if dist_type == "Manual": | ||
return manual_values | ||
elif dist_type == "Linear": | ||
return Distributions.linear(n_levels, start, start + tp) | ||
elif dist_type == "Fibonacci": | ||
return Distributions.fibonacci(n_levels, start) | ||
elif dist_type == "Logarithmic": | ||
return Distributions.logarithmic(n_levels, base, scaling_factor, start) | ||
elif dist_type == "Arithmetic": | ||
return Distributions.arithmetic(n_levels, start, step) | ||
elif dist_type == "Geometric": | ||
return Distributions.geometric(n_levels, start, ratio) | ||
elif dist_type == "GeoCustom": | ||
return [Decimal("0")] + Distributions.geometric(n_levels - 1, start, ratio) | ||
|
||
spread_distribution = get_distribution(spread_dist_type, n_levels, spread_start, spread_base, spread_scaling, spread_step, spread_ratio, manual_spreads) | ||
amount_distribution = normalize(get_distribution(amount_dist_type, n_levels, amount_start, amount_base, amount_scaling, amount_step, amount_ratio, manual_amounts)) | ||
order_amounts = [Decimal(amount_dist * total_amount_quote) for amount_dist in amount_distribution] | ||
spreads = [Decimal(spread - spread_distribution[0]) for spread in spread_distribution] | ||
|
||
|
||
# Export Button | ||
if st.button('Export as YAML'): | ||
yaml_data = convert_to_yaml(spreads, order_amounts) | ||
st.download_button( | ||
label="Download YAML", | ||
data=yaml_data, | ||
file_name='config.yaml', | ||
mime='text/yaml' | ||
) | ||
|
||
break_even_values = [] | ||
take_profit_values = [] | ||
for level in range(n_levels): | ||
spreads_normalized = [Decimal(spread) + Decimal(0.01) for spread in spreads[:level+1]] | ||
amounts = order_amounts[:level+1] | ||
break_even = (sum([spread * amount for spread, amount in zip(spreads_normalized, amounts)]) / sum(amounts)) - Decimal(0.01) | ||
break_even_values.append(break_even) | ||
take_profit_values.append(break_even - Decimal(tp)) | ||
|
||
accumulated_amount = [sum(order_amounts[:i+1]) for i in range(len(order_amounts))] | ||
|
||
|
||
def calculate_unrealized_pnl(spreads, break_even_values, accumulated_amount): | ||
unrealized_pnl = [] | ||
for i in range(len(spreads)): | ||
distance = abs(spreads[i] - break_even_values[i]) | ||
pnl = accumulated_amount[i] * distance / 100 # PNL calculation | ||
unrealized_pnl.append(pnl) | ||
return unrealized_pnl | ||
|
||
# Calculate unrealized PNL | ||
cum_unrealized_pnl = calculate_unrealized_pnl(spreads, break_even_values, accumulated_amount) | ||
|
||
|
||
tech_colors = { | ||
'spread': '#00BFFF', # Deep Sky Blue | ||
'break_even': '#FFD700', # Gold | ||
'take_profit': '#32CD32', # Green | ||
'order_amount': '#1E90FF', # Dodger Blue | ||
'cum_amount': '#4682B4', # Steel Blue | ||
'stop_loss': '#FF0000', # Red | ||
} | ||
|
||
# Create Plotly figure with secondary y-axis and a dark theme | ||
fig = make_subplots(specs=[[{"secondary_y": True}]]) | ||
fig.update_layout(template="plotly_dark") | ||
|
||
# Update the Scatter Plots and Horizontal Lines | ||
fig.add_trace(go.Scatter(x=list(range(len(spreads))), y=spreads, name='Spread (%)', mode='lines+markers', line=dict(width=3, color=tech_colors['spread'])), secondary_y=False) | ||
fig.add_trace(go.Scatter(x=list(range(len(break_even_values))), y=break_even_values, name='Break Even (%)', mode='lines+markers', line=dict(width=3, color=tech_colors['break_even'])), secondary_y=False) | ||
fig.add_trace(go.Scatter(x=list(range(len(take_profit_values))), y=take_profit_values, name='Take Profit (%)', mode='lines+markers', line=dict(width=3, color=tech_colors['take_profit'])), secondary_y=False) | ||
|
||
# Add the new Bar Plot for Cumulative Unrealized PNL | ||
fig.add_trace(go.Bar( | ||
x=list(range(len(cum_unrealized_pnl))), | ||
y=cum_unrealized_pnl, | ||
text=[f"{pnl:.2f}" for pnl in cum_unrealized_pnl], | ||
textposition='auto', | ||
textfont=dict(color='white', size=12), | ||
name='Cum Unrealized PNL', | ||
marker=dict(color='#FFA07A', opacity=0.6) # Light Salmon color, adjust as needed | ||
), secondary_y=True) | ||
|
||
fig.add_trace(go.Bar( | ||
x=list(range(len(order_amounts))), | ||
y=order_amounts, | ||
text=[f"{amt:.2f}" for amt in order_amounts], # List comprehension to format text labels | ||
textposition='auto', | ||
textfont=dict( | ||
color='white', | ||
size=12 | ||
), | ||
name='Order Amount', | ||
marker=dict(color=tech_colors['order_amount'], opacity=0.5), | ||
), secondary_y=True) | ||
|
||
# Modify the Bar Plot for Accumulated Amount | ||
fig.add_trace(go.Bar( | ||
x=list(range(len(accumulated_amount))), | ||
y=accumulated_amount, | ||
text=[f"{amt:.2f}" for amt in accumulated_amount], # List comprehension to format text labels | ||
textposition='auto', | ||
textfont=dict( | ||
color='white', | ||
size=12 | ||
), | ||
name='Cum Amount', | ||
marker=dict(color=tech_colors['cum_amount'], opacity=0.5), | ||
), secondary_y=True) | ||
|
||
|
||
# Add Horizontal Lines for Last Breakeven Price and Stop Loss Level | ||
last_break_even = break_even_values[-1] | ||
stop_loss_value = last_break_even + Decimal(sl) | ||
# Horizontal Lines for Last Breakeven and Stop Loss | ||
fig.add_hline(y=last_break_even, line_dash="dash", annotation_text=f"Global Break Even: {last_break_even:.2f} (%)", annotation_position="top left", line_color=tech_colors['break_even']) | ||
fig.add_hline(y=stop_loss_value, line_dash="dash", annotation_text=f"Stop Loss: {stop_loss_value:.2f} (%)", annotation_position="bottom right", line_color=tech_colors['stop_loss']) | ||
|
||
# Update Annotations for Spread and Break Even | ||
for i, (spread, be_value, tp_value) in enumerate(zip(spreads, break_even_values, take_profit_values)): | ||
fig.add_annotation(x=i, y=spread, text=f"{spread:.2f}%", showarrow=True, arrowhead=1, yshift=10, xshift=-2, font=dict(color=tech_colors['spread'])) | ||
fig.add_annotation(x=i, y=be_value, text=f"{be_value:.2f}%", showarrow=True, arrowhead=1, yshift=5, xshift=-2, font=dict(color=tech_colors['break_even'])) | ||
fig.add_annotation(x=i, y=tp_value, text=f"{tp_value:.2f}%", showarrow=True, arrowhead=1, yshift=10, xshift=-2, font=dict(color=tech_colors['take_profit'])) | ||
# Update Layout with a Dark Theme | ||
fig.update_layout( | ||
title="Spread, Accumulated Amount, Break Even, and Take Profit by Order Level", | ||
xaxis_title="Order Level", | ||
yaxis_title="Spread (%)", | ||
yaxis2_title="Amount (Quote)", | ||
height=800, | ||
width=1800, | ||
plot_bgcolor='rgba(0, 0, 0, 0)', # Transparent background | ||
paper_bgcolor='rgba(0, 0, 0, 0.1)', # Lighter shade for the paper | ||
font=dict(color='white') # Font color | ||
) | ||
|
||
# Calculate metrics | ||
max_loss = total_amount_quote * Decimal(sl / 100) | ||
profit_per_level = [cum_amount * Decimal(tp / 100) for cum_amount in accumulated_amount] | ||
loots_to_recover = [max_loss / profit for profit in profit_per_level] | ||
|
||
# Define a consistent annotation size and maximum value for the secondary y-axis | ||
circle_text = "●" # Unicode character for a circle | ||
max_secondary_value = max(max(accumulated_amount), max(order_amounts), max(cum_unrealized_pnl)) # Adjust based on your secondary y-axis data | ||
|
||
# Determine an appropriate y-offset for annotations | ||
y_offset_secondary = max_secondary_value * Decimal(0.1) # Adjusts the height relative to the maximum value on the secondary y-axis | ||
|
||
# Add annotations to the Plotly figure for the secondary y-axis | ||
for i, loot in enumerate(loots_to_recover): | ||
fig.add_annotation( | ||
x=i, | ||
y=max_secondary_value + y_offset_secondary, # Position above the maximum value using the offset | ||
text=f"{circle_text}<br>LTR: {round(loot, 2)}", # Circle symbol and loot value in separate lines | ||
showarrow=False, | ||
font=dict(size=16, color='purple'), | ||
xanchor="center", # Centers the text above the x coordinate | ||
yanchor="bottom", # Anchors the text at its bottom to avoid overlapping | ||
align="center", | ||
yref="y2" # Reference the secondary y-axis | ||
) | ||
# Add Max Loss Metric as an Annotation | ||
max_loss_annotation_text = f"Max Loss (Quote): {max_loss:.2f}" | ||
fig.add_annotation( | ||
x=max(len(spreads), len(break_even_values)) - 1, # Positioning the annotation to the right | ||
text=max_loss_annotation_text, | ||
showarrow=False, | ||
font=dict(size=20, color='white'), | ||
bgcolor='red', # Red background for emphasis | ||
xanchor="left", | ||
yanchor="top", | ||
yref="y2" # Reference the secondary y-axis | ||
) | ||
|
||
st.write("---") | ||
|
||
# Display in Streamlit | ||
st.plotly_chart(fig) | ||
|