Overall Statistics
Total Trades
54
Average Win
0.45%
Average Loss
-0.56%
Compounding Annual Return
6.007%
Drawdown
14.100%
Expectancy
0.330
Net Profit
33.919%
Sharpe Ratio
0.827
Probabilistic Sharpe Ratio
31.798%
Loss Rate
26%
Win Rate
74%
Profit-Loss Ratio
0.80
Alpha
0.052
Beta
-0.009
Annual Standard Deviation
0.062
Annual Variance
0.004
Information Ratio
-0.437
Tracking Error
0.183
Treynor Ratio
-5.911
Total Fees
$93.48
Estimated Strategy Capacity
$200000000.00
Lowest Capacity Asset
DBC TFVSB03UY0DH
from Model import Model
class SmoothVioletCoyote(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2015, 10, 1)  # Set Start Date
        self.SetEndDate(2020, 10, 1)
        self.SetCash(100000)  # Set Strategy Cash
        
        tickers = ['VTI', 'AGG', 'DBC', 'VIXY']
        
        for ticker in tickers:
            self.AddEquity(ticker, Resolution.Daily)
        
        n_periods = 51
        
        self.data = RollingWindow[Slice](n_periods) 
        
        self.Train(self.DateRules.MonthStart('VTI'), self.TimeRules.Midnight, self.Rebalance)
        
        self.model = None
        
        self.SetWarmup(n_periods)
        
        self.prev_day = -1
        
    def OnData(self, data):
        # this prevents duplicate bars that sometimes occurs because of SetWarmUp
        if self.prev_day != self.Time.day:
            self.data.Add(data)
        
        self.prev_day = self.Time.day

    def Rebalance(self):
        
        if not self.data.IsReady:
            return
        
        try:
            # since RollingWindow is recent at top, we need to reverse it
            data = self.PandasConverter.GetDataFrame(self.data).iloc[::-1]
        except:
            return
     
        # turn the closing prices for each equity into columns
        data = data['close'].unstack(level=0)
        
        # sometimes we are missing a row of data
        if len(data) < self.data.Count:
            return
        
        tickers = [symbol.split(' ')[0] for symbol in data.columns]
        
        if self.model is None:
            self.model = Model()
        
        allocations = self.model.get_allocations(data)
        self.Log(f'Portfolio Allocations: {allocations}')
        
        for ticker, allocation in zip(tickers, allocations):
            self.SetHoldings(ticker, allocation)
import tensorflow as tf
from tensorflow.keras.layers import LSTM, Flatten, Dense
from tensorflow.keras.models import Sequential
import tensorflow.keras.backend as K

class Model:
    def __init__(self):
        self.data = None
        self.model = None
        
    def __build_model(self, input_shape, outputs):
        model = Sequential([
            LSTM(64, input_shape=input_shape),
            Flatten(),
            Dense(outputs, activation='softmax')
        ])

        def sharpe_loss(_, y_pred):
            
            data = tf.divide(self.data, self.data[0])  
            
            
            portfolio_values = tf.reduce_sum(tf.multiply(data, y_pred), axis=1) 
            portfolio_returns = (portfolio_values[1:] - portfolio_values[:-1]) / portfolio_values[:-1]  

            sharpe = K.mean(portfolio_returns) / K.std(portfolio_returns)
            
            return -sharpe
        
        model.compile(loss=sharpe_loss, optimizer='adam')
        return model
    
    def get_allocations(self, data):
        
        data_w_ret = np.concatenate([ data.values[1:], data.pct_change().values[1:] ], axis=1)
        
        data = data.iloc[1:]
        self.data = tf.cast(tf.constant(data), float)
        
        if self.model is None:
            self.model = self.__build_model(data_w_ret.shape, len(data.columns))
        
        fit_predict_data = data_w_ret[np.newaxis,:]        
        self.model.fit(fit_predict_data, np.zeros((1, len(data.columns))), epochs=30, shuffle=False)
        return self.model.predict(fit_predict_data)[0]