Metadata-Version: 2.4
Name: edgeorluck
Version: 2.5.0
Summary: Is a trading strategy's record a real edge, or luck? An honest, reproducible judge for traders and their AI assistants.
Author: edge or luck
License-Expression: MIT
Project-URL: Homepage, https://github.com/Sakke-code/edge-or-luck
Project-URL: Source, https://github.com/Sakke-code/edge-or-luck
Keywords: trading,backtest,statistics,e-values,multiple-testing,mcp,overfitting
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23
Requires-Dist: pandas>=1.5
Requires-Dist: mcp>=1.2
Provides-Extra: web
Requires-Dist: flask>=2.2; extra == "web"
Requires-Dist: waitress>=2.1; extra == "web"
Provides-Extra: mcp
Provides-Extra: host
Requires-Dist: flask>=2.2; extra == "host"
Requires-Dist: a2wsgi>=1.10; extra == "host"
Requires-Dist: mcp>=2.2; extra == "host"
Provides-Extra: xlsx
Requires-Dist: openpyxl>=3.0; extra == "xlsx"
Provides-Extra: all
Requires-Dist: flask>=2.2; extra == "all"
Requires-Dist: waitress>=2.1; extra == "all"
Requires-Dist: a2wsgi>=1.10; extra == "all"
Requires-Dist: openpyxl>=3.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: jsonschema>=4; extra == "dev"
Requires-Dist: flask>=2.2; extra == "dev"
Requires-Dist: waitress>=2.1; extra == "dev"
Requires-Dist: a2wsgi>=1.10; extra == "dev"
Requires-Dist: openpyxl>=3.0; extra == "dev"
Dynamic: license-file

# edge or luck
<!-- mcp-name: io.github.Sakke-code/edge-or-luck -->

**Is your trading strategy a real edge, or just luck?** Drop in your trades and get an honest answer in plain words: what the record shows, how sure that is, what would change it, and one next step. No trades yet? [Test the idea honestly](#test-an-idea-before-you-have-trades) first.

Most strategies that look good are luck: the best of many tries, a rising market, a backtest that peeked at the future, or a few big trades. This judge checks for each of those. The same file always gets the same answer, and a "real edge" verdict is wrong rarely. That rate is measured, not promised (see [VALIDATION.md](VALIDATION.md)).

Open source (MIT). It runs on your machine and keeps nothing. Not financial advice: a statistical check of past trades.

```text
$ edgeorluck "BTC breakout.csv" --tried 40
## Edge or luck: Can't tell yet
**Can't tell yet: +0.458% per trade so far, but luck often makes records like this when you tried 40 versions.**
...
**Next step:** Keep it on paper until about 630 more trades, then check again.
```

## Install

```bash
pip install edgeorluck            # the judge, the honest backtester and the MCP server for AI assistants
pip install "edgeorluck[web]"     # + the web page and API
pip install "edgeorluck[xlsx]"    # + Excel files
pip install "edgeorluck[host]"    # + the hosted site: web page, API and the assistant tools over HTTP
```

The latest code, straight from GitHub: `pip install "edgeorluck[all] @ git+https://github.com/Sakke-code/edge-or-luck"`.

## Use it

```python
import edgeorluck

r = edgeorluck.judge("trades.csv", variants_tried=40, benchmark_return_pct=34.5)
print(r["verdict"]["headline"])   # one sentence, safe to quote
print(r.to_markdown())            # the whole answer, ready to paste
```

What it reads (it never crashes on messy input; when it can't judge, it says exactly what it needs):

| You have | Give it |
|---|---|
| TradingView strategy tester | Strategy Tester, then **List of trades**, then export (CSV or XLSX), old or new column names |
| An exchange account | the spot **trade history (fills)**: time, pair, side, price, quantity, fee. Paired into round trips first-in-first-out, net of fees |
| A stock broker | **Trading 212** (History export), **Interactive Brokers** (activity statement or Flex trades), **Nordnet** (transactions, in Finnish, Swedish or English), **eToro** (closed positions), **Degiro** (Transactions.csv: paired first-in-first-out), **MetaTrader 5** (positions report) |
| [backtesting.py](https://kernc.github.io/backtesting.py/) | `stats = bt.run(); print(edgeorluck.judge(stats).to_markdown())` (also uses its Buy & Hold return) |
| [vectorbt](https://vectorbt.dev/) | `edgeorluck.judge(pf)` or `edgeorluck.judge(pf.trades.records_readable)`; each column is a version |
| [freqtrade](https://www.freqtrade.io/) | `edgeorluck user_data/backtest_results/backtest-result-....json` (or the `.zip`) |
| A bot's own log | any CSV with a P&L column (money or %), or `edgeorluck.judge([{"pnl": 12.5, "closed_at": "2025-01-03"}, ...])` |
| Just numbers | `edgeorluck.judge([12.5, -3, 4.1, ...])` or the text `"+12.5%, -3%, 4.1%"` |

Exchange fills: a fee in the base coin is valued at the fill price; a fee in a third coin (BNB, BGB) is reported, not guessed. Sells with no earlier buy, and positions still open at the end, are named in the warnings.

**About the export formats:** we built the readers from public descriptions of each export's columns (Binance spot trade history old and new, Bitget spot fills and order history, TradingView's list of trades, and the stock brokers above), not from real accounts. If yours fails, please [open an issue](https://github.com/Sakke-code/edge-or-luck/issues) with the header row and a few anonymised rows.

### Make the verdict more honest

| Parameter | Why it matters |
|---|---|
| `variants_tried=N` | If you tried 40 versions and kept the best, one of them looks good by luck alone. The bar rises to match. |
| `benchmark_return_pct=34.5` | A strategy that only buys looks good whenever prices rise. Did it beat simply holding? (Derived automatically from TradingView and backtesting.py results when possible.) |
| `fees_included=True/False` | A small edge before fees is often a loss after them. |
| `source="backtest" / "paper" / "live"` | Backtests are usually better than live trading. |
| `backtest=...` | Pass the backtest next to the live trades: is live keeping the backtest's promise? |

### The command line

```bash
edgeorluck trades.csv [--tried 50] [--benchmark 34.5] [--fees yes] [--source live] [--json | --md]
edgeorluck live.csv --backtest backtest.csv
edgeorluck --schema
edgeorluck --serve --port 5078        # the web page, served by waitress
edgeorluck --single-file              # write edgeorluck_single.py
```

## Test an idea before you have trades

Just an idea, no trades yet? A quick backtest script from an AI is usually too kind. It trades at prices it couldn't have had, forgets fees, or peeks at the future. Test the idea here instead: the same judge decides.

```python
import edgeorluck as eol

px = eol.prices("BTCUSDT", "1h", start="2024-01-01", end="2026-10-01")  # Binance spot, public data, no key
bt = eol.backtest(px, {"buy": "rsi(14) < 30 and close > sma(200)", "sell": "rsi(14) > 70"})
print(bt.to_markdown())
```

That classic rule made 31 trades and +1.5% in total. Simply holding made +96.6% over the same dates. Verdict: **No edge once the market is taken out.**

Stocks, ETFs, indices and currencies work the same way:

```python
eol.prices("SPY", "1d", start="2010-01-01")      # stocks, ETFs, indices as Yahoo Finance writes them: AAPL, NOKIA.HE, ^GSPC
eol.prices("EURUSD", "1d", start="2015-01-01")   # two currencies: the European Central Bank's daily reference rates
```

Stock prices are split- and dividend-adjusted, with 1h (about the last two years), 1d or 1w bars. Currencies have one rate a day, so their trades fill at the next day's rate.

What makes it honest:

- **No impossible fills.** Each decision is made at a bar's close and filled at the next bar's open. Every fill pays a fee and slippage (0.1% and 0.05% by default; `fee_pct=`, `slippage_pct=`).
- **No peeking.** A rule in the rule language (below) can only look back. A Python rule is checked instead: a function that takes the price table and returns 1, 0 or -1 per bar. If its decision at a bar changes when later bars are added, it is using the future, and the backtest stops instead of judging it.
- **One bar later.** Every backtest runs again with each fill one bar later. If the edge vanishes, the check fails: the edge depended on instant fills or a hidden look-ahead.
- **Against holding.** The trades are compared with simply holding over the same dates.

### Tune without fooling yourself

```python
tuned = eol.tune(px, {"buy": "rsi(14) < {low}", "sell": "rsi(14) > {high}"},
                 {"low": [20, 25, 30, 35], "high": [60, 65, 70, 75]})
print(tuned.to_markdown())
```

If you try 16 settings and keep the best, you find the luckiest one. `tune()` tries them all on the first 70% of the prices only (`holdout=0.3`). It then judges the chosen setting once on the last 30%, which played no part in the choice, and shows how the other settings did there.

On the prices above, the chosen setting averaged +2.9% per trade on the part that chose it. On the unseen part it averaged -3.4%: it ranked 15th of the 16, and only 2 of the 16 made money there.

Don't tune again after seeing the unseen verdict: that turns the unseen part into tuning data too.

### The rule language

A rule is a condition checked at each bar's close. It is built from:

- the prices `open`, `high`, `low`, `close` and `volume`;
- numbers, `+ - * /` and `< <= > >=`;
- `and`, `or` and `not`;
- the functions below.

A function given just a number works on the close, so `sma(20)` is `sma(close, 20)`.

| Function | What it gives |
|---|---|
| `sma(n)`, `ema(n)` | Moving averages. |
| `rsi(n)` | Wilder's RSI, 0 to 100. |
| `highest(n)`, `lowest(n)` | The highest or lowest of the last n bars, this one included. Example: `highest(high, 20)`. |
| `std(n)`, `atr(n)` | The spread: standard deviation, or average true range. |
| `change(n)`, `pct(n)`, `prev(x, n)` | The difference from n bars ago, the percent change, or the value n bars ago. |
| `crosses_above(a, b)`, `crosses_below(a, b)` | True when a crossed b on this bar. |
| `abs(x)`, `min(a, b)`, `max(a, b)` | Absolute value, smaller, larger. |

`ema`, `rsi`, `atr` and `std` match TradingView's `ta.ema`, `ta.rsi`, `ta.atr` and `ta.stdev` from the first bar. `highest(high, 20)` includes this bar, so a breakout above the earlier high is `close > prev(highest(high, 20))`.

**Getting out:** give at least one of `sell`, `stop_loss_pct`, `take_profit_pct` or `max_hold_bars`. Stops and targets are checked at each close (no fills inside a bar are assumed) and filled at the next open. `"side": "short"` sells first.

Nothing else is allowed: no attributes, indexing, imports or code. That makes a rule from a web page or an AI safe to run.

From the command line:

```bash
edgeorluck backtest --buy "rsi(14) < 30" --sell "rsi(14) > 70" --symbol ETHUSDT --interval 4h --start 2023-01-01
edgeorluck tune --buy "rsi(14) < {low}" --sell "rsi(14) > {high}" --grid "low=20,25,30 high=65,70,75"
edgeorluck backtest --buy "pct(24) < -8" --take 6 --hold 72 --prices my_prices.csv
```

**Kept simple on purpose:**

- One position at a time, all in.
- Spot prices: no funding, borrowing or leverage.
- Stops are checked at closes.
- Prices are fetched on your own machine from free public sources (Binance's market data, Yahoo Finance's chart data, which is unofficial, and the ECB's reference rates), and nothing is stored.

## What you get back

A versioned JSON result (`schema_version` 1.0; `edgeorluck.schema()` or `edgeorluck --schema` for the JSON Schema) built so an assistant can answer a non-coder in three sentences without computing anything else:

- `verdict`: `code` (`real_edge`, `cant_tell`, `no_edge`, `losing`, `faded`, `too_early`), `headline` (one sentence, safe to quote), `plain` (2-4 short sentences) and `why`.
- `next_step`: one concrete step about testing, never buy, sell or size advice, with its `why`.
- `ask_the_user`: the questions whose answers would change the verdict, each with `why` and the parameter to set (`answer_with`).
- `what_would_change_it`: for example, "About 140 more trades at this average would settle it."
- `checks`: pass / warn / fail, each with a fix and a plain `why`. Money comes first: when the average percent is positive but the money is negative, it says so.
- "Is it broken, or just a bad streak?": the odds of the latest stretch if nothing changed, e.g. "a drop that deep within 30 trades happens about 1 in 12 times for this strategy".
- Red flags for records that look too good (a very high win rate, no losing streaks, a huge profit factor, almost no drawdown, many small wins with rare huge losses) and an `ai_audit_prompt` to paste to the AI that wrote the bot.
- `numbers`, `compare` (live vs backtest), `input_fingerprint`, `how_we_know` (the method, with references), `limitations`.

## Use it from your AI assistant

The MCP server runs on your machine, so the assistant can pass the path of your export. Its tools:

- `judge_trades` takes `file_path`, `csv_text` or `pnl`, plus `variants_tried`, `benchmark_return_pct`, `fees_included`, `source`, `unit` and the `backtest_...` fields. It answers in Markdown (`detail="full"` adds the JSON).
- `backtest_rule` and `tune_rule` test an idea honestly, on fetched prices or a local price file. `freeze=true` keeps a rule, and `forward_test` later judges only the trades it made after that: the honest way to a verdict after many tries.
- `rule_language` lists the rule language.
- The server counts every record and rule it judges in a session as a version tried, so the bar rises with them even when nobody says.

**Claude Code plugin** (the MCP server plus a skill that tells Claude when to reach for it and how to relay a verdict honestly):

```text
/plugin install edge-or-luck --marketplace Sakke-code/edge-or-luck
```

**Any agent that reads Agent Skills** (Claude, Codex and others): copy [`plugin/skills/edge-or-luck`](plugin/skills/edge-or-luck/SKILL.md) into its skills folder.

The MCP server alone uses [uv](https://docs.astral.sh/uv/)'s `uvx`, which fetches the package from PyPI on first use:

Claude Code:

```bash
claude mcp add edgeorluck -- uvx edgeorluck mcp
```

Codex (`~/.codex/config.toml`):

```toml
[mcp_servers.edgeorluck]
command = "uvx"
args = ["edgeorluck", "mcp"]
```

Cursor (`.cursor/mcp.json`) and Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "edgeorluck": {
      "command": "uvx",
      "args": ["edgeorluck", "mcp"]
    }
  }
}
```

**Without uv** (after `pip install edgeorluck` in a virtual environment), point the same entries at that environment's Python:

```bash
claude mcp add edgeorluck -- /path/to/edge-or-luck/.venv/bin/python -m edgeorluck.mcp_server
```

```toml
[mcp_servers.edgeorluck]
command = "/path/to/edge-or-luck/.venv/bin/python"
args = ["-m", "edgeorluck.mcp_server"]
```

On Windows the Python is `C:\\path\\to\\edge-or-luck\\.venv\\Scripts\\python.exe` (double the backslashes in JSON and TOML).

**ChatGPT and other chats without installs:** download `edgeorluck_single.py` (from the web page, or `edgeorluck --single-file`), upload it together with your trades, and paste:

> I uploaded edgeorluck_single.py and my trades. Run `python edgeorluck_single.py <my file>` (add `--tried N` if I tell you how many versions I tried). Then answer me from its output only: quote the verdict's headline word for word, explain it in short simple sentences, tell me the next step, and ask me its questions. Don't add your own statistics. It is not financial advice.

**One prompt for any AI** (for a trader who can't code):

> Before you tell me my strategy is good, check it with edge or luck (https://github.com/Sakke-code/edge-or-luck): run `edgeorluck.judge()` on my trade export, tell me how many versions I tried (ask me if you don't know), quote its headline word for word, and give me its next step. Don't round "can't tell" up to "good".

[llms.txt](llms.txt) describes all of this for assistants in one page.

## Freeze a rule, then judge only what comes after

A backtest, however honest, has seen its data. After many tries the honest test is the one that hasn't: freeze the rule, and judge only the trades it makes from then on. Nobody can tune a rule on trades that hadn't happened yet. `backtest_rule(freeze=true)` freezes it and `forward_test` judges it later; in Python, `backtest(prices, rule, since=frozen_at)`. On your machine the frozen rules are kept in `~/.edgeorluck/frozen.json`. On a hosted server nothing is kept: the freeze returns a receipt the server signs (the rule, its market, the time), and `forward_test(receipt)` checks the signature first, so a changed rule or an earlier date fails. Nothing is public.

## Host it

One app serves the page, the guides, the API and the assistant tools, for chat assistants that connect to a URL (ChatGPT, Claude) at `/mcp`, and as plain JSON at `/api/tools/...` for GPT actions (described at `/openapi.json`):

```bash
docker build -t edgeorluck . && docker run -p 8080:8080 -v eol-data:/data edgeorluck
# or: pip install "edgeorluck[host]" && uvicorn --factory edgeorluck.web.remote:make_app --port 8080
```

Keep `/data` (`EDGEORLUCK_DATA_DIR`) on a volume: the forecast ledger lives there. `EDGEORLUCK_PROXIES` is the number of proxies in front of the app (the Dockerfile sets 1): the rate limits then key on the address the nearest proxy added to `X-Forwarded-For`, never on the part a visitor can write. Set `EDGEORLUCK_SECRET` (any long random text) so receipts outlive a restart, and `EDGEORLUCK_HOSTS` (your host names, comma-separated) to turn on the MCP server's host check. `EDGEORLUCK_BMC_URL` and `EDGEORLUCK_PATREON_URL` put the Buy Me a Coffee and Patreon buttons on `/support`; `python -m edgeorluck.web.support link <tier>` makes the thank-you link that unlocks a tier's extras (Flip's wardrobe and the Flippy games at `/games`) for the platforms' thank-you messages. Over the web the tools never read files on anyone's computer, and since the protocol keeps no session, each answer names a study id for the assistant to pass back, so every version tried in a conversation still counts.

## How it judges (the short version)

- **Luck is the default.** A record has to beat what luck does with the same trades. The judge flips each trade's sign at random 1,000 times ("coin-flip copies") to show what luck looks like.
- **You can check any time.** The 95% range is anytime-valid: from 30 trades on (no range is shown before) it stays honest however often you look. A normal confidence interval checked every day is wrong far more often than it says.
- **Every version you tried counts.** All strategies shown and versions tried form one family, and the bar rises with its size.
- **The market is taken out**, roughly, when the input shows prices or a buy-and-hold return.
- **Skewed records need more.** Many small wins with rare big losses must clear a higher bar, and "real edge" needs at least five losing trades.

The method, with references, is in `how_we_know` and [VALIDATION.md](VALIDATION.md): Waudby-Smith, Arbour, Sinha, Kennedy & Ramdas (2021) and Howard, Ramdas, McAuliffe & Sekhon (2021) for anytime-valid ranges; Wang & Ramdas (2022) for e-BH; Newey & West (1987) for autocorrelation; Hall (1987) for the skew correction.

## How often it's wrong

Measured on simulated records whose truth is known ([VALIDATION.md](VALIDATION.md), regenerated by `python tools/validate.py`):

- A strategy with **no edge** is called a "real edge" at most 0.35% of the time, across normal, heavy-tailed, autocorrelated, skewed and martingale-shaped trades at 30 to 1,000 trades (target: 2.5%).
- **Checked every 10 trades** from 30 to 300, a no-edge record is ever called real 0.4% of the time (0.8% when neighbouring trades move together).
- **A steady edge is called "faded"** 0.6% of the time; live trades as good as the backtest are called worse 0.4% of the time.
- **The best of 100 versions** of a no-edge strategy passes 7.6% of the time when the judge isn't told, and 0% with `variants_tried=100`.
- **The price of that honesty:** a small edge (0.2 standard deviations per trade) is found 58% of the time at 300 trades and in all 1,000 tries at 1,000. Below that, the judge says "can't tell yet" and how many more trades would settle it.
- **Same file, same answer:** identical results in a fresh process, every time.

## What it can't tell you

- What happens next. It judges past trades; markets change.
- Whether the backtest itself is honest. It can see signs of a bug (records that look too good), not the bug.
- Anything exact about small samples: the maths is a normal approximation that gets better as trades accumulate.
- Trades are treated as independent draws. Trades that move together (open at the same time, the same market move) count more than they should. It warns when trades overlap.
- The buy-and-hold comparison is rough: it assumes the price moved at a steady pace.

## Popular strategies, judged

Ten rules you will hear again and again (the golden cross, the 200-day average, RSI 30/70, RSI(2) dips, Bollinger bounces, MACD, EMA 9/21, breakouts, buying dips), tested honestly on the S&P 500, the Nasdaq 100, gold, EUR/USD and bitcoin, with costs on every fill. Each strategy on each market is one of 50 tries, and every verdict counts all of them. Not one shows a real edge, and most made less than simply holding: [the results](docs/strategies.md). The site serves them at `/strategies` (and `.md` for assistants); `python tools/write_strategies.py` recomputes them. Past prices only: nothing there says what to trade now.

## Mini guides

Short reads, a few minutes each, for traders and for the AI explaining things to them: [five ways a backtest lies](docs/guides/five-ways-a-backtest-lies.md), [tune settings without finding luck](docs/guides/tune-without-finding-luck.md), [write your idea as a rule](docs/guides/write-your-idea-as-a-rule.md), [is live keeping the backtest's promise?](docs/guides/live-vs-backtest.md), [why the judge is so strict](docs/guides/why-the-judge-is-strict.md), [count your versions](docs/guides/count-your-versions.md), [did it just ride the market?](docs/guides/did-it-just-ride-the-market.md) and [broken, or just a bad streak?](docs/guides/broken-or-bad-streak.md).

The web page serves them at `/guides`. Their measured numbers are filled in from the validation run, and the tests check that every rule they show compiles and fires.

## The web page

`edgeorluck --serve` runs the page, the guides, Flip's 45-second animated story (`/story`: what luck looks like, how the bar rises with versions tried, and a record that finally clears it, drawn with the judge's own formula), Flip Flip (`/play`: call coin flips and watch luck even out inside the same kind of funnel the judge uses) and a small JSON API with waitress: `POST /api/judge` (multipart `files`, optional `backtest`, fields `variants_tried`, `benchmark_return_pct`, `fees_included`, `source`, `unit`; or JSON `{sample}` / `{csv_text | pnl | rows, ...}`), `GET /api/schema`, `/api/samples`, `/api/engine`, `/api/track-record`, `/api/validation`, `/llms.txt`, `/healthz`, `/edgeorluck_single.py`. Uploads are read in memory and never stored. The page's own forecasts ("will the next 30 trades end positive?") are kept as one-way fingerprints in a hash-chained ledger (`EDGEORLUCK_DATA_DIR`) and graded when a trader comes back. The library itself writes nothing.

## Development

```bash
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest
python tools/validate.py          # VALIDATION.md and src/edgeorluck/data/validation.json
python tools/write_examples.py    # docs/examples/*.json
```

License: MIT.
