Strasmore Research
Learn Matt ConnorBy Matt Connor · data as of August 17, 2026 · refreshed weekly

Test Your Trade Log for Real Edge

Given N closed trades, is your average result better than luck? Test your trade log with a free open source CLI: t tests and bootstrap intervals, run offline.

To test your trade log for real edge, start with the question a broker statement never answers: given the trades you have already closed, could an average this good have come from luck alone? Your app totals the profit and loss. It does not tell you how often a log with no skill in it produces the same total. The gap between those two things is measurable, and you can measure it on your own file in about fifteen minutes with a free open source tool.

Is a profitable trade log evidence of skill?

Expectancy is the plain version of the question: the average money one trade adds or subtracts, fees included. A log with positive expectancy made money per trade over the stretch it covers. That is a description of what happened, not a measurement of skill, since the average sits inside a spread of single outcomes wide enough to produce a positive total by chance.

Your own log is private, so the panels here use public daily prices to show the shape of the problem. Every session of a widely held ETF is one draw from a distribution, in the same way every closed trade is one draw from yours.

QueryEvery SPY session since 2016, bucketed by size of move
The exact SQL behind every number
SELECT
    multiIf(
        bucket <= -3, '-3% or worse',
        bucket >= 2,  '+2% or better',
        concat(toString(bucket), '% to ', toString(bucket + 1), '%')
    )        AS return_bucket,
    count()  AS session_count
FROM
(
    SELECT toInt32(least(greatest(floor(ret_pct), -3), 2)) AS bucket
    FROM
    (
        SELECT
            100 * (toFloat64(close) / lagInFrame(toFloat64(close))
                OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - 1) AS ret_pct
        FROM global_markets.stocks_daily_aggs
        WHERE ticker = 'SPY'
          AND date >= '2016-01-04'
          AND date <  today() - 2
    )
    WHERE isFinite(ret_pct)
)
GROUP BY bucket
ORDER BY bucket
Run this yourself

Most of the weight sits in the two middle bands: 896 sessions in the -1% to 0% band and 1132 in the 0% to 1% band. The tails are thin, and they are where totals get decided: 92 sessions printed -3% or worse. A dozen draws from a shape like this can add up to almost anything, which is why a dozen trades tell you very little about the hand that placed them.

How many trades before the average means anything?

The number that answers this is the standard error: the margin of error around your own average. It falls with the square root of the count, so four times the trades cuts it in half. Watch what that does to a real series.

QueryThe average SPY session and its own margin of error, by sample size
The exact SQL behind every number
WITH rets AS
(
    SELECT
        row_number() OVER (ORDER BY date DESC) AS trades_back,
        ret_pct
    FROM
    (
        SELECT
            date,
            100 * (toFloat64(close) / lagInFrame(toFloat64(close))
                OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - 1) AS ret_pct
        FROM global_markets.stocks_daily_aggs
        WHERE ticker = 'SPY'
          AND date >= today() - 1400
          AND date <  today() - 2
    )
    WHERE isFinite(ret_pct)
)
SELECT
    concat(toString(n), ' trades')                                       AS sample_size,
    round(avg(ret_pct), 3)                                               AS avg_trade_pct,
    round(stddevSamp(ret_pct) / sqrt(count()), 3)                        AS std_error_pct,
    round(abs(avg(ret_pct)) / (stddevSamp(ret_pct) / sqrt(count())), 2)  AS t_stat_ratio
FROM rets
ARRAY JOIN [20, 40, 80, 160, 320, 640] AS n
WHERE trades_back <= n
GROUP BY n
ORDER BY n
Run this yourself

The average per session stays inside a narrow band across all six samples, while the error around it collapses: 0.188 points at 20 trades, 0.04 points at 640 trades. The last column divides the average by its own error, a t statistic in its simplest form. At 20 trades it reads 1.17, and at 640 trades it reads 1.96. Common practice starts treating a mean as distinguishable from zero somewhere above 2. A small sample can land above 2 on noise alone, which is why the interval around the average tells you more than the average.

A bootstrap answers the same question without assuming a bell curve: resample your own closed trades with replacement a few thousand times, then read the interval off the spread of the resampled averages. It widens when the count is small, exactly as it should. The method is worked through step by step in bootstrap confidence intervals for backtests.

Test your trade log for real edge with one command

The tool is anti-gambling-trader-tw, an MIT licensed Python project whose stated purpose is separating a repeatable edge from luck and survivorship bias. Its core analysis uses the Python standard library only, and it reads your file on your own machine.

Two facts shape the install. As of August 2026 the repository carries no tagged release and publishes no package on PyPI, so there is no version number to ask for. Pin the commit instead. A full commit SHA names one exact state of the code, and a reader who runs the same line next year gets the same code you ran.

git clone https://github.com/mars-tw/anti-gambling-trader-tw.git
cd anti-gambling-trader-tw
git checkout 9d938b64c80ee29363aed496ba4e61d9110a7222
python -m pip install -e .

That commit is the tip of the main branch as of 17 August 2026, and Python 3.10 or newer is required. Run everything below from the folder you cloned into. Start with the built in sample report, which needs no data of your own and no account anywhere:

python -m core.cli demo

Now your own file. Save these lines as my_trades.csv. This is a made up log, small on purpose. The reader accepts a direct profit and loss column, so an export with no entry and exit prices in it still works:

symbol,entry_time,exit_time,pnl,pnl_currency,tag
MSFT,2026-01-06,2026-01-08,142.50,USD,gap-fade
KO,2026-01-13,2026-01-21,-88.00,USD,gap-fade
SPY,2026-01-22,2026-01-23,61.25,USD,trend
HD,2026-02-03,2026-02-11,-215.40,USD,trend
JNJ,2026-02-17,2026-02-18,97.10,USD,gap-fade
MCD,2026-02-24,2026-03-04,-42.75,USD,trend
MSFT,2026-03-10,2026-03-12,318.60,USD,trend
SPY,2026-03-17,2026-03-18,-119.90,USD,gap-fade
KO,2026-04-01,2026-04-07,54.30,USD,gap-fade
HD,2026-04-14,2026-04-22,-76.85,USD,trend
JNJ,2026-05-05,2026-05-06,133.20,USD,gap-fade
MCD,2026-05-19,2026-05-27,-61.40,USD,trend

Then point the analyzer at it:

python -m core.cli analyze my_trades.csv

The report covers, among other things:

  • Expectancy, win rate, profit factor, Sharpe and Sortino ratios, and maximum drawdown across the whole log.
  • A significance test on per trade results, a t test alongside a bootstrap.
  • One fixed out of sample split, which needs at least 10 trades on each side of the cut.
  • A breakdown keyed on the tag column, plus flags for patterns the project files under gambling, including profit concentration and long losing streaks.

Twelve trades sits below the bar the project sets for itself. Its own getting started guide asks for 30 or more before treating a conclusion as reliable, and the holdout split cannot run at all on a file this size. The reason is visible by hand in the sample above: one winner of 318.60 carries the entire result, and the other eleven trades together add up to a loss. That is what profit concentration means in one line. Run python -m core.cli init-template if you would rather start from the tool's own columns, and note that the reader recognizes English or Chinese header names.

What a t-test assumes about your trades

A t test on per trade returns treats every trade as an independent draw. A trend following log breaks that assumption in a specific way: positions overlap in time, and two overlapping holding periods share most of the same price path. The panel measures how large that effect is on five session windows.

QueryFive-session returns: overlapping windows against separated windows
The exact SQL behind every number
WITH px AS
(
    SELECT
        ticker,
        date,
        toFloat64(close) AS close_px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('HD', 'JNJ', 'KO', 'MCD', 'MSFT', 'SPY')
      AND date >= '2019-01-02'
      AND date <  today() - 2
),
holds AS
(
    SELECT
        ticker,
        date,
        100 * (close_px / lagInFrame(close_px, 5) OVER w - 1) AS hold_pct
    FROM px
    WINDOW w AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
),
pairs AS
(
    SELECT
        ticker,
        hold_pct,
        row_number() OVER w2             AS seq_no,
        lagInFrame(hold_pct, 1) OVER w2  AS window_one_apart,
        lagInFrame(hold_pct, 5) OVER w2  AS window_five_apart
    FROM holds
    WHERE isFinite(hold_pct)
    WINDOW w2 AS (PARTITION BY ticker ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
)
SELECT
    ticker,
    round(corr(hold_pct, window_one_apart), 3)   AS overlapping_corr,
    round(corr(hold_pct, window_five_apart), 3)  AS separated_corr
FROM pairs
WHERE seq_no > 5
GROUP BY ticker
ORDER BY ticker
Run this yourself

Each name carries two readings. The first pairs every five session window with the window that started one session earlier, which shares four of its five sessions: HD reads 0.792. The second pairs windows five sessions apart, which share no sessions at all, and the same name reads -0.069. SPY shows the same split, 0.775 against -0.037. The only difference between the two readings is the shared price path.

What that means for your log is arithmetic. A log full of overlapping positions holds fewer independent observations than it has rows, and a t test that counts rows reports a smaller p value than the evidence supports. When your trades overlap in time, read the p value as the friendliest possible number rather than a verdict.

Why only an out-of-sample split catches curve fitting

Curve fitting is choosing the rule after seeing which version worked. Every parameter you tried and dropped is a coin flip you already ran, and the winner of many coin flips looks impressive on the data it won on. More significance testing on that same data cannot undo it, since the data is where the choice came from.

The remedy is a split: cut the record by date, decide on the older part, and score on the newer part you kept closed. The panel below runs one fixed rule with no tuning across six household names. Enter at the close of any session that finished lower than the session before it, exit at the next close. The cut falls at the start of 2021.

QueryOne fixed rule, six names: first half against the holdout half
The exact SQL behind every number
WITH px AS
(
    SELECT
        ticker,
        date,
        toFloat64(close) AS close_px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('HD', 'JNJ', 'KO', 'MCD', 'MSFT', 'SPY')
      AND date >= '2016-01-04'
      AND date <  today() - 2
),
seq AS
(
    SELECT
        ticker,
        date,
        close_px,
        lagInFrame(close_px)
            OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)  AS prev_close,
        leadInFrame(close_px)
            OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS next_close
    FROM px
),
sim_trades AS
(
    SELECT
        ticker,
        date,
        100 * (next_close / close_px - 1) AS trade_pct
    FROM seq
    WHERE prev_close > 0
      AND next_close > 0
      AND close_px < prev_close
)
SELECT
    ticker,
    round(avgIf(trade_pct, date <  '2021-01-01'), 3) AS in_sample_pct,
    round(avgIf(trade_pct, date >= '2021-01-01'), 3) AS holdout_pct,
    count()                                          AS trade_count
FROM sim_trades
GROUP BY ticker
HAVING countIf(date <  '2021-01-01') > 20
   AND countIf(date >= '2021-01-01') > 20
ORDER BY in_sample_pct DESC
Run this yourself

Sorted by the first half, the leader is MSFT, averaging 0.355% per trade over 1231 trades in the full window. On the holdout half the same rule on the same name averaged 0.095%. Reading down the first column and keeping the best name is a choice made with the answer already in view, which is the family of mistakes look-ahead bias in backtesting covers in full. A split you decide on before you look is the only part of this workflow that catches it.

What else the toolkit does, and what this post skips

The default broker in the project is the paper one, a simulated account with no credentials attached, and everything above runs on it. python -m core.cli brokers lists 14 options: that credential free default plus 13 real brokers and exchanges, several of them Taiwan specific, one of which fronts a hundred or more crypto venues. Wiring any of them up means live order routing with real money, which this post does not cover. If simulated fills are new to you, paper trading before real money sets out what a simulator reproduces and what it cannot.

One more piece is worth naming. The scan-text command reads pasted chat text, including LINE exports, and flags language patterns common to investment scams in Taiwan, the guaranteed profit promise and the VIP customer service account among them. The maintainer frames the whole project as anti-gambling and anti-fraud work. The project's own documentation states plainly that its statistics cannot prove future performance and cannot show that a strategy is safe or profitable. Take that as the honest limit of the tool rather than a claim about anyone's results, ours or the maintainer's. The same standard belongs on every track record put in front of you, which is the subject of verifiable trading track records.

FAQ

How many closed trades do I need before my results mean anything?

There is no clean threshold, since the answer depends on how spread out your individual results are. As a working floor, the project's own guide asks for 30 closed trades before treating a conclusion as reliable, and its holdout split needs at least 10 on each side of the cut. The margin of error around your average shrinks with the square root of the count, so going from 25 trades to 100 halves it.

Does a low p value mean my trading edge is real?

It means an average at least this large turns up rarely in a world where your edge is zero, under the assumptions the test makes. Those assumptions include independence between trades, which overlapping positions break. A low p value on a small log of overlapping trades is weaker evidence than the same number on a large log of separated ones.

Can I analyze my trades without connecting a broker account?

Yes. The analysis runs on a CSV file on your own computer, and the default broker in the project is a simulated paper account that needs no credentials. Nothing about the analysis step leaves your machine.

What is the difference between a t-test and a bootstrap on a trade log?

The t test compares your average against zero with a formula that assumes a particular shape for the spread of results. The bootstrap assumes no shape: it resamples your own closed trades thousands of times and reads the interval off what comes back. On a thin log, both answers should come out wide.

Why pin the install to a commit SHA instead of a version number?

The repository has no tagged release and no PyPI package as of August 2026, so no version number exists to pin. A full commit SHA names one exact state of the code, which keeps your run reproducible and lets you audit what changed when you move to a newer commit.


Every panel here ships with the SQL that produced it, open one and the counting is all in view. To run the same sample size math on a name you follow, ask the question in plain English on the Strasmore terminal.