| Overall Statistics |
|
Total Orders 26 Average Win 0.15% Average Loss 0% Compounding Annual Return -4.002% Drawdown 22.900% Expectancy 0 Start Equity 2000.00 End Equity 1558.51 Net Profit -22.075% Sharpe Ratio 0.181 Sortino Ratio 0.864 Probabilistic Sharpe Ratio 0.024% Loss Rate 0% Win Rate 100% Profit-Loss Ratio 0 Alpha 0.064 Beta -0.024 Annual Standard Deviation 0.34 Annual Variance 0.115 Information Ratio -0.112 Tracking Error 0.379 Treynor Ratio -2.616 Total Fees $0.00 Estimated Strategy Capacity $6100000.00 Lowest Capacity Asset ENAUSDT 18N Portfolio Turnover 0.02% |
from datetime import datetime, timedelta
import numpy as np
from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Data import *
from QuantConnect.Indicators import *
from QuantConnect.Orders import OrderStatus
class DCACryptoStrategy(QCAlgorithm):
def Initialize(self):
# Strategy configuration parameters
self.mode = self.GetParameter("mode", "training")
self.SetStartDate(2019, 1, 1)
self.SetEndDate(2025, 3, 31)
self.initial_cash = float(self.GetParameter("initial_cash", "1000"))
self.allocation = float(self.GetParameter("allocation", "0.1"))
self.sell_percentage = float(self.GetParameter("sell_percentage", "0.2"))
self.trading_symbols = self.GetParameter("trading_symbols", "TAOUSDT,ENAUSDT").split(',')
self.exchange = self.GetParameter("exchange", "binance")
# Minimum USDT required for purchases
self.min_purchase_amount = 10.0
self.min_usdt_threshold = 15.0 # Threshold to ensure meaningful purchases
# Track buying activities and P&L
self.last_buy_date = None
self.last_log_date = {}
self.realized_pnl = 0.0 # Track realized profits/losses
# Set initial cash
self.SetCash(self.initial_cash)
self.SetCash("USDT", self.initial_cash, 1.0)
# Set data resolution
self.resolution = Resolution.Daily
if self.mode == "live":
self.resolution = Resolution.Hour
# Dictionary to store symbols and indicators
self.symbols_data = {}
# Add each crypto symbol
for symbol_str in self.trading_symbols:
symbol = self.AddCrypto(symbol_str.strip(), self.resolution, self.exchange).Symbol
sma20 = self.SMA(symbol, 20, Resolution.Daily)
sma50 = self.SMA(symbol, 50, Resolution.Daily)
rsi = self.RSI(symbol, 14, MovingAverageType.Simple, Resolution.Daily)
self.symbols_data[symbol] = {
'symbol': symbol,
'sma20': sma20,
'sma50': sma50,
'rsi': rsi,
'price_history': []
}
# Set warmup period
self.SetWarmUp(50)
# Schedule daily screening
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(10, 0), self.DailyScreening)
def OnData(self, data):
if self.IsWarmingUp:
return
for symbol, symbol_data in self.symbols_data.items():
if data.ContainsKey(symbol) and data[symbol] is not None:
current_price = data[symbol].Close
symbol_data['price_history'].append(current_price)
if len(symbol_data['price_history']) > 30:
symbol_data['price_history'] = symbol_data['price_history'][-30:]
else:
self.Debug(f"No data available for {symbol} at {self.Time}")
def DailyScreening(self):
if self.IsWarmingUp:
return
current_date = self.Time.date()
if self.last_buy_date == current_date:
return
usdt_available = self.Portfolio.CashBook["USDT"].Amount
if usdt_available < self.min_usdt_threshold:
if self.SellForCash():
self.last_buy_date = current_date
self.Log(f"USDT balance after selling: ${self.Portfolio.CashBook['USDT'].Amount}")
return
else:
self.Log(f"Skipping DCA purchase: insufficient USDT (${usdt_available}) and no profitable positions to sell")
return
ranked_symbols = self.RankSymbols()
for symbol_data in ranked_symbols:
symbol = symbol_data['symbol']
original_data = self.symbols_data[symbol]
if not self.IndicatorsReady(original_data):
continue
if self.ShouldSell(symbol_data):
if self.ExecuteSell(symbol):
self.last_buy_date = current_date
return
for symbol_data in ranked_symbols:
symbol = symbol_data['symbol']
original_data = self.symbols_data[symbol]
if not self.IndicatorsReady(original_data):
continue
if self.ShouldBuy(symbol_data):
if self.ExecuteBuy(symbol):
self.last_buy_date = current_date
return
def SellForCash(self):
usdt_available = self.Portfolio.CashBook["USDT"].Amount
target_amount = self.min_usdt_threshold
if usdt_available >= target_amount:
return True
# Only sell profitable positions
profitable_positions = []
for symbol, data in self.symbols_data.items():
if self.Portfolio[symbol].Invested:
avg_price = self.Portfolio[symbol].AveragePrice
current_price = self.Securities[symbol].Price
profit_pct = (current_price / avg_price) - 1
if profit_pct >= 0.02: # Minimum 2% profit
profitable_positions.append({
'symbol': symbol,
'profit_pct': profit_pct,
'quantity': self.Portfolio[symbol].Quantity,
'current_price': current_price
})
profitable_positions.sort(key=lambda x: x['profit_pct'], reverse=True)
for position in profitable_positions:
symbol = position['symbol']
sell_quantity = min(position['quantity'] * self.sell_percentage, position['quantity'])
if self.ExecuteSell(symbol, sell_quantity):
usdt_available = self.Portfolio.CashBook["USDT"].Amount
self.Log(f"Raised cash by selling {sell_quantity} {symbol}. New USDT balance: ${usdt_available}")
if usdt_available >= target_amount:
return True
self.Log(f"Failed to raise enough USDT. Available: ${usdt_available}, Needed: ${target_amount}. No profitable positions to sell.")
return False
def IndicatorsReady(self, symbol_data):
return (symbol_data['sma20'].IsReady and
symbol_data['sma50'].IsReady and
symbol_data['rsi'].IsReady)
def RankSymbols(self):
ranked = []
for symbol, data in self.symbols_data.items():
if not self.IndicatorsReady(data):
continue
price = self.Securities[symbol].Price
rsi_score = data['rsi'].Current.Value / 100
if len(data['price_history']) > 0:
min_price = min(data['price_history'])
price_to_min_ratio = price / min_price if min_price > 0 else 1
else:
price_to_min_ratio = 1
sma_signal = 0
if price < data['sma20'].Current.Value:
sma_signal -= 0.3
if price < data['sma50'].Current.Value:
sma_signal -= 0.3
buy_score = rsi_score + (price_to_min_ratio - 1) + sma_signal
ranked.append({
'symbol': symbol,
'buy_score': buy_score,
'rsi': data['rsi'].Current.Value,
'sma20': data['sma20'].Current.Value,
'sma50': data['sma50'].Current.Value,
'price_history': data['price_history'],
'current_price': price
})
return sorted(ranked, key=lambda x: x['buy_score'])
def ShouldBuy(self, symbol_data):
return True # DCA strategy always wants to buy
def ShouldSell(self, symbol_data):
symbol = symbol_data['symbol']
if not self.Portfolio[symbol].Invested:
return False
current_price = symbol_data['current_price']
rsi = symbol_data['rsi']
avg_price = self.Portfolio[symbol].AveragePrice
# Profitability check
profitable = current_price > avg_price * 1.02 # Minimum 2% profit
if not profitable:
return False
overbought = rsi > 70
near_highs = False
if len(symbol_data['price_history']) > 5:
min_price = min(symbol_data['price_history'])
max_price = max(symbol_data['price_history'])
price_range = max_price - min_price
if price_range > 0:
relative_position = (current_price - min_price) / price_range
near_highs = relative_position > 0.8
return overbought or near_highs
def GetMinimumOrderSize(self, symbol):
return 10.0
def CalculateOrderQuantity(self, symbol, dollar_amount):
price = self.Securities[symbol].Price
if price <= 0:
return 0
raw_quantity = dollar_amount / price
lot_size = self.GetLotSize(symbol)
quantity = np.floor(raw_quantity / lot_size) * lot_size
if quantity * price < self.GetMinimumOrderSize(symbol):
return 0
return quantity
def GetLotSize(self, symbol):
symbol_str = str(symbol)
if "ETH" in symbol_str:
return 0.0001
else:
return 1.0
def ExecuteBuy(self, symbol):
usdt_available = self.Portfolio.CashBook["USDT"].Amount
allocation_amount = usdt_available * self.allocation # Dynamic allocation
if usdt_available < self.min_usdt_threshold:
current_date = self.Time.date()
if symbol not in self.last_log_date or self.last_log_date[symbol] != current_date:
self.Debug(f"Not enough USDT for {symbol} purchase: have ${usdt_available}, need ${self.min_usdt_threshold}")
self.last_log_date[symbol] = current_date
return False
if allocation_amount < self.min_purchase_amount:
allocation_amount = min(usdt_available * 0.99, self.min_purchase_amount)
quantity = self.CalculateOrderQuantity(symbol, allocation_amount)
if quantity <= 0:
current_date = self.Time.date()
if symbol not in self.last_log_date or self.last_log_date[symbol] != current_date:
self.Debug(f"Skipping buy for {symbol} - calculated quantity too small: {quantity}")
self.last_log_date[symbol] = current_date
return False
price = self.Securities[symbol].Price
actual_cost = quantity * price
order_ticket = self.MarketOrder(symbol, quantity)
self.Log(f"Buying {quantity} {symbol} at ${price} for ${actual_cost}")
return True
def ExecuteSell(self, symbol, custom_quantity=None, force=False):
if not self.Portfolio[symbol].Invested:
return False
if custom_quantity is not None:
sell_quantity = min(custom_quantity, self.Portfolio[symbol].Quantity)
else:
holdings = self.Portfolio[symbol].Quantity
sell_quantity = holdings * self.sell_percentage
lot_size = self.GetLotSize(symbol)
sell_quantity = np.floor(sell_quantity / lot_size) * lot_size
if sell_quantity <= 0:
self.Debug(f"Skipping sell for {symbol} - calculated quantity too small: {sell_quantity}")
return False
avg_price = self.Portfolio[symbol].AveragePrice
current_price = self.Securities[symbol].Price
is_profitable = current_price > avg_price * 1.02 # Minimum 2% profit
if not is_profitable and not force:
self.Debug(f"Skipping sell for {symbol} - not profitable: current ${current_price}, avg ${avg_price}")
return False
order_ticket = self.MarketOrder(symbol, -sell_quantity)
self.Log(f"Selling {sell_quantity} {symbol} at ${current_price} for ${sell_quantity * current_price}")
# Update realized P&L
profit_loss = (current_price - avg_price) * sell_quantity
self.realized_pnl += profit_loss
return True
def OnEndOfAlgorithm(self):
self.Log(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue}")
total_unrealized_pnl = 0.0
for symbol in self.symbols_data:
if self.Portfolio[symbol].Invested:
qty = self.Portfolio[symbol].Quantity
avg_price = self.Portfolio[symbol].AveragePrice
current_price = self.Securities[symbol].Price
unrealized_pnl = (current_price - avg_price) * qty
total_unrealized_pnl += unrealized_pnl
profit_loss_pct = ((current_price / avg_price) - 1) * 100
self.Log(f"{symbol}: {qty} units, avg price: ${avg_price}, current: ${current_price}")
self.Log(f"Unrealized P&L: ${unrealized_pnl} ({profit_loss_pct:.2f}%)")
total_pnl = self.realized_pnl + total_unrealized_pnl
initial_value = self.initial_cash
total_pnl_pct = ((self.Portfolio.TotalPortfolioValue / initial_value) - 1) * 100
self.Log(f"Realized P&L: ${self.realized_pnl:.2f}")
self.Log(f"Total Unrealized P&L: ${total_unrealized_pnl:.2f}")
self.Log(f"Total P&L: ${total_pnl:.2f} ({total_pnl_pct:.2f}%)")
self.Log("Note: Unrealized P&L reflects current market prices and may fluctuate. DCA strategies expect long-term accumulation.")