# Keltner Channel Volatility Breakout with ADX Trend Filter
#
# Layout convention (read by the Quarto report generator and the chart
# generator): "# %% <title>" starts a section; "#'" lines are prose shown
# above the section's code. Required globals when the script finishes:
# symbols, start_date, end_date, init_equity, portfolio.st, account.st,
# strategy.st, plus a function keltner_channel() if the strategy uses one.

# %% Environment
#' blotter is attached explicitly: quantstrat only lists it under Depends, and
#' some sessions do not attach it, which breaks `rm.strat()`/`initPortf()`.
library(blotter)
library(quantstrat)
library(TTR)
Sys.setenv(TZ = "UTC")
FinancialInstrument::currency("USD")

# %% Universe & Parameters
#' Six large-cap U.S. technology names plus QQQ over a fixed three-year
#' window. Each position is sized as one equal dollar slot
#' (starting equity / number of symbols), so the portfolio is long-only and
#' never levered.
symbols     <- c("QQQ", "NVDA", "AAPL", "MSFT", "AMD", "AMZN")
start_date  <- "2023-08-29"
end_date    <- "2026-08-27"
init_equity <- 100000

ema_n     <- 20    # Keltner centre line: EMA of the close
atr_n     <- 10    # Keltner band width: ATR period
atr_mult  <- 2     # Keltner band width: ATR multiplier
adx_n     <- 14    # trend-strength filter period
adx_min   <- 25    # entries require ADX above this level
slot_usd  <- init_equity / length(symbols)
cost_rate <- 0.0005  # 5 bps of traded value charged on every fill

stock(symbols, currency = "USD", multiplier = 1)

# %% Data
#' Daily bars from Yahoo Finance. Yahoo's close is already split-adjusted, so
#' `getSymbols(adjust = TRUE)` would apply the split a second time (NVDA's
#' 10-for-1 split in June 2024 would show up as a 10x price jump). Instead the
#' download is left raw and every OHLC column is scaled by Yahoo's own
#' Adjusted/Close ratio, which carries both splits and dividends once.
getSymbols(symbols, from = start_date, to = end_date, src = "yahoo", adjust = FALSE)
for (sym in symbols) assign(sym, adjustOHLC(get(sym), use.Adjusted = TRUE))

# Data sanity check: no single-day move above 40% in any adjusted series.
for (sym in symbols) {
  worst <- max(abs(dailyReturn(Cl(get(sym)))))
  if (worst > 0.40) stop(sprintf("%s has a %.0f%% one-day move: likely an unadjusted split.", sym, worst * 100))
}

# %% Portfolio, Account & Strategy Setup
portfolio.st <- "KeltnerADX"
account.st   <- "KeltnerADX"
strategy.st  <- "KeltnerADX"

suppressWarnings(try(rm.strat(portfolio.st), silent = TRUE))
suppressWarnings(try(rm.strat(account.st), silent = TRUE))
suppressWarnings(try(rm.strat(strategy.st), silent = TRUE))

initPortf(portfolio.st, symbols = symbols, initDate = start_date, currency = "USD")
initAcct(account.st, portfolios = portfolio.st, initDate = start_date, currency = "USD", initEq = init_equity)
initOrders(portfolio.st, initDate = start_date)
strategy(strategy.st, store = TRUE)

# %% Indicators
#' The Keltner Channel is an EMA centre line with bands set a multiple of the
#' Average True Range above and below it. ADX measures trend strength.
keltner_channel <- function(HLC, n_ema = 20, n_atr = 10, mult = 2) {
  mid <- EMA(Cl(HLC), n = n_ema)
  atr <- ATR(HLC, n = n_atr)[, "atr"]
  out <- cbind(mid - mult * atr, mid, mid + mult * atr)
  colnames(out) <- c("lower", "mid", "upper")
  out
}

add.indicator(strategy.st, name = "keltner_channel",
              arguments = list(HLC = quote(HLC(mktdata)), n_ema = ema_n, n_atr = atr_n, mult = atr_mult),
              label = "Keltner")
add.indicator(strategy.st, name = "ADX",
              arguments = list(HLC = quote(HLC(mktdata)), n = adx_n),
              label = "ADX")

# %% Signals
#' Entry fires on the first bar where the close is above the upper band AND
#' ADX is above its threshold. Exit fires when the close crosses below the
#' EMA centre line.
add.signal(strategy.st, name = "sigComparison",
           arguments = list(columns = c("Close", "upper.Keltner"), relationship = "gt"), label = "above_upper")
add.signal(strategy.st, name = "sigThreshold",
           arguments = list(column = "ADX.ADX", threshold = adx_min, relationship = "gt"), label = "adx_strong")
add.signal(strategy.st, name = "sigFormula",
           arguments = list(formula = "above_upper & adx_strong", cross = TRUE), label = "entry")
add.signal(strategy.st, name = "sigCrossover",
           arguments = list(columns = c("Close", "mid.Keltner"), relationship = "lt"), label = "exit")

# %% Rules
#' Orders fill at the next bar's open (`prefer = "Open"`), so a signal can only
#' be acted on after the bar that produced it has closed. Every fill pays 5 bps
#' of traded value.
fill_cost <- function(TxnQty, TxnPrice, Symbol) -abs(TxnQty * TxnPrice) * cost_rate

# Position sizing: whole shares worth one equal dollar slot at the signal bar's
# close, and nothing while the symbol is already held (no pyramiding).
osEqualSlot <- function(data, timestamp, orderqty, ordertype, orderside, portfolio, symbol, ruletype, ..., slot) {
  if (getPosQty(portfolio, symbol, timestamp) > 0) return(0)
  floor(slot / as.numeric(Cl(data[timestamp])))
}

add.rule(strategy.st, name = "ruleSignal",
         arguments = list(sigcol = "entry", sigval = TRUE, orderqty = 1, ordertype = "market",
                          orderside = "long", osFUN = "osEqualSlot", slot = slot_usd,
                          replace = FALSE, prefer = "Open", TxnFee = "fill_cost"),
         type = "enter", label = "EnterLong")

add.rule(strategy.st, name = "ruleSignal",
         arguments = list(sigcol = "exit", sigval = TRUE, orderqty = "all", ordertype = "market",
                          orderside = "long", replace = TRUE, prefer = "Open", TxnFee = "fill_cost"),
         type = "exit", label = "ExitLong")

# %% Run the Backtest
applyStrategy(strategy.st, portfolio.st)
updatePortf(portfolio.st)
updateAcct(account.st)
updateEndEq(account.st)

# %% Trade Statistics
#' One row per symbol: closed-trade count, net P&L, win rate and profit factor.
tstats <- tradeStats(portfolio.st)
print(tstats[, c("Symbol", "Num.Trades", "Net.Trading.PL", "Percent.Positive", "Profit.Factor", "Max.Drawdown")])
