I kept running into the same problem with trading ideas. An indicator looks perfect on a chart, you convince yourself it has an edge, and then you find out the “edge” was just the market going up the whole time. I wanted a way to answer one question honestly: does this signal actually beat buying and holding, or does it only look good in hindsight? WeissWave is what I built to answer it.

This did not start from an indicator I downloaded. I spent a long time reading the original Richard D. Wyckoff, learning how he weighed volume against the price move it actually produces, and only once I understood the core concepts did I build my own studies in TradingView around the parts that held up. Those custom Pine Script studies, a WaveTrend and volume engine and a Weis Wave reader, are the foundation, and the signal suite I rely on most came out of my own testing rather than anyone else’s playbook.

Porting all of it to Python, with the Pine Script as my reference, was the next step. But the port was the mechanical part. The real work was figuring out what was worth encoding in the first place, and then building the layer around it that turns those signals into strategies you can test the way a skeptic would.

WeissWave chart explorer: candlesticks with buy and sell markers, WaveTrend oscillator, and Weis Wave volume
The chart explorer, running locally. Candles carry the buy and sell markers, with the WaveTrend oscillator and the Weis Wave volume panel stacked below. Every control in the sidebar applies across all tabs.

What it does

The whole thing is a pipeline, and each stage exists to keep the next one honest.

First it turns every idea from the original scripts into a plain true/false column, aligned to each bar and only true when you could have actually known it at that bar’s close. That last part matters more than it sounds. A divergence signal, for example, only fires on the confirmation bar two bars after the pivot, not on the pivot itself the way a chart draws it after the fact.

Then an event study measures each signal on its own. For every signal it looks at the forward return over the next 1, 3, 5, 10, and 20 bars across the whole S&P 500 and compares it to the return of all bars generally. A signal only earns a spot in a strategy if it shows a real edge by itself.

From there a two stage strategy finder does the heavy lifting. Stage one scores every reasonable combination of entries, regime filters, and strictness on a training slice of history. Stage two takes only the survivors and fully simulates them across a grid of exits, stops, and hold times, on the training half and the test half separately. The training columns tell you what looked good. The test columns are the honesty check. Anything that falls apart between the two was luck.

The honesty check

This is the part I care about most. Every backtest and every finder result is measured against an equal weight buy-and-hold version of the same universe over the same dates. For each trade it computes the excess, which is just the trade return minus what the market did over the exact same holding window.

The finder ranks strategies by their average training excess, so a strategy can only rank highly if it actually beat holding the market, not just made money while the market made more. The backtest view shows the market’s return for the window, the average excess per trade, and the beat-the-market rate, and it warns you outright when buy-and-hold would have won. A strategy that makes 12 percent while the index made 15 is a losing strategy, and the tool says so instead of hiding it.

WeissWave strategy finder train and test results table
The strategy finder ranks configurations by training performance, with the test columns beside them as the reality check. Any config can be drilled into per symbol and exported as a bot-ready JSON strategy.

No peeking at the future

There is a subtler way a backtest can lie to you, and it fooled me for a while. A lot of tools let an indicator use information from bars that had not happened yet at the moment of the trade. The clearest example is a repainting signal. On a live chart the arrow looks like it showed up right at the low, but the indicator actually needed the next couple of bars to confirm that low before it could draw the arrow at all. If your backtest enters on that arrow, you are trading on something you could not have seen in real time, and the results look incredible for a reason that never survives contact with a real account.

WeissWave treats that as the cardinal sin. Every signal is built to be true only when it was knowable at that bar’s close. A divergence fires on the confirmation bar two bars after the pivot, not back-dated onto the pivot the way the chart paints it after the fact. Entries fill at the next bar’s open, never at the close of the bar that produced the signal. If the information was not available yet, the strategy does not get to use it.

Proving the fills are real

Most backtests fake the intrabar path. They assume your stop or your exit filled at some price without any evidence it ever traded there. I did not want to trust results built on that guess, so the backtest replays every trade against real 15 minute, 5 minute, and 1 minute bars stored in the database. Entry fills, exit fills, and stop touches all get re-derived from those fine bars and compared against the coarse daily model.

The result is a per-trade discrepancy table, an overall bias number, and a chart that shows the fine bars around any single trade with the modeled fill and the verified fill marked side by side. When the fine data does not go back far enough, and Yahoo only serves about 55 days of 15 minute bars and 7 days of 1 minute, the trade is reported as unverifiable rather than quietly guessed at.

How it is built

The engine is a small Python package with one job per file: the indicator math, the Weis Wave state machine, the divergence detection, the signal table builder, the strategy search, the backtester, the fill verifier, and a data layer. It leans on nothing but pandas and numpy for the math, partly on purpose, since the popular technical-analysis library is broken on current numpy and I did not want a dependency I could not trust.

Storage is DuckDB, and the design goal was that a fetch can never corrupt or duplicate what is already there. One table, a primary key on symbol plus interval plus timestamp, and every write is an insert-or-replace. Overlapping fetches overwrite instead of duplicating, and a failed run never destroys existing data because nothing is ever cleared.

def upsert_prices(con, df):
    """Insert-or-replace price rows keyed on (symbol, interval, ts)."""
    df = df[PRICE_COLS].copy()
    df["fetched_at"] = datetime.now(timezone.utc).replace(tzinfo=None)
    con.register("incoming", df)
    con.execute(f"""
        INSERT OR REPLACE INTO prices ({", ".join(PRICE_COLS)}, fetched_at)
        SELECT {", ".join(PRICE_COLS)}, fetched_at FROM incoming
    """)
    con.unregister("incoming")

That upsert is why the newest daily bar behaves. During market hours the data feed hands you the live session as a partial bar, and after the close the same day comes back complete. Both normalize to the same key, so the finished bar simply replaces the partial one. Every incremental run also refetches a few days of overlap because the newest bars are the least trustworthy, and the upsert corrects them in place.

Strategies themselves are just boolean expressions over the signal columns, which keeps them readable and easy to combine:

wt_exit_oversold & recent(volume_cross_up, 3)

The data source is abstracted behind one class. Yahoo is the default for daily history, and an Alpaca provider pulls SIP intraday data with split and dividend adjustment when I want depth. Its credentials come from HashiCorp Vault at runtime and never touch the disk, which ties this project into the same secrets setup I run across my home lab. A separate signal cache computes full-history signals once per database update and reuses them, so a backtest that used to take 90 seconds now runs in 3 to 5.

The direction this is heading matches how I actually think about a trade. You use the higher timeframe to read the trend, then you dial down to a lower timeframe to time the entry. The daily chart decides whether you should be looking to buy at all, and an hourly or faster chart decides exactly when. A cross-timeframe gate makes the simulator work that way on purpose: it only takes a lower-timeframe entry while the higher-timeframe trend is in force, and it shifts that trend by one bar so the gate obeys the same no-lookahead rule as everything else. On top of it a portfolio simulator models finite capital, a position cap, and a compounding equity curve against the benchmark, which is the bridge between a signal having an edge and an account actually growing.

Keeping the edge private

The public repository runs on the open signal set: the standard textbook signals and the WaveTrend and Weis Wave ports. The experience-driven signal suite that I actually rely on lives in one file that is excluded from the repo. The package detects its absence and runs cleanly without it. The framework is worth sharing. The specific edge is not, and separating the two was a design decision from the start.

Where it stands

WeissWave is the tool I use to decide whether a trading idea is worth trusting with real money. It is deliberately built to talk me out of bad strategies, which is the opposite of what most backtesting software does. The framework is on GitHub. The proprietary strategy logic is not, and it stays that way.

Code: github.com/grafman56/WeissWave

More detail: Building a Stock Screener I Can Actually Trust and From Pine Script to a Python Trading Platform.