# Sector Momentum Breakout & Rotation -- R / quantstrat
# Strategy code: data, indicators, signals, rules, backtest execution, basic stats.
# Companion file: sector-momentum-R-analysis.R
# Full notebook: https://quantstr.at/wp-content/uploads/reports/sector-momentum-R.html

suppressPackageStartupMessages({
  library(blotter)
  library(quantstrat)
  library(PerformanceAnalytics)
  library(TTR)
  library(gt)
})
Sys.setenv(TZ = "UTC")
# gt::currency() would otherwise mask FinancialInstrument::currency() once
# gt is loaded, so this one call stays namespace-qualified.
FinancialInstrument::currency("USD")

symbols <- c("XLK", "XLF", "XLI", "XLE", "XLV", "XLY")
start_date <- "2023-08-29"
end_date   <- "2026-08-27"
init_equity <- 100000

stock(symbols, currency = "USD", multiplier = 1)

# Some Yahoo Finance split-adjusted histories contain a spurious
# adjustment factor applied before a given cutoff date that does not
# correspond to any real split in the raw (unadjusted) price series.
# This block detects and removes any such phantom adjustment factor
# before running the strategy.
affected_symbols <- c("XLK", "XLE", "XLY")

fix_symbol <- function(sym) {
  raw <- getSymbols(sym, from = start_date, to = end_date, auto.assign = FALSE, src = "yahoo", adjust = FALSE)
  adj <- getSymbols(sym, from = start_date, to = end_date, auto.assign = FALSE, src = "yahoo", adjust = TRUE)
  raw_cl <- as.numeric(Cl(raw)); adj_cl <- as.numeric(Cl(adj))
  ratio <- adj_cl / raw_cl
  ratio_change <- c(NA, diff(ratio) / head(ratio, -1))
  jump_idx <- which(abs(ratio_change) > 0.3)
  if (length(jump_idx) == 0) return(adj)
  j <- jump_idx[1]
  correction_factor <- ratio[j] / ratio[j - 1]
  corrected <- adj
  corrected[1:(j - 1), ] <- adj[1:(j - 1), ] * correction_factor
  corrected
}

for (sym in affected_symbols) {
  assign(sym, fix_symbol(sym), envir = .GlobalEnv)
}
for (sym in setdiff(symbols, affected_symbols)) {
  assign(sym, getSymbols(sym, from = start_date, to = end_date, auto.assign = FALSE, src = "yahoo", adjust = TRUE), envir = .GlobalEnv)
}

portfolio.st <- "blog_portfolio"
account.st   <- "blog_account"
strategy.st  <- "blog_strategy"

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)

for (sym in symbols) {
  addPosLimit(portfolio.st, sym, timestamp = start_date, maxpos = 300)
}

# Indicators: 10-day EMA, 30-day EMA, 14-day RSI
add.indicator(strategy.st, name = "EMA", arguments = list(x = quote(Cl(mktdata)), n = 10), label = "ema10")
add.indicator(strategy.st, name = "EMA", arguments = list(x = quote(Cl(mktdata)), n = 30), label = "ema30")
add.indicator(strategy.st, name = "RSI", arguments = list(price = quote(Cl(mktdata)), n = 14), label = "rsi14")

# Signals
add.signal(strategy.st, name = "sigComparison", arguments = list(columns = c("ema10", "ema30"), relationship = "gt"), label = "ema_gt")
add.signal(strategy.st, name = "sigCrossover", arguments = list(columns = c("ema10", "ema30"), relationship = "lt"), label = "bearish_ema_cross")
add.signal(strategy.st, name = "sigThreshold", arguments = list(column = "rsi14", threshold = 50, relationship = "gt"), label = "rsi_gt_50")
add.signal(strategy.st, name = "sigFormula", arguments = list(formula = "ema_gt & rsi_gt_50", cross = TRUE), label = "entry_signal")

# Rules: T+1 open fill, 5 bps slippage, fixed 300-share size, replace-on-exit
add.rule(strategy.st, name = "ruleSignal",
         arguments = list(sigcol = "entry_signal", sigval = TRUE,
                          orderqty = 300, ordertype = "market", orderside = "long",
                          osFUN = "osMaxPos", replace = FALSE, prefer = "Open", TxnFee = -0.0005),
         type = "enter", label = "EnterLong")

add.rule(strategy.st, name = "ruleSignal",
         arguments = list(sigcol = "bearish_ema_cross", sigval = TRUE,
                          orderqty = "all", ordertype = "market", orderside = "long",
                          replace = TRUE, prefer = "Open", TxnFee = -0.0005),
         type = "exit", label = "ExitLong")

applyStrategy(strategy.st, portfolio.st)
updatePortf(portfolio.st)
updateAcct(account.st)
updateEndEq(account.st)

strategy_eq_r1 <- getAccount(account.st)$summary$End.Eq
final_eq <- as.numeric(last(strategy_eq_r1))
total_ret_pct <- (final_eq / init_equity - 1) * 100
strategy_ret_r1 <- dailyReturn(strategy_eq_r1)
ann_ret_pct <- as.numeric(Return.annualized(strategy_ret_r1)) * 100
sharpe <- as.numeric(SharpeRatio.annualized(strategy_ret_r1, Rf = 0))
maxdd_pct <- as.numeric(maxDrawdown(strategy_ret_r1)) * 100

tstats_kpi <- tradeStats(portfolio.st)
n_trades <- sum(tstats_kpi$Num.Trades)
# tradeStats() reports Percent.Positive per symbol, not a raw winner count --
# reconstruct total winners from it (Percent.Positive = winners / trades * 100
# per symbol, so this recovers an integer count exactly).
n_winners <- sum(round(tstats_kpi$Percent.Positive / 100 * tstats_kpi$Num.Trades))
win_rate <- n_winners / n_trades * 100
profit_factor <- sum(tstats_kpi$Gross.Profits) / abs(sum(tstats_kpi$Gross.Losses))

tstats <- tradeStats(portfolio.st)
