Extending
This page is the interface reference — every method, every contract. If you are starting from "I have an idea", read your own strategies first; it covers installing a private package and the path from idea to evidence.
Three common extension points. Each touches one layer. Strategies and scanners can
live either in this repository or in a separate private Python package; the private
package route is the intended shape for proprietary signal IP, and the one strategy
and one scanner shipped here (demo_trend, demo_volume) are examples — they
demonstrate the interface and are not edges.
They also arrive the same way yours does. Both live in tradeflow/demo/ and are
declared as entry points by the engine's own pyproject.toml, so every install
exercises the discovery path this page describes rather than leaving it to CI. The
registry seeds them directly as well, because enumeration order across distributions
is undefined and a reserved name is worth nothing if which class answers to it is a
coin flip.
Add a strategy
-
Subclass
Strategyintradeflow/strategies/:class MyStrategy(Strategy):TIMEFRAME = "5Min"PARAM_RANGES = { # min/max/step/default/type per tunable param"lookback": {"type": "int", "min": 5, "max": 50, "step": 5, "default": 20},"risk_per_trade": {"type": "float", "min": 0.01, "max": 0.05, "step": 0.01, "default": 0.02},"stop_loss": {"type": "float", "min": 0.01, "max": 0.05, "step": 0.01, "default": 0.02},"take_profit": {"type": "float", "min": 0.02, "max": 0.10, "step": 0.02, "default": 0.04},}# Relationships between parameters go here, where a sampler can read them —# not in initialize(), where only a constructed instance can.PARAM_CONSTRAINTS = (("stop_loss", "<", "take_profit"),)def calculate_required_lookback(self): return self.config["lookback"] + 1def initialize(self): ...def process_data(self, df): ... # add indicator columnsdef calculate_scores(self, df): ... # -> {timestamp: signed score}You implement
calculate_scores(one signed conviction per bar) and nothing else for decisions: the base class derivesBUY/SELL/HOLDfrom the score, and the alpha layer scales the same score. SetLONG_ONLY = Falseto allow shorts, and overridesignal_thresholds()for asymmetric entry/exit bands. -
Expose it through the
tradeflow.strategiesentry-point group. For proprietary strategies that is a separate installed package:[project.entry-points."tradeflow.strategies"]private_trend = "yourfirm_signals.strategies:PrivateTrendStrategy"A public strategy shipped with the engine takes the same route — put it in
tradeflow/demo/strategies.py, declare it in this repository'spyproject.toml, and add it toBUILTIN_STRATEGIESintradeflow/services/registry.pyas well. The entry point is the path a user's install actually takes; the registry entry reserves the name and pins which class answers to it. Registry-only would ship a strategy that never exercises discovery, which is the mechanism the whole feature rests on.Once installed in the same environment, it works in
backtest,live,optimize, the MCP server, and the research agent — sizing, fills, execution, and metrics come for free because they only depend on the base interface. (create_with_defaults()is inherited fromStrategy; no need to write it.)
Use the pure indicators; don't reach for a compiled TA library.
Add a scanner
-
Subclass
ScannerStrategy— implementprocess_dataandgenerate_signals_df(emitSCANNER_BUY/SCANNER_SELL/SCANNER_HOLDplus asignal_strength).tradeflow/scanners/holds the base class and the driver; a scanner itself goes in your own package, or intradeflow/demo/scanners.pyif it ships here. -
Expose it through the
tradeflow.scannersentry-point group:[project.entry-points."tradeflow.scanners"]private_volume = "yourfirm_signals.scanners:PrivateVolumeScanner"As with strategies, a public scanner shipped here does the same and additionally goes in
BUILTIN_SCANNERSintradeflow/services/registry.py. Not theBUILTIN_SCANNERSliteral intradeflow/scanners/symbol_scanner.py, which is empty and reserves names for classes defined in that module — there are none — and notSymbolScanner.SCANNERS, which discovery overwrites.Keep it TA-Lib-free.
Private alpha packs
Keep the engine boring and open; keep the signal IP elsewhere. A private package
can depend on tradeflow-engine, define strategies/scanners in its own modules,
and expose them with entry points. TradeFlow loads entry points at startup, but
built-in names are reserved, so a private package cannot silently replace
demo_trend or demo_volume.
A private package can also return several contributions from one entry point:
[project.entry-points."tradeflow.strategies"]
private_pack = "yourfirm_signals.registry:strategies"
def strategies():
return {
"private_trend": PrivateTrendStrategy,
"private_reversal": PrivateReversalStrategy,
}
The MCP server includes draft validation tools for the workbench phase:
validate_draft_strategy_codechecks generated/private strategy source against the sandbox and base-class contract without registering or running it.validate_draft_scanner_codedoes the same for scanner source and verifies the scanner output schema.run_draft_walk_forwardvalidates strategy source in-memory, runs the normal walk-forward validator, and records the result underdraft:<ClassName>:<code_hash>when journaling is enabled.
That gives an agent a safe loop for proposing and modifying code without putting the proprietary implementation in this repository. Once a candidate survives validation, move it into the private package and expose it by entry point so future runs can refer to it by name and share the normal registry/memoization path.
Add a broker
- Implement
Broker(and optionallyMarketDataProvider) for the venue in a newtradeflow/brokers/<vendor>/package, mapping the SDK to the domain types. - Construct it in
main.build_data_and_broker().
Three parts of the contract are easy to get wrong, and all three are load-bearing:
- Map failures onto
BrokerError, don't returnNone. Callers act differently on a rate limit, revoked credentials, and a rejected order; collapsing them into one non-answer removes the only basis for choosing. list_positionsraises rather than returning[]when the account cannot be read. An empty list is the claim that the account is flat, and the strategy's position book is rebuilt from it.- Honor
client_order_id. A venue that has already accepted one must reject the duplicate asDuplicateOrderErrorrather than placing a second order — that rejection is the idempotency guarantee, and the only one that survives a restart.
Nothing in engine/, execution/, strategies/, scanners/, or the optimizer
changes — they only ever knew the interface. Prove it the same way the suite does:
run against your adapter, or against FakeBroker first. FakeBroker models all
three behaviors above, and FailingBroker fails any named method with a chosen
error, so an adapter can be exercised against the same expectations.
Add an optimization objective
Any key in the metrics dict (sharpe_ratio, total_return, calmar_ratio, ...)
is a valid --objective. To add a new one, compute it in
analytics.performance.compute_backtest_metrics and it's immediately selectable.