| Overall Statistics |
|
Total Trades 2162 Average Win 0.47% Average Loss -3.34% Compounding Annual Return 166.046% Drawdown 17.300% Expectancy 0.140 Net Profit 966.588% Sharpe Ratio 1.15 Loss Rate 0% Win Rate 100% Profit-Loss Ratio 0.14 Alpha 0.955 Beta 0.659 Annual Standard Deviation 0.903 Annual Variance 0.815 Information Ratio 1.012 Tracking Error 0.901 Treynor Ratio 1.576 Total Fees $0.00 |
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using QuantConnect.Orders.Slippage;
using QuantConnect.Securities;
namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Bottom Fishing of Penny Stocks.
/// </summary>
public class PennyStocksBottomFishingAlgorithm : QCAlgorithm
{
static int daysInSlowEma = 45;
static int daysInFastEma = 3;
static int daysIn20Ema = 20;
// minimal stock price in dollars
private readonly decimal minStockPrice = 0.4m;
private readonly decimal maxStockPrice = 2.0m;
private readonly decimal minDollarVolume = 100000;
// moving averages by stock symbol
private readonly ConcurrentDictionary<Symbol, MovingAverages> averages = new ConcurrentDictionary<Symbol, MovingAverages>();
private readonly int maxNumberOfCandidates = 100;
private readonly decimal maxBuyOrdersAtOnce = 10;
List<Symbol> candidates = new List<Symbol>();
private class MovingAverages
{
public readonly ExponentialMovingAverage Fast;
public readonly ExponentialMovingAverage Slow;
public readonly ExponentialMovingAverage ema20;
public MovingAverages()
{
Fast = new ExponentialMovingAverage(daysInFastEma);
Slow = new ExponentialMovingAverage(daysInSlowEma);
ema20 = new ExponentialMovingAverage(daysIn20Ema);
}
// updates the EMA50 and EMA100 indicators, returning true when they're both ready
public bool Update(DateTime time, decimal value)
{
return Fast.Update(time, value) && Slow.Update(time, value) && ema20.Update(time, value);
}
}
Symbol benchmark = QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA);
ISlippageModel slippageModel;
// Custom slippage implementation
public class CustomSlippageModel : ISlippageModel
{
private readonly QCAlgorithm _algorithm;
public CustomSlippageModel(QCAlgorithm algorithm)
{
_algorithm = algorithm;
}
public decimal GetSlippageApproximation(Security asset, Order order)
{
return 0;
}
}
/// <summary>
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
/// </summary>
public override void Initialize()
{
UniverseSettings.Resolution = Resolution.Daily;
this.SetBrokerageModel(BrokerageName.Alpaca, AccountType.Cash);
this.slippageModel = new CustomSlippageModel(this);
SetStartDate(2016, 1, 1);
SetEndDate(2018, 6, 1);
SetCash(1000);
AddUniverse(CoarseSelectionFunction, FineSelectionFunction);
// TODO age
this.AddEquity(benchmark);
this.SetBenchmark(benchmark);
this.Log("TODO");
Schedule.On(DateRules.EveryDay("SPY"), TimeRules.BeforeMarketClose("SPY", 1), () =>
{
if (this.Securities["SPY"].Exchange.ExchangeOpen)
{
// this.Log("BeforeMarketClose");
CancelAllOpenOrders();
}
});
Schedule.On(DateRules.EveryDay("SPY"), TimeRules.Every(TimeSpan.FromMinutes(105)), () =>
{
if (this.Securities["SPY"].Exchange.ExchangeOpen)
{
// this.Log("Every 105 minutes");
Rebalance();
}
});
}
decimal BuyFactor = .9995m;
decimal SellFactor = 1.005m;
void Rebalance()
{
CancelAllOpenOrders();
TryToSellExistingPositions();
TryToBuy();
}
void CancelOpenBuyOrders()
{
var submittedTickets = Transactions.GetOrderTickets(
t => t.Status == OrderStatus.Submitted && t.Quantity > 0);
CancelOrders(submittedTickets);
}
bool HasOpenBuyOrders(Symbol symbol)
{
return Transactions.GetOrderTickets(
t => t.Status == OrderStatus.Submitted && t.Quantity > 0 && t.Symbol == symbol).Count() > 0;
}
void CancelAllOpenOrders()
{
var submittedTickets = Transactions.GetOrderTickets(
t => t.Status == OrderStatus.Submitted);
CancelOrders(submittedTickets);
}
void CancelOrders(IEnumerable<OrderTicket> tickets)
{
foreach (OrderTicket ticket in tickets)
{
this.Log("Cancelling order for: " + ticket.Symbol);
ticket.Cancel();
}
}
void TryToSellExistingPositions()
{
// Order sell at profit target in hope that somebody actually buys it
foreach (SecurityHolding security in this.Portfolio.Values)
{
if (security.Invested)
{
// TODO remove
if (!HasOpenBuyOrders(security.Symbol))
{
var costBasis = this.Portfolio[security.Symbol].AveragePrice;
// TODO age
var sellPrice = make_div_by_05(costBasis * SellFactor, false);
var quantity = -this.Portfolio[security.Symbol].Quantity;
this.Log("Sell order for: " + security.Symbol + ", quantity=" + quantity + ", sellPrice=" + sellPrice);
this.LimitOrder(security.Symbol, quantity, sellPrice);
}
}
}
}
// if cents not divisible by .05, round down if buy, round up if sell
decimal make_div_by_05(decimal s, bool buy = false)
{
s *= 20.00m;
if (buy)
{
s = Math.Floor(s);
}
else
{
s = Math.Ceiling(s);
}
s /= 20.00m;
return s;
}
void TryToBuy()
{
decimal weight = 1.00m / this.maxBuyOrdersAtOnce;
int numberOfOrders = 0;
foreach (Symbol candidate in this.candidates)
{
decimal averagePrice = this.GetOrCreateAverage(candidate).ema20;
decimal currentPrice = this.Securities[candidate].Price;
decimal buyPrice = currentPrice;
if (currentPrice <= 1.25m * averagePrice)
{
buyPrice = currentPrice * BuyFactor;
}
buyPrice = make_div_by_05(buyPrice, true);
if (buyPrice > 0)
{
int quantity = (int)Math.Round(weight * this.Portfolio.Cash / buyPrice, MidpointRounding.ToEven);
if (quantity > 0)
{
this.Log("Buy order for: " + candidate + ", quantity=" + quantity + ", buyPrice=" + buyPrice);
this.LimitOrder(candidate, quantity, buyPrice);
numberOfOrders++;
if (numberOfOrders >= this.maxBuyOrdersAtOnce)
{
break;
}
}
}
}
}
public IEnumerable<Symbol> CoarseSelectionFunction(IEnumerable<CoarseFundamental> coarse)
{
var selectedByPriceAndVolume = (from stock in coarse
where stock.Price >= this.minStockPrice
where stock.Price <= this.maxStockPrice
where stock.DollarVolume >= this.minDollarVolume
where stock.HasFundamentalData
select stock);
return SelectWorstPerformingStocks(selectedByPriceAndVolume).Select(stock => stock.Symbol);
}
public IEnumerable<Symbol> FineSelectionFunction(IEnumerable<FineFundamental> fine)
{
var selectedStocks = (from stock in fine
where stock.CompanyReference.CountryId == "USA"
where !stock.CompanyReference.IsLimitedPartnership
where !stock.CompanyReference.IsREIT
where stock.SecurityReference.ExchangeId != "OTC"
where !stock.SecurityReference.IsDepositaryReceipt
where stock.SecurityReference.ShareClassStatus == "A"
where stock.SecurityReference.SecurityType == "ST00000001"
select stock).Select(stock => stock.Symbol);
this.candidates.Clear();
foreach (Symbol selectedStock in selectedStocks)
{
this.AddEquity(selectedStock);
Security security = this.Securities[selectedStock];
if (security.Price >= this.minStockPrice && security.Price <= this.maxStockPrice)
{
this.candidates.Add(selectedStock);
this.Securities[selectedStock].SetSlippageModel(this.slippageModel);
}
// this.Log("Adding candidate: " + selectedStock + ", "+ security.Exchange +", "+ security.Price + ", " + security.AskPrice);
}
return this.candidates;
}
private IEnumerable<CoarseFundamental> SelectWorstPerformingStocks(IEnumerable<CoarseFundamental> coarse)
{
List<CoarseFundamental> selected = new List<CoarseFundamental>();
// select stocks with fast EMA below slow EMA
foreach (CoarseFundamental stock in coarse)
{
// create or update averages for the stock
MovingAverages average = GetOrCreateAverage(stock.Symbol);
// if indicators are ready
if (average.Update(stock.EndTime, stock.Price))
{
// if fast EMA is below slow EMA
if (average.Fast < average.Slow)
{
selected.Add(stock);
}
}
}
return selected.OrderBy(stock => CalculatePercentageDifference(stock.Symbol)).Take(this.maxNumberOfCandidates);
}
private decimal CalculatePercentageDifference(Symbol symbol)
{
var average = GetOrCreateAverage(symbol);
decimal difference = (average.Fast - average.Slow) / average.Slow;
return difference;
}
private MovingAverages GetOrCreateAverage(Symbol symbol)
{
MovingAverages average;
if (this.averages.ContainsKey(symbol))
{
this.averages.TryGetValue(symbol, out average);
}
else
{
average = new MovingAverages();
this.averages.TryAdd(symbol, average);
}
return average;
}
}
}