Onepagecode

Onepagecode

Quant Trading & Backtesting: A Framework for Portfolio Optimization

Translating predictive alpha signals into risk-managed execution and performance evaluation

Onepagecode's avatar
Onepagecode
Jun 20, 2026
∙ Paid

Subscribe to further post, making final tweak in the code base, it will be availble in the next article!

In this chapter, we will take those alpha factors a step further and turn them into actionable buy and sell signals by using the custom MeanReversion factor built in the previous chapter.

Required imports

import warnings
warnings.filterwarnings('ignore')

The purpose of this cell is to quiet down warning messages before the rest of the notebook runs. It first brings in Python’s warning-handling tools, then tells the interpreter to ignore warnings from that point onward. Behind the scenes, that means messages that would normally appear about deprecations, harmless numerical issues, or other non-fatal alerts will no longer clutter the notebook output. Since the cell only changes how warnings are displayed and does not perform any calculation or print anything, there is no saved output.

import sys
from pytz import UTC
import logbook

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from logbook import (NestedSetup, NullHandler, Logger, 
                     StreamHandler, StderrHandler, 
                     INFO, WARNING, DEBUG, ERROR)

from zipline import run_algorithm
from zipline.api import (attach_pipeline,
                         date_rules,
                         time_rules,
                         get_datetime,
                         order_target_percent,
                         pipeline_output,
                         record, schedule_function,
                         get_open_orders,
                         calendars,
                         set_commission, 
                         set_slippage)
from zipline.finance import commission, slippage
from zipline.pipeline import Pipeline, CustomFactor
from zipline.pipeline.factors import Returns, AverageDollarVolume

from pypfopt.efficient_frontier import EfficientFrontier
from pypfopt import risk_models, objective_functions
from pypfopt import expected_returns
from pypfopt.exceptions import OptimizationError

from pyfolio.utils import extract_rets_pos_txn_from_zipline

The purpose of this cell is to gather all the tools needed for the strategy, backtest, optimization, and performance analysis that follow later in the notebook. Nothing is calculated yet, so there is no saved output to show; the cell simply prepares the environment by loading the libraries and functions that the later cells will rely on.

It starts by importing standard utilities such as system access, timezone handling, and logging support. Those are useful for controlling the notebook’s runtime behavior and making sure timestamps are handled consistently in UTC, which matters when working with market data and trading calendars. After that, it brings in the main data science libraries used throughout the analysis: pandas for tabular data, NumPy for numerical work, Matplotlib and Seaborn for plotting and styling.

The next set of imports sets up Logbook, which is used to manage messages, warnings, and informational output from the backtest in a cleaner way. Then the Zipline imports come in, which are the core of the trading simulation. These provide the engine for running the algorithm, attaching a pipeline, scheduling functions at specific times, placing orders, checking open orders, and setting commissions and slippage so the simulation includes trading frictions. The pipeline imports also bring in the building blocks for constructing a custom factor and screening the stock universe based on things like returns and average dollar volume.

After that, the notebook loads PyPortfolioOpt, which supplies the portfolio optimization machinery used later to choose weights for the selected long and short stocks. The risk model, expected return, and objective function modules are imported because the strategy needs estimates of return and covariance, along with the optimization routines that maximize the portfolio’s Sharpe ratio. An optimization error class is also imported so the code can handle failed optimization attempts gracefully if the available data or portfolio constraints make the problem unsolvable.

Finally, the notebook imports a pyfolio utility that extracts returns, positions, and transactions from a Zipline backtest. That function becomes important at the end of the workflow, when the simulated trading results are converted into a format that can be analyzed and plotted. Since this cell only sets up dependencies and does not run any analysis itself, it finishes quietly without producing output.

sns.set_style('whitegrid')

The purpose of this step is to set a cleaner visual style for any plots that come later in the notebook. By choosing a whitegrid theme, the notebook tells Seaborn to draw charts with a white background and light grid lines, which usually makes time series and performance comparisons easier to read. Nothing is calculated or displayed here, so there is no saved output. The effect happens behind the scenes: once this style is set, subsequent graphs inherit the same look unless a later cell changes it again.

Logging Configuration

# setup stdout logging
format_string = '[{record.time: %H:%M:%S.%f}]: {record.level_name}: {record.message}'
zipline_logging = NestedSetup([NullHandler(level=DEBUG),
                               StreamHandler(sys.stdout, format_string=format_string, level=INFO),
                               StreamHandler(sys.stdout, format_string=format_string, level=WARNING),
                               StreamHandler(sys.stderr, level=ERROR)])
zipline_logging.push_application()
log = Logger('Algorithm')

The purpose here is to set up logging so the backtest’s messages are easy to read while it runs. A custom message format is defined first, which tells each log entry to show the time, the severity level, and the actual message. That makes the output much more informative than plain text, because you can immediately see when something happened and whether it was just informational, a warning, or an error.

Next, a logging configuration is assembled that routes different kinds of messages to different places. Lower-level debug messages are suppressed, ordinary informational and warning messages are sent to standard output, and errors are sent to standard error. Behind the scenes, this helps separate normal progress updates from serious problems, which is especially useful in a long-running trading simulation where a lot of internal activity can be happening at once.

After that configuration is pushed into the application, a named logger for the algorithm is created. That logger becomes the object the rest of the strategy can use to write messages in a consistent format. There is no saved output because nothing is being calculated or displayed yet; the cell is simply preparing the notebook so later parts of the backtest can produce cleaner, more structured logs.

Algorithm Settings

# Settings
MONTH = 21
YEAR = 12 * MONTH
N_LONGS = 50
N_SHORTS = 50
MIN_POS = 5
VOL_SCREEN = 1000

This cell sets the basic trading and screening parameters that the rest of the strategy will rely on. First it defines a month as 21 trading days, which is a common approximation in market data because it counts trading sessions rather than calendar days. From there, a year is built as 12 of those trading months, giving a 252-trading-day lookback window that lines up with standard finance conventions.

The next values control how many stocks the strategy will hold on each side of the portfolio. It aims for 50 long positions and 50 short positions, so the portfolio is designed to be balanced between bets on stocks expected to rise and stocks expected to fall. The minimum position count is set to 5, which acts as a safeguard before optimization is attempted; the strategy wants enough candidates on both sides before it tries to build a meaningful portfolio. Finally, the volume screen is set to 1000, which means the universe will be limited to the 1000 most actively traded stocks by average dollar volume. That filter helps keep the strategy focused on liquid names, where trading is more realistic and transaction costs are less likely to dominate results.

There is no saved output here because the cell is not producing results yet. It is preparing constants that will be reused later when the factor is built, the universe is filtered, and the portfolio is rebalanced.

start = pd.Timestamp('2013-01-01', tz=UTC)
end = pd.Timestamp('2017-01-01', tz=UTC)
capital_base = 1e7

The cell sets up the basic backtest window and the amount of starting capital for the strategy. It creates a start timestamp for the beginning of 2013 and an end timestamp for the beginning of 2017, and both are marked with the UTC time zone so they line up correctly with Zipline’s trading calendar and data handling. It also defines the capital base as 10 million dollars, which tells the backtester how much money the portfolio starts with before any trades are made.

Nothing is printed when this runs, so there is no visible output. The effect is purely to establish a few important parameters that later parts of the notebook will use when the algorithm is actually executed.

Mean Reversion Factor

class MeanReversion(CustomFactor):
    """Compute ratio of latest monthly return to 12m average,
       normalized by std dev of monthly returns"""
    inputs = [Returns(window_length=MONTH)]
    window_length = YEAR

    def compute(self, today, assets, out, monthly_returns):
        df = pd.DataFrame(monthly_returns)
        factor = df.iloc[-1].sub(df.mean()).div(df.std())
        out[:] = factor

The purpose of this cell is to define the custom factor that powers the strategy’s stock ranking. It creates a new Zipline factor class called MeanReversion, which tells the pipeline how to turn recent return data into a single score for each asset. The short docstring describes the intent: compare the most recent monthly return with the stock’s average monthly return over the past year, then normalize that difference by the volatility of those monthly returns.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 Onepagecode · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture