# Common Task Framework submission function
main <- function(chars, features, daily_ret) {
  
  # ------------------------------------------------------------
  # 1. Extract the names of the predictor characteristics
  # ------------------------------------------------------------
  
  feature_names <- as.character(features$features)
  
  
  # ------------------------------------------------------------
  # 2. Define the monthly preprocessing function
  # ------------------------------------------------------------
  
  preprocess_month <- function(data, feature_names) {
    
    data |>
      dplyr::mutate(
        dplyr::across(
          dplyr::all_of(feature_names),
          ~ {
            x <- .x
            
            # If the entire characteristic is missing this month,
            # assign a neutral standardized value of 0
            if (all(is.na(x))) {
              
              rep(0, length(x))
              
            } else {
              
              # Replace missing observations with the
              # cross-sectional median for that month
              x[is.na(x)] <- median(
                x,
                na.rm = TRUE
              )
              
              # Calculate the cross-sectional standard deviation
              x_sd <- sd(x)
              
              # If there is no usable cross-sectional variation,
              # assign a neutral standardized value of 0
              if (!is.finite(x_sd) || x_sd == 0) {
                
                rep(0, length(x))
                
              } else {
                
                # Otherwise standardize to mean 0
                # and standard deviation 1
                (x - mean(x)) / x_sd
                
              }
            }
          }
        )
      )
  }
  
  
  # ------------------------------------------------------------
  # 3. Preprocess every month separately
  # ------------------------------------------------------------
  
  chars_processed <- chars |>
    dplyr::group_by(eom) |>
    dplyr::group_modify(
      ~ preprocess_month(.x, feature_names)
    ) |>
    dplyr::ungroup()
  
  test_flag <- toupper(as.character(chars_processed$ctff_test)) %in% c("1", "TRUE", "T")
  
  
  # ------------------------------------------------------------
  # 4. Convert the predictors to a numerical matrix
  # ------------------------------------------------------------
  
  X <- as.matrix(
    chars_processed[, feature_names]
  )
  
  
  # ------------------------------------------------------------
  # 5. Store the next-month excess returns
  # ------------------------------------------------------------
  
  y <- chars_processed$ret_exc_lead1m
  
  
  # ------------------------------------------------------------
  # 6. Identify the test months
  #
  # Only ctff_test observations affect the CTF score.
  # Historical non-test observations remain available for
  # training the model.
  # ------------------------------------------------------------
  
  test_months <- sort(
    unique(
      chars_processed$eom[
        test_flag
      ]
    )
  )
  
  
  # ------------------------------------------------------------
  # 7. Determine the annual model-refitting dates
  #
  # Fit once at the first test month, then refit every January.
  # ------------------------------------------------------------
  
  first_test_month <- test_months[1]
  
  january_test_months <- test_months[
    format(test_months, "%m") == "01"
  ]
  
  refit_months <- sort(
    unique(
      c(
        first_test_month,
        january_test_months
      )
    )
  )
  
  
  # ------------------------------------------------------------
  # 8. Determine when each fitted model stops being used
  # ------------------------------------------------------------
  
  prediction_end <- c(
    refit_months[-1],
    max(test_months) + 1
  )
  
  
  # ------------------------------------------------------------
  # 9. Create storage for predictions
  # ------------------------------------------------------------
  
  prediction_list <- vector(
    "list",
    length(refit_months)
  )
  
  
  # ------------------------------------------------------------
  # 10. Use the ridge penalty selected from the
  # historical validation period
  # ------------------------------------------------------------
  
  ridge_lambda <- 0.04551499
  
  
  # ------------------------------------------------------------
  # 11. Fit the expanding-window ridge models
  # ------------------------------------------------------------
  
  for (j in seq_along(refit_months)) {
    
    # Current annual refitting date
    refit_date <- refit_months[j]
    
    # End of the period predicted by this fitted model
    end_date <- prediction_end[j]
    
    
    # ----------------------------------------------------------
    # Training observations
    #
    # A return may enter the model only if its associated
    # return month-end has occurred by the refitting date.
    #
    # Also require the outcome to be nonmissing.
    # ----------------------------------------------------------
    
    training_rows <-
      chars_processed$eom_ret <= refit_date &
      !is.na(y)
    
    
    # ----------------------------------------------------------
    # Prediction observations
    #
    # Predict only ctff_test observations belonging to the
    # period covered by the current annual model.
    # ----------------------------------------------------------
    
    prediction_rows <-
      test_flag &
      chars_processed$eom >= refit_date &
      chars_processed$eom < end_date
    
    
    # ----------------------------------------------------------
    # Fit the ridge model
    # ----------------------------------------------------------
    
    annual_model <- glmnet::glmnet(
      x = X[training_rows, , drop = FALSE],
      y = y[training_rows],
      alpha = 0,
      lambda = ridge_lambda,
      standardize = FALSE
    )
    
    
    # ----------------------------------------------------------
    # Generate predicted next-month returns
    # ----------------------------------------------------------
    
    annual_predictions <- predict(
      annual_model,
      newx = X[prediction_rows, , drop = FALSE]
    )
    
    
    # ----------------------------------------------------------
    # Store identifiers, dates, and predictions
    # ----------------------------------------------------------
    
    prediction_list[[j]] <-
      chars_processed[prediction_rows, ] |>
      dplyr::select(
        id,
        eom
      ) |>
      dplyr::mutate(
        predicted_return =
          as.numeric(annual_predictions)
      )
    
    
    # ----------------------------------------------------------
    # Free memory before fitting the next annual model
    # ----------------------------------------------------------
    
    rm(
      annual_model,
      annual_predictions
    )
    
    gc()
  }
  
  
  # ------------------------------------------------------------
  # 12. Combine predictions from all annual models
  # ------------------------------------------------------------
  
  predictions <- dplyr::bind_rows(
    prediction_list
  )
  
  
  # ------------------------------------------------------------
  # 13. Convert predicted returns into portfolio weights
  #
  # Higher predicted returns receive positive weights.
  # Lower predicted returns receive negative weights.
  #
  # Each month:
  #
  #     long exposure  = +1
  #     short exposure = -1
  #     gross exposure =  2
  #     net exposure   =  0
  # ------------------------------------------------------------
  
  output <- predictions |>
    dplyr::group_by(eom) |>
    dplyr::mutate(
      
      prediction_rank = rank(
        predicted_return,
        ties.method = "average"
      ),
      
      centered_rank =
        prediction_rank -
        mean(prediction_rank),
      
      w =
        2 * centered_rank /
        sum(abs(centered_rank))
      
    ) |>
    dplyr::ungroup()
  
  
  # ------------------------------------------------------------
  # 14. Return exactly the columns required by the CTF
  # ------------------------------------------------------------
  
  output |>
    dplyr::select(
      id,
      eom,
      w
    )
}