# Factor regression (factor_regress)

Desk: Equities. Page: https://hedgefund.md/models/factor_regress

**What it asks.** After the factors, how much of this return stream is left, and can that intercept be told apart from zero?

Fits a return series on up to 6 factor series by least squares with an intercept: annualized intercept and its t-stat, betas with t-stats, R squared, adjusted R squared, residual vol and Durbin-Watson. Every return comes from the caller. It describes one sample of numbers. It doesn't test a strategy, adjust for data mining or say what comes next.

Results are hypothetical: arithmetic on the numbers sent, a made-up template or a stated default. Nobody achieved them, and they aren't a forecast, a recommendation, an offer or a record of real trading. Don't show any output to investors or clients, or use it to market an investment. Terms: https://hedgefund.md/terms

## Inputs

| Input | Type | Range | Required | What it is |
| --- | --- | --- | --- | --- |
| `returns_pct` | array | 3 to 2000 items | yes | The series to explain, per period, in percent. |
| `factors` | array | 1 to 6 items | yes | The factor series, same length and order as returns_pct. |
| `factors[].name` | string |  | yes | A label for the factor, such as Market. |
| `factors[].returns_pct` | array | 3 to 2000 items | yes | The factor's return per period, in percent. |
| `periods_per_year` | number | 1 to 366 | yes | Periods per year: 12 for monthly, 252 for daily. Used only to annualize. |

## Template

Made-up numbers that run as they stand, so you can see the shape of a result. They aren't market levels, typical values or estimates, and the names in them don't exist. Replace every number before you read anything into a run.

```json
{
  "returns_pct": [
    2.26, -2.73, -6.84, -0.96, -0.94, 1.51, 0.79, -3.29, 0.74, 4.22,
    1.14, 0.79, 1.64, 3.65, 1.9, -2.27, 3.89, 1.22, 1.86, 4.2,
    5.94, -1.19, 4.04, 1.5, -0.32, 3.76, 1.19, 1.88, -1.62, 8.44,
    -5.48, -1.43, 2.55, -6.1, 1.01, 5.14, 0, 5.84, -6.17, 5.03,
    -0.1, 2.16, 0.76, 1.76, 2.87, 1.43, 2.58, 2.25, -0.89, -0.94,
    -3.84, -0.21, -0.16, 4.31, 0.29, -1.73, 0.02, -3.21, 1.87, -0.15
  ],
  "factors": [
    {
      "name": "Market",
      "returns_pct": [
        1.83, -3.89, -4.14, -0.61, -0.41, 1.85, -2.03, -5.63, -0.58, 4.06,
        0.23, 2.9, 3.52, 6.22, -1.58, -0.51, 4.9, 0.08, 2.8, 4.4,
        7.34, -1.73, 3.06, -0.23, -0.39, 6.35, 3.4, -0.4, -0.72, 11.32,
        -6.12, -2.09, 4.53, -5.66, 1.26, 4.4, -0.86, 6.6, -7.2, 4.85,
        3.05, 1.23, 1.69, 0.47, 3.1, 1.98, 2.83, 2.7, -2.35, -1.17,
        -4.24, 0.54, -1.72, 5.07, -0.45, 2.44, 1.29, -5.27, -0.43, -5.14
      ]
    },
    {
      "name": "Value",
      "returns_pct": [
        2.52, 2.35, -4.46, -1.08, -1.39, -0.28, 0.35, -1.25, -1.49, 0.47,
        1.79, -1.21, -1.27, -0.67, 1.02, 0.23, 2.32, 2.72, 1.07, 4.66,
        -0.99, -1.01, 1.03, -1.42, 0.14, -3.82, -0.46, 1.41, -2.48, -0.59,
        -0.23, -0.31, -1.07, -1.71, 0.06, -3.23, -1.87, -0.03, -1.9, 1.04,
        -3.99, 2.52, -1.33, -1.08, 1.3, -2.5, 2.11, 0.82, -1.17, 0.11,
        -0.8, 2.91, 0.71, 0.01, -1.41, -3.47, -0.95, 0.49, 2.52, -0.37
      ]
    },
    {
      "name": "Momentum",
      "returns_pct": [
        2.48, -3.37, -2.54, 0.88, 2.98, -1.93, 5.53, -2.9, 0.08, 2.35,
        -1.7, 0.95, -2.15, -1.16, -4.11, -5.82, 0.58, -0.26, 2.2, -3.37,
        2.33, 2.48, -3.26, 3.74, 3.2, 1.06, 1.92, 0.13, 1.29, 7.79,
        1.65, 4.04, -0.31, 0.36, 0.24, 3.56, 2.35, 3.2, -0.13, -0.3,
        -2.95, 2.75, 5.03, 3.83, -0.11, 3.02, -1.87, 5.2, -5.05, 3.42,
        -0.02, 1.59, 0.09, -1.39, 0.54, 0.92, -1.64, -7.58, -0.13, 5.98
      ]
    }
  ],
  "periods_per_year": 12
}
```

## What the template gives

On these numbers, the factors explain 85.52% of the variance, and the annualized intercept is 2.99% with a t-stat of 1.55, against a mean of 9.17% a year on the series sent.

- Annualized intercept: 2.9858% a year
- Intercept t-stat: 1.5455 (watch)
- R squared: 0.8552
- Adjusted R squared: 0.8474
- Annualized residual vol: 4.1459%
- Durbin-Watson: 1.7445
- Observations per factor: 20 (ok)
- Beta on Market: 0.701563

Where it flips, with the other inputs held:

- `observations` at 101: About this many observations would bring the intercept's t-stat to 2, if the intercept and residual vol stayed as they are.

## Formula

y_t = a + sum b_j x_jt + e_t, by the normal equations on centered, scaled factors. s^2 = RSS / (n - k - 1). Var(b) = s^2 (X'X)^-1. Var(a) = s^2 / n + xbar' Var(b) xbar. R^2 = 1 - RSS / TSS. Adjusted R^2 = 1 - (1 - R^2)(n - 1) / (n - k - 1). DW = sum (e_t - e_t-1)^2 / RSS.

## Assumptions

- Annualizing is simple: intercept times periods_per_year, residual vol times the square root of periods_per_year.
- A t-stat doesn't change when the intercept is annualized, because the standard error scales with it.
- Standard errors are the classical ones: independent residuals with one variance.
- Thin means fewer than 10 observations per factor. The intercept isn't counted as a factor.
- Collinear means a factor's R squared on the factors before it is above 1 - 1e-10.
- A mean under 1e-12 of the largest return is rounding, so it reads as zero and has no shares.
- Both the series and the factors are taken as sent, with no excess-return adjustment.

## Not modeled

- Corrections for autocorrelated residuals or changing variance, such as Newey-West. Durbin-Watson only warns.
- Betas that change inside the sample. There is one fit for the whole window.
- Data mining: how many factor sets or samples were tried before this one.
- Survivorship, stale prices and smoothing in the series sent.
- Compounding, fees and taxes beyond what the series already holds.

## Call it

Every result has the same fields: summary, readings, tables, charts, flags, flips, formula, assumptions, not_modeled, hypothetical and inputs_hash. Read flags, flips and not_modeled with the summary, and pass them along together.

### HTTP

No key. The tools keep nothing you send. Send positions by POST, because a URL can land in a server log.

```bash
curl -s https://hedgefund.md/api/v1/factor_regress \
  -H 'content-type: application/json' \
  -d '{"returns_pct":[2.26,-2.73,-6.84,-0.96,-0.94,1.51,0.79,-3.29,0.74,4.22,1.14,0.79,1.64,3.65,1.9,-2.27,3.89,1.22,1.86,4.2,5.94,-1.19,4.04,1.5,-0.32,3.76,1.19,1.88,-1.62,8.44,-5.48,-1.43,2.55,-6.1,1.01,5.14,0,5.84,-6.17,5.03,-0.1,2.16,0.76,1.76,2.87,1.43,2.58,2.25,-0.89,-0.94,-3.84,-0.21,-0.16,4.31,0.29,-1.73,0.02,-3.21,1.87,-0.15],"factors":[{"name":"Market","returns_pct":[1.83,-3.89,-4.14,-0.61,-0.41,1.85,-2.03,-5.63,-0.58,4.06,0.23,2.9,3.52,6.22,-1.58,-0.51,4.9,0.08,2.8,4.4,7.34,-1.73,3.06,-0.23,-0.39,6.35,3.4,-0.4,-0.72,11.32,-6.12,-2.09,4.53,-5.66,1.26,4.4,-0.86,6.6,-7.2,4.85,3.05,1.23,1.69,0.47,3.1,1.98,2.83,2.7,-2.35,-1.17,-4.24,0.54,-1.72,5.07,-0.45,2.44,1.29,-5.27,-0.43,-5.14]},{"name":"Value","returns_pct":[2.52,2.35,-4.46,-1.08,-1.39,-0.28,0.35,-1.25,-1.49,0.47,1.79,-1.21,-1.27,-0.67,1.02,0.23,2.32,2.72,1.07,4.66,-0.99,-1.01,1.03,-1.42,0.14,-3.82,-0.46,1.41,-2.48,-0.59,-0.23,-0.31,-1.07,-1.71,0.06,-3.23,-1.87,-0.03,-1.9,1.04,-3.99,2.52,-1.33,-1.08,1.3,-2.5,2.11,0.82,-1.17,0.11,-0.8,2.91,0.71,0.01,-1.41,-3.47,-0.95,0.49,2.52,-0.37]},{"name":"Momentum","returns_pct":[2.48,-3.37,-2.54,0.88,2.98,-1.93,5.53,-2.9,0.08,2.35,-1.7,0.95,-2.15,-1.16,-4.11,-5.82,0.58,-0.26,2.2,-3.37,2.33,2.48,-3.26,3.74,3.2,1.06,1.92,0.13,1.29,7.79,1.65,4.04,-0.31,0.36,0.24,3.56,2.35,3.2,-0.13,-0.3,-2.95,2.75,5.03,3.83,-0.11,3.02,-1.87,5.2,-5.05,3.42,-0.02,1.59,0.09,-1.39,0.54,0.92,-1.64,-7.58,-0.13,5.98]}],"periods_per_year":12}' \
  | jq '{summary, flags, flips, not_modeled, inputs_hash}'
```

### MCP

The tool is `factor_regress` on the server at https://hedgefund.md/mcp.

```bash
curl -s https://hedgefund.md/mcp \
  -H 'content-type: application/json' -H 'accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"factor_regress","arguments":{"returns_pct":[2.26,-2.73,-6.84,-0.96,-0.94,1.51,0.79,-3.29,0.74,4.22,1.14,0.79,1.64,3.65,1.9,-2.27,3.89,1.22,1.86,4.2,5.94,-1.19,4.04,1.5,-0.32,3.76,1.19,1.88,-1.62,8.44,-5.48,-1.43,2.55,-6.1,1.01,5.14,0,5.84,-6.17,5.03,-0.1,2.16,0.76,1.76,2.87,1.43,2.58,2.25,-0.89,-0.94,-3.84,-0.21,-0.16,4.31,0.29,-1.73,0.02,-3.21,1.87,-0.15],"factors":[{"name":"Market","returns_pct":[1.83,-3.89,-4.14,-0.61,-0.41,1.85,-2.03,-5.63,-0.58,4.06,0.23,2.9,3.52,6.22,-1.58,-0.51,4.9,0.08,2.8,4.4,7.34,-1.73,3.06,-0.23,-0.39,6.35,3.4,-0.4,-0.72,11.32,-6.12,-2.09,4.53,-5.66,1.26,4.4,-0.86,6.6,-7.2,4.85,3.05,1.23,1.69,0.47,3.1,1.98,2.83,2.7,-2.35,-1.17,-4.24,0.54,-1.72,5.07,-0.45,2.44,1.29,-5.27,-0.43,-5.14]},{"name":"Value","returns_pct":[2.52,2.35,-4.46,-1.08,-1.39,-0.28,0.35,-1.25,-1.49,0.47,1.79,-1.21,-1.27,-0.67,1.02,0.23,2.32,2.72,1.07,4.66,-0.99,-1.01,1.03,-1.42,0.14,-3.82,-0.46,1.41,-2.48,-0.59,-0.23,-0.31,-1.07,-1.71,0.06,-3.23,-1.87,-0.03,-1.9,1.04,-3.99,2.52,-1.33,-1.08,1.3,-2.5,2.11,0.82,-1.17,0.11,-0.8,2.91,0.71,0.01,-1.41,-3.47,-0.95,0.49,2.52,-0.37]},{"name":"Momentum","returns_pct":[2.48,-3.37,-2.54,0.88,2.98,-1.93,5.53,-2.9,0.08,2.35,-1.7,0.95,-2.15,-1.16,-4.11,-5.82,0.58,-0.26,2.2,-3.37,2.33,2.48,-3.26,3.74,3.2,1.06,1.92,0.13,1.29,7.79,1.65,4.04,-0.31,0.36,0.24,3.56,2.35,3.2,-0.13,-0.3,-2.95,2.75,5.03,3.83,-0.11,3.02,-1.87,5.2,-5.05,3.42,-0.02,1.59,0.09,-1.39,0.54,0.92,-1.64,-7.58,-0.13,5.98]}],"periods_per_year":12}}}' \
  | jq '.result.structuredContent | {summary, flags, flips, not_modeled, inputs_hash}'
```

### The library

The same code on your own machine, with no dependencies, and nothing is sent anywhere.

```bash
curl -fsSL https://hedgefund.md/hedgefund.mjs -o hedgefund.mjs   # read it before you run it
node hedgefund.mjs template factor_regress > mine.json           # replace every number in it
node hedgefund.mjs run factor_regress mine.json | jq '{summary, flags, flips, not_modeled, inputs_hash}'
```

In a script:

```js
import { run, template } from "./hedgefund.mjs";

const inputs = template("factor_regress"); // replace every number in it
const { summary, flags, flips, not_modeled } = run("factor_regress", inputs);
```
