Overall Statistics
Total Orders
80
Average Win
3.85%
Average Loss
-2.19%
Compounding Annual Return
7.938%
Drawdown
7.400%
Expectancy
0.378
Start Equity
200000
End Equity
270265.18
Net Profit
35.133%
Sharpe Ratio
0.141
Sortino Ratio
0.065
Probabilistic Sharpe Ratio
11.524%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.76
Alpha
0.015
Beta
0.002
Annual Standard Deviation
0.104
Annual Variance
0.011
Information Ratio
-0.154
Tracking Error
0.182
Treynor Ratio
7.599
Total Fees
$11031.26
Estimated Strategy Capacity
$350000.00
Lowest Capacity Asset
BITO XSSNZDP7WC4L
Portfolio Turnover
4.44%
Drawdown Recovery
217
# region imports
from AlgorithmImports import *
# endregion

# Strategy overview:
#   - IBIT etf is bought on Friday 15 minutes before market close if current price is at 10d high.
#   - Close trade on Monday, 15 minutes before market close.

class BitcoinOvernightSession(QCAlgorithm):

    _notional_value: int = 200_000
    _trade_exec_minute_offset: int = 15
    
    _period: int = 10
    _trading_days: List[str] = [DayOfWeek.FRIDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY]

    # 1 - 3 trading days
    _selected_trading_days: int = 1
    
    def initialize(self) -> None:
        self.set_start_date(2022, 1, 1)
        self.set_cash(self._notional_value)

        _ticker: str = 'IBIT' if self.live_mode else 'BITO'

        self._traded_asset: Symbol = self.add_equity(_ticker, Resolution.MINUTE).symbol
        self._trading_days = self._trading_days[:self._selected_trading_days]

        self.log(f'Traded asset: {_ticker}')
        self.log(f'Trading days: {self._trading_days}')

        self._rebalance_flag: bool = False
        self.schedule.on(
            self.date_rules.every_day(),
            self.time_rules.before_market_close(self._traded_asset, self._trade_exec_minute_offset),
            self._rebalance
        )

    def on_data(self, slice: Slice) -> None:
        if not self._rebalance_flag:
            return
        self._rebalance_flag = False

        data_is_present: bool = slice.contains_key(self._traded_asset) and slice[self._traded_asset]
        if self.live_mode:
            self.log(f'New rebalance day. Data present: {data_is_present}. Weekday + 1: {self.time.weekday() + 1}')

        if data_is_present and self.time.weekday() + 1 in self._trading_days:
            history: DataFrame = self.history(TradeBar, self._traded_asset, self._period, Resolution.DAILY)
            if history.empty or len(history) < self._period:
                self.log(f'Insufficient data for a signal calculation history length {len(history)}; {self._period} needed')
                return

            recent_price: float = slice[self._traded_asset].close
            price_high: float = history.loc[self._traded_asset].close.max()
            trade_flag: bool = True if recent_price >= price_high else False
            
            self.log(f'Trade  flag: {trade_flag}. Recent price: {recent_price}. Price high: {price_high}')
            
            if trade_flag:
                if not self.portfolio[self._traded_asset].invested:
                    self.log('Condition met. Opening position.')

                    q: int = self._notional_value // slice[self._traded_asset].price
                    self.market_order(self._traded_asset, q) 
            else:
                if self.portfolio[self._traded_asset].invested:
                    self.log('Condition not met. Liquidating position.')
                    self.liquidate(self._traded_asset)
        else:
            if self.portfolio[self._traded_asset].invested:
                self.log('Not a trading day. Liquidating position.')
                self.liquidate(self._traded_asset)
        
    def _rebalance(self) -> None:
        self._rebalance_flag = True