640 lines
No EOL
33 KiB
Python
640 lines
No EOL
33 KiB
Python
import websocket
|
|
import json
|
|
import pandas as pd
|
|
import numpy as np
|
|
from ta.trend import SMAIndicator
|
|
from ta.momentum import RSIIndicator
|
|
from ta.volatility import AverageTrueRange
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.animation import FuncAnimation
|
|
import matplotlib.dates as mdates
|
|
import logging
|
|
import time
|
|
from datetime import timedelta, datetime
|
|
|
|
# Setup logging
|
|
logging.basicConfig(filename='trade_log.txt', level=logging.INFO, format='%(asctime)s - %(message)s')
|
|
|
|
# Strategy parameters
|
|
initial_capital = 100000
|
|
base_size = 0.01 # 1% of portfolio
|
|
risk_per_trade = 0.01 # 1% risk
|
|
simulation_duration = 600 # 10 minutes in seconds
|
|
buffer_threshold = 0.1 # 10% buffer for position changes
|
|
ticker = "BTC/USDT"
|
|
|
|
# TRADING COSTS - Realistic exchange fees
|
|
maker_fee = 0.001 # 0.1% maker fee (limit orders)
|
|
taker_fee = 0.001 # 0.1% taker fee (market orders)
|
|
spread_cost = 0.0005 # 0.05% bid-ask spread cost
|
|
slippage_cost = 0.0002 # 0.02% slippage on market orders
|
|
|
|
# Data storage
|
|
data = []
|
|
position_dollars = 0 # Position size in dollars (positive = long, negative = short)
|
|
btc_units = 0 # Actual BTC units (positive = owned, negative = borrowed/short)
|
|
entry_price = 0
|
|
stop_loss = 0
|
|
trailing_stop = 0
|
|
highest_price = 0 # For long positions
|
|
lowest_price = 0 # For short positions
|
|
portfolio_value = initial_capital
|
|
cumulative_costs = 0 # Track all trading costs
|
|
trade_log = []
|
|
cost_log = [] # Track costs over time
|
|
entries = []
|
|
exits = []
|
|
df_global = pd.DataFrame()
|
|
start_time = None # Will be set when first data arrives
|
|
current_price_base = None # For price scaling
|
|
|
|
# Plot setup - WHITE BACKGROUND with 3 panels
|
|
plt.style.use('default') # Use default (white) background
|
|
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 12), height_ratios=[2, 1, 1])
|
|
fig.suptitle(f'Real-Time {ticker} Long/Short Strategy with Trading Costs', fontsize=16, fontweight='bold')
|
|
|
|
# Create right axis for real price
|
|
ax1_right = ax1.twinx()
|
|
|
|
# Main plot (price and SMAs)
|
|
price_line, = ax1.plot([], [], label='Price', color='blue', linewidth=2)
|
|
sma5_line, = ax1.plot([], [], label='SMA5', color='orange', linewidth=1.5)
|
|
sma15_line, = ax1.plot([], [], label='SMA15', color='green', linewidth=1.5)
|
|
stop_loss_line, = ax1.plot([], [], label='Stop Loss', color='red', linestyle='--', alpha=0.8)
|
|
trailing_stop_line, = ax1.plot([], [], label='Trailing Stop', color='purple', linestyle='--', alpha=0.8)
|
|
|
|
# Buy/Sell markers - larger and more visible
|
|
buy_scatter = ax1.scatter([], [], color='lime', marker='^', label='BUY (Long)', s=200, edgecolor='darkgreen', linewidth=2, zorder=10)
|
|
short_scatter = ax1.scatter([], [], color='orange', marker='v', label='SHORT', s=200, edgecolor='darkorange', linewidth=2, zorder=10)
|
|
close_long_scatter = ax1.scatter([], [], color='red', marker='x', label='Close Long', s=200, edgecolor='darkred', linewidth=2, zorder=10)
|
|
close_short_scatter = ax1.scatter([], [], color='cyan', marker='+', label='Close Short', s=200, edgecolor='darkcyan', linewidth=2, zorder=10)
|
|
|
|
ax1.set_ylabel('Price Change (%)', fontsize=12, fontweight='bold')
|
|
ax1_right.set_ylabel('Real Price (USDT)', fontsize=12, fontweight='bold', color='gray')
|
|
ax1.legend(loc='upper left', fontsize=10)
|
|
ax1.grid(True, alpha=0.3)
|
|
ax1.set_title('Price Chart with Long/Short Indicators', fontsize=12, fontweight='bold')
|
|
|
|
# Portfolio subplot
|
|
portfolio_line, = ax2.plot([], [], label='Portfolio Value (After Costs)', color='darkblue', linewidth=2)
|
|
portfolio_gross_line, = ax2.plot([], [], label='Portfolio Value (Before Costs)', color='lightblue', linewidth=1, linestyle='--', alpha=0.7)
|
|
ax2.axhline(y=initial_capital, color='gray', linestyle='-', alpha=0.5, label='Initial Capital')
|
|
ax2.axhline(y=initial_capital * 1.01, color='lightgreen', linestyle='--', alpha=0.7, label='+1%')
|
|
ax2.axhline(y=initial_capital * 0.99, color='lightcoral', linestyle='--', alpha=0.7, label='-1%')
|
|
|
|
ax2.set_ylabel('Portfolio Value ($)', fontsize=12, fontweight='bold')
|
|
ax2.legend(loc='upper left', fontsize=10)
|
|
ax2.grid(True, alpha=0.3)
|
|
ax2.set_title('Portfolio Performance (Net vs Gross)', fontsize=12, fontweight='bold')
|
|
|
|
# Costs subplot (NEW - 3rd panel)
|
|
costs_line, = ax3.plot([], [], label='Cumulative Costs', color='red', linewidth=2)
|
|
fees_line, = ax3.plot([], [], label='Trading Fees', color='orange', linewidth=1.5, alpha=0.8)
|
|
spread_line, = ax3.plot([], [], label='Spread Costs', color='purple', linewidth=1.5, alpha=0.8)
|
|
slippage_line, = ax3.plot([], [], label='Slippage Costs', color='brown', linewidth=1.5, alpha=0.8)
|
|
|
|
ax3.set_ylabel('Costs ($)', fontsize=12, fontweight='bold')
|
|
ax3.set_xlabel('Time (MM:SS)', fontsize=12, fontweight='bold')
|
|
ax3.legend(loc='upper left', fontsize=10)
|
|
ax3.grid(True, alpha=0.3)
|
|
ax3.set_title('Trading Costs Breakdown', fontsize=12, fontweight='bold')
|
|
|
|
def calculate_indicators(df):
|
|
"""Calculate technical indicators with proper error handling"""
|
|
try:
|
|
if len(df) >= 5:
|
|
df['sma5'] = SMAIndicator(df['close'], window=5).sma_indicator()
|
|
else:
|
|
df['sma5'] = df['close']
|
|
|
|
if len(df) >= 15:
|
|
df['sma15'] = SMAIndicator(df['close'], window=15).sma_indicator()
|
|
else:
|
|
df['sma15'] = df['close']
|
|
|
|
if len(df) >= 7:
|
|
df['rsi'] = RSIIndicator(df['close'], window=7).rsi()
|
|
df['atr'] = AverageTrueRange(df['high'], df['low'], df['close'], window=7).average_true_range()
|
|
else:
|
|
df['rsi'] = pd.Series([50] * len(df), index=df.index)
|
|
df['atr'] = pd.Series([1.0] * len(df), index=df.index)
|
|
|
|
if len(df) >= 10:
|
|
df['roc'] = df['close'].pct_change(10) * 100
|
|
else:
|
|
df['roc'] = pd.Series([0.0] * len(df), index=df.index)
|
|
|
|
except Exception as e:
|
|
print(f"Error calculating indicators: {e}")
|
|
df['sma5'] = df['close']
|
|
df['sma15'] = df['close']
|
|
df['rsi'] = pd.Series([50] * len(df), index=df.index)
|
|
df['atr'] = pd.Series([1.0] * len(df), index=df.index)
|
|
df['roc'] = pd.Series([0.0] * len(df), index=df.index)
|
|
|
|
return df
|
|
|
|
def calculate_trading_costs(trade_value, trade_type="market"):
|
|
"""Calculate all trading costs for a given trade"""
|
|
# Base trading fee (maker or taker)
|
|
fee_rate = maker_fee if trade_type == "limit" else taker_fee
|
|
trading_fee = trade_value * fee_rate
|
|
|
|
# Spread cost (always applies)
|
|
spread_cost_amount = trade_value * spread_cost
|
|
|
|
# Slippage (only for market orders)
|
|
slippage_cost_amount = trade_value * slippage_cost if trade_type == "market" else 0
|
|
|
|
total_cost = trading_fee + spread_cost_amount + slippage_cost_amount
|
|
|
|
return {
|
|
'total_cost': total_cost,
|
|
'trading_fee': trading_fee,
|
|
'spread_cost': spread_cost_amount,
|
|
'slippage_cost': slippage_cost_amount
|
|
}
|
|
|
|
def get_time_in_seconds(timestamp):
|
|
"""Convert timestamp to seconds from start"""
|
|
if start_time is None:
|
|
return 0
|
|
return (timestamp - start_time).total_seconds()
|
|
|
|
def format_time_axis(seconds):
|
|
"""Format seconds as MM:SS"""
|
|
minutes = int(seconds // 60)
|
|
secs = int(seconds % 60)
|
|
return f"{minutes:02d}:{secs:02d}"
|
|
|
|
def get_position_type():
|
|
"""Return current position type"""
|
|
if position_dollars > 0:
|
|
return "LONG"
|
|
elif position_dollars < 0:
|
|
return "SHORT"
|
|
else:
|
|
return "FLAT"
|
|
|
|
# WebSocket callback functions
|
|
def on_message(ws, message):
|
|
global data, position_dollars, btc_units, entry_price, stop_loss, trailing_stop
|
|
global highest_price, lowest_price, portfolio_value, cumulative_costs, df_global, start_time, current_price_base
|
|
|
|
try:
|
|
msg = json.loads(message)
|
|
if 'k' in msg:
|
|
kline = msg['k']
|
|
if kline['x']: # Closed candle
|
|
timestamp = pd.to_datetime(kline['t'], unit='ms')
|
|
close = float(kline['c'])
|
|
high = float(kline['h'])
|
|
low = float(kline['l'])
|
|
|
|
# Set start time on first data point
|
|
if start_time is None:
|
|
start_time = timestamp
|
|
current_price_base = close
|
|
print(f"🕐 Long/Short Strategy with Costs started at {start_time}")
|
|
print(f"💰 Base price: ${current_price_base:.2f}")
|
|
print(f"💸 Trading Costs: Maker {maker_fee*100:.1f}%, Taker {taker_fee*100:.1f}%, Spread {spread_cost*100:.2f}%, Slippage {slippage_cost*100:.2f}%")
|
|
|
|
# Calculate time in seconds from start
|
|
time_seconds = get_time_in_seconds(timestamp)
|
|
|
|
data.append({
|
|
'timestamp': timestamp,
|
|
'time_seconds': time_seconds,
|
|
'close': close,
|
|
'high': high,
|
|
'low': low
|
|
})
|
|
|
|
# Keep only last 100 data points for efficiency
|
|
data[:] = data[-100:] if len(data) > 100 else data
|
|
|
|
if len(data) > 5: # Minimum data needed
|
|
df = pd.DataFrame(data)
|
|
df = calculate_indicators(df)
|
|
df_global = df.copy() # Store for plotting
|
|
|
|
current = df.iloc[-1]
|
|
prev = df.iloc[-2] if len(df) > 1 else None
|
|
|
|
# Risk management factors
|
|
if len(df) >= 5:
|
|
atr_avg = df['atr'].rolling(5).mean().iloc[-1]
|
|
vol_factor = 0.8 if current['atr'] > 2 * atr_avg else 1.0
|
|
else:
|
|
vol_factor = 1.0
|
|
|
|
signal_factor = 1.2 if abs(current['rsi'] - 50) > 20 else 1.0 # Strong RSI signal
|
|
market_factor = 1.2 if abs(current['roc']) > 3 else (0.8 if abs(current['roc']) < 1 else 1.0)
|
|
|
|
# ENTRY LOGIC - Only when no position
|
|
if position_dollars == 0 and prev is not None and len(df) >= 15:
|
|
|
|
# LONG ENTRY: SMA5 crosses above SMA15 AND RSI > 50
|
|
if (current['sma5'] > current['sma15'] and prev['sma5'] <= prev['sma15'] and
|
|
current['rsi'] > 50):
|
|
# Calculate position size in dollars (positive = long)
|
|
gross_position = (base_size * signal_factor * vol_factor * market_factor) * initial_capital
|
|
|
|
# Calculate trading costs
|
|
costs = calculate_trading_costs(gross_position, "market") # Market order for entries
|
|
cumulative_costs += costs['total_cost']
|
|
|
|
# Net position after costs
|
|
position_dollars = gross_position
|
|
entry_price = current['close']
|
|
# Calculate actual BTC units (positive = owned)
|
|
btc_units = position_dollars / entry_price
|
|
stop_loss = entry_price - 1.5 * current['atr']
|
|
highest_price = entry_price
|
|
trailing_stop = stop_loss
|
|
entries.append((time_seconds, entry_price, 'BUY'))
|
|
|
|
# Update portfolio with costs
|
|
portfolio_value -= costs['total_cost']
|
|
|
|
print(f"🚀 LONG ENTRY: Time={format_time_axis(time_seconds)}, Price=${entry_price:.2f}")
|
|
print(f" Investment: ${position_dollars:.2f}, BTC Units: {btc_units:.6f}, Stop: ${stop_loss:.2f}")
|
|
print(f" 💸 Entry Costs: ${costs['total_cost']:.2f} (Fee: ${costs['trading_fee']:.2f}, Spread: ${costs['spread_cost']:.2f}, Slippage: ${costs['slippage_cost']:.2f})")
|
|
logging.info(f"LONG: Time={format_time_axis(time_seconds)}, Price={entry_price}, Investment=${position_dollars:.2f}, BTC={btc_units:.6f}, Costs=${costs['total_cost']:.2f}")
|
|
|
|
# Log costs
|
|
cost_log.append({
|
|
'timestamp': timestamp,
|
|
'time_seconds': time_seconds,
|
|
'cumulative_costs': cumulative_costs,
|
|
'trading_fees': costs['trading_fee'],
|
|
'spread_costs': costs['spread_cost'],
|
|
'slippage_costs': costs['slippage_cost']
|
|
})
|
|
|
|
# SHORT ENTRY: SMA5 crosses below SMA15 AND RSI < 50
|
|
elif (current['sma5'] < current['sma15'] and prev['sma5'] >= prev['sma15'] and
|
|
current['rsi'] < 50):
|
|
# Calculate position size in dollars (negative = short)
|
|
gross_position = (base_size * signal_factor * vol_factor * market_factor) * initial_capital
|
|
|
|
# Calculate trading costs
|
|
costs = calculate_trading_costs(gross_position, "market") # Market order for entries
|
|
cumulative_costs += costs['total_cost']
|
|
|
|
# Net position after costs
|
|
position_dollars = -gross_position # Negative for short
|
|
entry_price = current['close']
|
|
# Calculate actual BTC units (negative = borrowed/short)
|
|
btc_units = position_dollars / entry_price # Will be negative
|
|
stop_loss = entry_price + 1.5 * current['atr'] # Stop above entry for shorts
|
|
lowest_price = entry_price
|
|
trailing_stop = stop_loss
|
|
entries.append((time_seconds, entry_price, 'SHORT'))
|
|
|
|
# Update portfolio with costs
|
|
portfolio_value -= costs['total_cost']
|
|
|
|
print(f"🔻 SHORT ENTRY: Time={format_time_axis(time_seconds)}, Price=${entry_price:.2f}")
|
|
print(f" Short Size: ${abs(position_dollars):.2f}, BTC Units: {btc_units:.6f}, Stop: ${stop_loss:.2f}")
|
|
print(f" 💸 Entry Costs: ${costs['total_cost']:.2f} (Fee: ${costs['trading_fee']:.2f}, Spread: ${costs['spread_cost']:.2f}, Slippage: ${costs['slippage_cost']:.2f})")
|
|
logging.info(f"SHORT: Time={format_time_axis(time_seconds)}, Price={entry_price}, Size=${abs(position_dollars):.2f}, BTC={btc_units:.6f}, Costs=${costs['total_cost']:.2f}")
|
|
|
|
# Log costs
|
|
cost_log.append({
|
|
'timestamp': timestamp,
|
|
'time_seconds': time_seconds,
|
|
'cumulative_costs': cumulative_costs,
|
|
'trading_fees': costs['trading_fee'],
|
|
'spread_costs': costs['spread_cost'],
|
|
'slippage_costs': costs['slippage_cost']
|
|
})
|
|
|
|
# EXIT LOGIC
|
|
if position_dollars != 0 and len(df) >= 15:
|
|
exit_reason = ""
|
|
should_exit = False
|
|
|
|
if position_dollars > 0: # LONG EXIT
|
|
if current['sma5'] < current['sma15']:
|
|
exit_reason = "SMA Crossover (Long → Exit)"
|
|
should_exit = True
|
|
elif current['close'] < stop_loss:
|
|
exit_reason = "Stop Loss (Long)"
|
|
should_exit = True
|
|
|
|
else: # SHORT EXIT
|
|
if current['sma5'] > current['sma15']:
|
|
exit_reason = "SMA Crossover (Short → Exit)"
|
|
should_exit = True
|
|
elif current['close'] > stop_loss:
|
|
exit_reason = "Stop Loss (Short)"
|
|
should_exit = True
|
|
|
|
if should_exit:
|
|
# Calculate exit costs
|
|
trade_value = abs(position_dollars)
|
|
costs = calculate_trading_costs(trade_value, "market") # Market order for exits
|
|
cumulative_costs += costs['total_cost']
|
|
|
|
# Profit calculation (before costs):
|
|
if position_dollars > 0: # Closing long
|
|
gross_profit = (current['close'] - entry_price) * btc_units
|
|
exits.append((time_seconds, current['close'], 'CLOSE_LONG'))
|
|
print(f"🚪 CLOSE LONG: Time={format_time_axis(time_seconds)}, {exit_reason}, Price=${current['close']:.2f}")
|
|
else: # Closing short
|
|
gross_profit = (entry_price - current['close']) * abs(btc_units) # Profit when price drops
|
|
exits.append((time_seconds, current['close'], 'CLOSE_SHORT'))
|
|
print(f"🚪 CLOSE SHORT: Time={format_time_axis(time_seconds)}, {exit_reason}, Price=${current['close']:.2f}")
|
|
|
|
# Net profit after exit costs
|
|
net_profit = gross_profit - costs['total_cost']
|
|
portfolio_value += gross_profit - costs['total_cost'] # Add net profit
|
|
|
|
print(f" 💰 Gross P&L: ${gross_profit:+.2f}")
|
|
print(f" 💸 Exit Costs: ${costs['total_cost']:.2f} (Fee: ${costs['trading_fee']:.2f}, Spread: ${costs['spread_cost']:.2f}, Slippage: ${costs['slippage_cost']:.2f})")
|
|
print(f" 📊 Net P&L: ${net_profit:+.2f}, Portfolio: ${portfolio_value:,.2f}")
|
|
|
|
logging.info(f"EXIT: Time={format_time_axis(time_seconds)}, Price={current['close']}, Reason={exit_reason}, GrossProfit=${gross_profit:.2f}, NetProfit=${net_profit:.2f}, Costs=${costs['total_cost']:.2f}")
|
|
|
|
# Log costs
|
|
cost_log.append({
|
|
'timestamp': timestamp,
|
|
'time_seconds': time_seconds,
|
|
'cumulative_costs': cumulative_costs,
|
|
'trading_fees': costs['trading_fee'],
|
|
'spread_costs': costs['spread_cost'],
|
|
'slippage_costs': costs['slippage_cost']
|
|
})
|
|
|
|
# Reset position variables
|
|
position_dollars = 0
|
|
btc_units = 0
|
|
entry_price = 0
|
|
stop_loss = 0
|
|
trailing_stop = 0
|
|
highest_price = 0
|
|
lowest_price = 0
|
|
|
|
# Update portfolio tracking
|
|
if position_dollars != 0:
|
|
if position_dollars > 0: # Long position
|
|
current_btc_value = btc_units * current['close']
|
|
unrealized_pnl = current_btc_value - position_dollars
|
|
else: # Short position
|
|
unrealized_pnl = (entry_price - current['close']) * abs(btc_units)
|
|
|
|
current_portfolio_value = portfolio_value + unrealized_pnl
|
|
trade_log.append({'timestamp': timestamp, 'time_seconds': time_seconds, 'portfolio_value': current_portfolio_value})
|
|
else:
|
|
trade_log.append({'timestamp': timestamp, 'time_seconds': time_seconds, 'portfolio_value': portfolio_value})
|
|
|
|
# Always log current costs (even if no new trades)
|
|
if not cost_log or cost_log[-1]['time_seconds'] != time_seconds:
|
|
cost_log.append({
|
|
'timestamp': timestamp,
|
|
'time_seconds': time_seconds,
|
|
'cumulative_costs': cumulative_costs,
|
|
'trading_fees': 0, # No new fees
|
|
'spread_costs': 0,
|
|
'slippage_costs': 0
|
|
})
|
|
|
|
# Print status every 10 seconds
|
|
if len(data) % 10 == 0:
|
|
pos_type = get_position_type()
|
|
status_emoji = {"LONG": "📈", "SHORT": "📉", "FLAT": "⏸️"}[pos_type]
|
|
price_change = ((current['close'] - current_price_base) / current_price_base) * 100
|
|
portfolio_change = ((portfolio_value - initial_capital) / initial_capital) * 100
|
|
cost_percentage = (cumulative_costs / initial_capital) * 100
|
|
|
|
if position_dollars != 0:
|
|
if position_dollars > 0: # Long
|
|
current_btc_value = btc_units * current['close']
|
|
unrealized_pnl = current_btc_value - position_dollars
|
|
print(f"{status_emoji} {pos_type} | Time: {format_time_axis(time_seconds)} | Price: ${current['close']:.2f} ({price_change:+.2f}%)")
|
|
print(f" Investment: ${position_dollars:.2f} | Current Value: ${current_btc_value:.2f} | Unrealized P&L: ${unrealized_pnl:+.2f}")
|
|
print(f" 💸 Total Costs: ${cumulative_costs:.2f} ({cost_percentage:.3f}% of capital)")
|
|
else: # Short
|
|
unrealized_pnl = (entry_price - current['close']) * abs(btc_units)
|
|
print(f"{status_emoji} {pos_type} | Time: {format_time_axis(time_seconds)} | Price: ${current['close']:.2f} ({price_change:+.2f}%)")
|
|
print(f" Short Size: ${abs(position_dollars):.2f} | Entry: ${entry_price:.2f} | Unrealized P&L: ${unrealized_pnl:+.2f}")
|
|
print(f" 💸 Total Costs: ${cumulative_costs:.2f} ({cost_percentage:.3f}% of capital)")
|
|
else:
|
|
print(f"{status_emoji} {pos_type} | Time: {format_time_axis(time_seconds)} | Price: ${current['close']:.2f} ({price_change:+.2f}%) | Portfolio: ${portfolio_value:,.2f} ({portfolio_change:+.2f}%)")
|
|
print(f" 💸 Total Costs: ${cumulative_costs:.2f} ({cost_percentage:.3f}% of capital)")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Error in on_message: {e}")
|
|
|
|
def on_error(ws, error):
|
|
print(f"❌ WebSocket Error: {error}")
|
|
|
|
def on_close(ws, close_status_code, close_msg):
|
|
print("🔌 WebSocket closed")
|
|
if trade_log:
|
|
pd.DataFrame(trade_log).to_csv('trade_log.csv', index=False)
|
|
print("💾 Trade log saved")
|
|
if cost_log:
|
|
pd.DataFrame(cost_log).to_csv('cost_log.csv', index=False)
|
|
print("💾 Cost log saved")
|
|
|
|
def on_open(ws):
|
|
print("🔗 WebSocket opened - Starting long/short trend following strategy with costs")
|
|
|
|
# Animation update function
|
|
def update_plot(frame):
|
|
global df_global, current_price_base
|
|
|
|
try:
|
|
if df_global.empty or len(df_global) == 0 or current_price_base is None:
|
|
return (price_line, sma5_line, sma15_line, stop_loss_line, trailing_stop_line,
|
|
portfolio_line, portfolio_gross_line, costs_line, fees_line, spread_line, slippage_line,
|
|
buy_scatter, short_scatter, close_long_scatter, close_short_scatter)
|
|
|
|
df = df_global.copy()
|
|
|
|
# Calculate price change percentage for left axis
|
|
df['price_change_pct'] = ((df['close'] - current_price_base) / current_price_base) * 100
|
|
df['sma5_change_pct'] = ((df['sma5'] - current_price_base) / current_price_base) * 100
|
|
df['sma15_change_pct'] = ((df['sma15'] - current_price_base) / current_price_base) * 100
|
|
|
|
# Update main plot - price change percentage
|
|
price_line.set_data(df['time_seconds'], df['price_change_pct'])
|
|
|
|
if 'sma5' in df.columns and not df['sma5'].isna().all():
|
|
sma5_line.set_data(df['time_seconds'], df['sma5_change_pct'])
|
|
else:
|
|
sma5_line.set_data([], [])
|
|
|
|
if 'sma15' in df.columns and not df['sma15'].isna().all():
|
|
sma15_line.set_data(df['time_seconds'], df['sma15_change_pct'])
|
|
else:
|
|
sma15_line.set_data([], [])
|
|
|
|
# Update markers - separate long and short entries/exits
|
|
long_entries = [(t, p) for t, p, action in entries if action == 'BUY']
|
|
short_entries = [(t, p) for t, p, action in entries if action == 'SHORT']
|
|
long_exits = [(t, p) for t, p, action in exits if action == 'CLOSE_LONG']
|
|
short_exits = [(t, p) for t, p, action in exits if action == 'CLOSE_SHORT']
|
|
|
|
if long_entries:
|
|
buy_times, buy_prices = zip(*long_entries)
|
|
buy_prices_pct = [((price - current_price_base) / current_price_base) * 100 for price in buy_prices]
|
|
buy_scatter.set_offsets(np.c_[buy_times, buy_prices_pct])
|
|
else:
|
|
buy_scatter.set_offsets(np.array([]).reshape(0, 2))
|
|
|
|
if short_entries:
|
|
short_times, short_prices = zip(*short_entries)
|
|
short_prices_pct = [((price - current_price_base) / current_price_base) * 100 for price in short_prices]
|
|
short_scatter.set_offsets(np.c_[short_times, short_prices_pct])
|
|
else:
|
|
short_scatter.set_offsets(np.array([]).reshape(0, 2))
|
|
|
|
if long_exits:
|
|
close_long_times, close_long_prices = zip(*long_exits)
|
|
close_long_prices_pct = [((price - current_price_base) / current_price_base) * 100 for price in close_long_prices]
|
|
close_long_scatter.set_offsets(np.c_[close_long_times, close_long_prices_pct])
|
|
else:
|
|
close_long_scatter.set_offsets(np.array([]).reshape(0, 2))
|
|
|
|
if short_exits:
|
|
close_short_times, close_short_prices = zip(*short_exits)
|
|
close_short_prices_pct = [((price - current_price_base) / current_price_base) * 100 for price in close_short_prices]
|
|
close_short_scatter.set_offsets(np.c_[close_short_times, close_short_prices_pct])
|
|
else:
|
|
close_short_scatter.set_offsets(np.array([]).reshape(0, 2))
|
|
|
|
# Handle trade_log for portfolio (net and gross)
|
|
if trade_log:
|
|
trade_df = pd.DataFrame(trade_log)
|
|
portfolio_line.set_data(trade_df['time_seconds'], trade_df['portfolio_value'])
|
|
|
|
# Calculate gross portfolio (before costs)
|
|
gross_portfolio = trade_df['portfolio_value'] + cumulative_costs
|
|
portfolio_gross_line.set_data(trade_df['time_seconds'], gross_portfolio)
|
|
|
|
# Handle cost_log for costs breakdown
|
|
if cost_log:
|
|
cost_df = pd.DataFrame(cost_log)
|
|
costs_line.set_data(cost_df['time_seconds'], cost_df['cumulative_costs'])
|
|
|
|
# Calculate cumulative costs by type
|
|
cost_df['cum_fees'] = cost_df['trading_fees'].cumsum()
|
|
cost_df['cum_spread'] = cost_df['spread_costs'].cumsum()
|
|
cost_df['cum_slippage'] = cost_df['slippage_costs'].cumsum()
|
|
|
|
fees_line.set_data(cost_df['time_seconds'], cost_df['cum_fees'])
|
|
spread_line.set_data(cost_df['time_seconds'], cost_df['cum_spread'])
|
|
slippage_line.set_data(cost_df['time_seconds'], cost_df['cum_slippage'])
|
|
|
|
# Fixed time scale: 0 to 600 seconds (10 minutes)
|
|
ax1.set_xlim(0, 600)
|
|
ax2.set_xlim(0, 600)
|
|
ax3.set_xlim(0, 600)
|
|
|
|
# Price scale: ±2%
|
|
ax1.set_ylim(-2, 2)
|
|
|
|
# Right axis for real prices
|
|
current_price = df['close'].iloc[-1]
|
|
price_range = current_price * 0.02 # 2%
|
|
ax1_right.set_ylim(current_price - price_range, current_price + price_range)
|
|
|
|
# Portfolio scale: initial capital ±1%
|
|
capital_range = initial_capital * 0.01 # 1%
|
|
ax2.set_ylim(initial_capital - capital_range, initial_capital + capital_range)
|
|
|
|
# Costs scale: 0 to max costs with some headroom
|
|
if cumulative_costs > 0:
|
|
ax3.set_ylim(0, cumulative_costs * 1.2)
|
|
else:
|
|
ax3.set_ylim(0, 100) # Default range
|
|
|
|
# Set tick marks for time (every minute = 60 seconds)
|
|
time_ticks = range(0, 601, 60) # 0, 60, 120, ..., 600
|
|
time_labels = [format_time_axis(t) for t in time_ticks]
|
|
for ax in [ax1, ax2, ax3]:
|
|
ax.set_xticks(time_ticks)
|
|
ax.set_xticklabels(time_labels)
|
|
|
|
# Add grid lines for better readability
|
|
ax1.grid(True, alpha=0.3)
|
|
ax2.grid(True, alpha=0.3)
|
|
ax3.grid(True, alpha=0.3)
|
|
|
|
except Exception as e:
|
|
print(f"Plot update error: {e}")
|
|
|
|
return (price_line, sma5_line, sma15_line, stop_loss_line, trailing_stop_line,
|
|
portfolio_line, portfolio_gross_line, costs_line, fees_line, spread_line, slippage_line,
|
|
buy_scatter, short_scatter, close_long_scatter, close_short_scatter)
|
|
|
|
# Run WebSocket and animation
|
|
if __name__ == "__main__":
|
|
print("🚀 Starting Enhanced Long/Short Strategy with Trading Costs")
|
|
print("📈 LONG: SMA5 crosses above SMA15 + RSI > 50")
|
|
print("📉 SHORT: SMA5 crosses below SMA15 + RSI < 50")
|
|
print("💸 Trading Costs Included:")
|
|
print(f" • Maker Fee: {maker_fee*100:.1f}%")
|
|
print(f" • Taker Fee: {taker_fee*100:.1f}%")
|
|
print(f" • Spread Cost: {spread_cost*100:.2f}%")
|
|
print(f" • Slippage Cost: {slippage_cost*100:.2f}%")
|
|
print("📊 3-Panel View: Price/SMAs, Portfolio (Net vs Gross), Trading Costs")
|
|
print("⚡ Realistic trading with all costs included!")
|
|
|
|
ws_url = "wss://stream.binance.com:9443/ws/btcusdt@kline_1s"
|
|
ws = websocket.WebSocketApp(ws_url, on_message=on_message, on_error=on_error,
|
|
on_close=on_close, on_open=on_open)
|
|
|
|
import threading
|
|
ws_thread = threading.Thread(target=ws.run_forever)
|
|
ws_thread.daemon = True
|
|
ws_thread.start()
|
|
|
|
try:
|
|
ani = FuncAnimation(fig, update_plot, interval=1000, blit=True, cache_frame_data=False)
|
|
plt.tight_layout()
|
|
plt.show()
|
|
|
|
time.sleep(simulation_duration)
|
|
except KeyboardInterrupt:
|
|
print("\n⚠️ Interrupted by user")
|
|
except Exception as e:
|
|
print(f"❌ Error: {e}")
|
|
finally:
|
|
print("🛑 Shutting down...")
|
|
ws.close()
|
|
|
|
# Print final summary with costs analysis
|
|
if entries or exits:
|
|
print(f"\n📊 LONG/SHORT TRADING SUMMARY WITH COSTS:")
|
|
long_entries_count = len([e for e in entries if e[2] == 'BUY'])
|
|
short_entries_count = len([e for e in entries if e[2] == 'SHORT'])
|
|
long_exits_count = len([e for e in exits if e[2] == 'CLOSE_LONG'])
|
|
short_exits_count = len([e for e in exits if e[2] == 'CLOSE_SHORT'])
|
|
|
|
print(f" Long Entries: {long_entries_count}")
|
|
print(f" Short Entries: {short_entries_count}")
|
|
print(f" Long Exits: {long_exits_count}")
|
|
print(f" Short Exits: {short_exits_count}")
|
|
print(f" Total Trades: {long_entries_count + short_entries_count}")
|
|
|
|
# Cost analysis
|
|
cost_percentage = (cumulative_costs / initial_capital) * 100
|
|
print(f"\n💸 COST ANALYSIS:")
|
|
print(f" Total Trading Costs: ${cumulative_costs:.2f}")
|
|
print(f" Cost as % of Capital: {cost_percentage:.3f}%")
|
|
print(f" Average Cost per Trade: ${cumulative_costs/(long_entries_count + short_entries_count):.2f}" if (long_entries_count + short_entries_count) > 0 else " No completed trades")
|
|
|
|
# Performance analysis
|
|
gross_return = ((portfolio_value + cumulative_costs - initial_capital) / initial_capital) * 100
|
|
net_return = ((portfolio_value - initial_capital) / initial_capital) * 100
|
|
print(f"\n📈 PERFORMANCE ANALYSIS:")
|
|
print(f" Gross Return (before costs): {gross_return:+.2f}%")
|
|
print(f" Net Return (after costs): {net_return:+.2f}%")
|
|
print(f" Cost Impact: {gross_return - net_return:.2f}%")
|
|
print(f" Final Portfolio: ${portfolio_value:,.2f}")
|
|
|
|
print("✅ Long/Short Strategy with Costs completed!") |