Strategy¶
The base class every trading strategy subclasses. Declare data and indicators
in init() and react to each new data point in on_data().
Strategy ¶
Base class for strategies. Compatible with BacktestEngine, LiveEngine and PoolBacktestEngine.
BUILT-IN LOGGER
────────────────
Access the logger through self.log. It is created lazily the first
time it is accessed — no cost if never called.
Customization via class attributes in the subclass:
class MyStrategy(Strategy):
log_color = "#FF6B35" # color of the {name} field
log_file = "logs/bt.log" # None → console only
log_level = SIGNAL # minimum level
log_enabled_in_pool = True # enable logs in pool
Or at runtime, before self.log is first accessed (typically in the subclass init):
def __init__(self):
super().__init__()
self.log_color = "#FF6B35"
Behavior by execution mode ────────────────────────────── ┌──────────────────┬──────────────────────────────────────────────────────┐ │ run_mode │ self.log returns … │ ├──────────────────┼──────────────────────────────────────────────────────┤ │ "backtest" │ Real logger (console; file only if save_log=True) │ │ "live" │ Real logger (console; file if save_log, def True) │ │ "optimize" │ NullLogger — always silent, no exception │ │ "pool" │ NullLogger by default │ │ │ Real logger if log_enabled_in_pool = True │ └──────────────────┴──────────────────────────────────────────────────────┘
Log file writing (opt-in)
──────────────────────────────
Defining log_file is NOT enough to write to disk: the file is
opt-in and controlled by the save_log parameter of engine.run(...):
· BacktestEngine.run / ReplayEngine.run → save_log=False by default.
· LiveEngine.run → save_log=True by default.
· In all cases, save_log=True/False forces the behavior.
If save_log is False, self.log only emits to console (when a real
logger exists). This prevents accumulating log files in repeated
backtests/replays.
LOGGER METHODS ────────────────── Standard : self.log.debug / .info / .warning / .error / .critical Custom : self.log.perf(msg) — tick-to-tick metrics (level 5) self.log.signal(msg) — detected signals (level 15) self.log.trading(msg) — orders / fills (level 25)
Minimal example
class MyStrategy(Strategy): def init(self): self.btc = self.subscribe_ohlc("BTCUSDT", timeframe='1m') self.sma = self.add_indicator(self.btc.close_ref, SMA(10))
def on_data(self):
if self.sma[-1] > self.btc.close[-2]:
self.log.signal("bullish cross → %.2f", self.btc.close[-1])
Source code in src/tradetropy/models/strategy.py
log
property
¶
Ready-to-use logger. Lazy: created the first time it is accessed.
Returns NullLogger (zero I/O) in "optimize" mode and in "pool" mode
unless log_enabled_in_pool = True is defined on the class.
In any other mode returns a real Logger with the PERF / SIGNAL /
TRADING levels registered.
The logger is automatically invalidated when the engine changes the run_mode (through the setter), so it is always consistent with the current execution mode.
is_backtest
property
¶
True if running in backtest (BacktestEngine / PoolBacktestEngine).
sesh
property
¶
Session injected by the engine (SeshSimulator in backtest, SeshMT5/SeshLive in live trading). None if no broker is configured.
run_if ¶
Executes the callable for the current environment and returns its result. The callable for the other environment is NEVER executed.
Source code in src/tradetropy/models/strategy.py
fetch_klines ¶
Fetch recent OHLC candles on demand from the venue (live only).
Delegates to the live session's fetch_klines. Raises ConfigError in any non-live run_mode (anti-lookahead).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symbol
|
str
|
Trading symbol. |
required |
timeframe
|
int | str
|
Candle interval ('1m', '1h', ... or ms). Standard recommended set: '1m', '15m', '1h', '4h', '1d', '1w', '1mo' ('min'/'wk' accepted as aliases for 'm'/'w'). |
required |
limit
|
int
|
Maximum number of candles. Default 200. |
200
|
Returns:
| Name | Type | Description |
|---|---|---|
KlineData |
The fetched candles. |
Example
klines = self.fetch_klines('BTC/USDT', '1h', limit=500)
Source code in src/tradetropy/models/strategy.py
fetch_ticks ¶
Fetch recent public trades (ticks) on demand from the venue (live only).
Delegates to the live session's fetch_ticks. Raises ConfigError in any non-live run_mode (anti-lookahead).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symbol
|
str
|
Trading symbol. |
required |
limit
|
int
|
Maximum number of trades. Default 500. |
500
|
Returns:
| Name | Type | Description |
|---|---|---|
TickData |
The fetched ticks. |
Example
ticks = self.fetch_ticks('BTC/USDT', limit=1000)
Source code in src/tradetropy/models/strategy.py
fetch_orderbook ¶
Fetch a single L2 order-book image on demand from the venue (live only).
Delegates to the live session's fetch_orderbook, returning a BookData that round-trips through tradetropy.io (save/read_book). Raises ConfigError in any non-live run_mode (anti-lookahead).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symbol
|
str
|
Trading symbol. |
required |
depth
|
int
|
Number of book levels K per side. Default 20. |
20
|
tick_size
|
float
|
Minimum price step propagated to BookData. |
0.01
|
Returns:
| Name | Type | Description |
|---|---|---|
BookData |
One book event (a REST snapshot is a single row). |
Example
book = self.fetch_orderbook('BTC/USDT', depth=20)
Source code in src/tradetropy/models/strategy.py
subscribe_orderbook ¶
subscribe_orderbook(symbol: str, depth: int = 20, window_size: int = 5000, record: 'str | Path | None' = None, record_flush_every: int = 500) -> OrderbookProxy
Subscribe to the real-time L2 order book (depth) of a symbol.
Returns an OrderbookProxy exposing top-of-book metrics (imbalance, spread, mid, best_bid/ask) and a causal book_as_of() for order-flow detectors. The engine feeds it from the streaming feed's order-book events. Available on streaming-capable live sessions and in replay of a recorded book; in plain backtest the book stays empty (stale).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symbol
|
str
|
Trading symbol. |
required |
depth
|
int
|
Number of book levels K retained per side. |
20
|
window_size
|
int
|
Max number of book events kept for book_as_of. |
5000
|
record
|
str | Path | None
|
If set, record every book event to this HDF5 file for later replay (read_book / ReplayFeed.from_records). |
None
|
record_flush_every
|
int
|
Flush the record buffer every N events. |
500
|
Returns:
| Name | Type | Description |
|---|---|---|
OrderbookProxy |
OrderbookProxy
|
Proxy to read the live book in on_data(). |
Source code in src/tradetropy/models/strategy.py
subscribe_mbo ¶
subscribe_mbo(symbol: str, window_size: int = 50000, record: 'str | Path | None' = None, record_flush_every: int = 1000) -> MboProxy
Subscribe to the L3 / market-by-order (per-order) stream of a symbol.
Returns an MboProxy exposing the recent per-order event window and reconstructed resting size per level - the data the L3 order-flow detectors (iceberg reloads, liquidity grabs) need. Available where the venue/feed delivers MBO and in replay of a recorded MBO log; empty otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symbol
|
str
|
Trading symbol. |
required |
window_size
|
int
|
Max number of MBO events retained. |
50000
|
record
|
str | Path | None
|
If set, record every MBO event to this HDF5 file for later replay (read_mbo). |
None
|
record_flush_every
|
int
|
Flush the record buffer every N events. |
1000
|
Returns:
| Name | Type | Description |
|---|---|---|
MboProxy |
MboProxy
|
Proxy to read the L3 stream in on_data() / DeepTrades. |
Source code in src/tradetropy/models/strategy.py
subscribe_ohlc ¶
subscribe_ohlc(symbol: str, timeframe: 'str | int | None' = None, window_size: int = 300, record: 'str | Path | None' = None, record_flush_every: int = 100) -> OhlcProxy
Subscribe to OHLC candles for a symbol.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symbol
|
str
|
Trading symbol (e.g. 'BTCUSDT'). |
required |
timeframe
|
str | int
|
Candle duration. Accepts a timeframe string (e.g. '1m', '5m', '1h', '1d') or an integer number of milliseconds, parsed via parse_timeframe(). Standard recommended set: '1m', '15m', '1h', '4h', '1d', '1w', '1mo' ('min'/'wk' accepted as aliases for 'm'/'w'; 'mo' is a fixed 30-day month). |
None
|
window_size
|
int
|
Maximum number of closed bars kept in memory (default 300). |
300
|
record
|
str | Path | None
|
Path to record the stream to HDF5. |
None
|
record_flush_every
|
int
|
How often to flush the recording buffer. |
100
|
Returns:
| Name | Type | Description |
|---|---|---|
OhlcProxy |
OhlcProxy
|
Proxy giving access to the OHLC window. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If timeframe is not provided or is invalid. |
Example
self.btc = self.subscribe_ohlc('BTCUSDT', timeframe='1m') self.btc = self.subscribe_ohlc('BTCUSDT', 60_000)
Source code in src/tradetropy/models/strategy.py
subscribe_footprint ¶
subscribe_footprint(symbol: str, timeframe: 'str | int | None' = None, window_size: int = 50, *, tick_size: float | None = None, levels: int = 4, value_area_pct: float = 0.7, aggressor_col: str | None = 'flags', vol_col: str = 'volume') -> FpProxy
Subscribe to the footprint (volume profile) of a symbol at the given interval.
Does not require calling subscribe_ohlc() first — the engine builds the tick→bar mapping internally if no compatible OhlcProxy exists. If one does exist, it reuses the already-computed mapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symbol
|
str
|
Trading symbol (e.g. "BTCUSDT"). |
required |
timeframe
|
str | int
|
Candle duration. Accepts a timeframe string (e.g. '1m', '5m', '1h', '1d') or an integer number of milliseconds, parsed via parse_timeframe(). Standard recommended set: '1m', '15m', '1h', '4h', '1d', '1w', '1mo' ('min'/'wk' accepted as aliases for 'm'/'w'; 'mo' is a fixed 30-day month). |
None
|
window_size
|
int
|
Maximum number of closed bars kept in memory. |
50
|
Configuration parameters (keyword-only): tick_size (float | None): Price level grouping size (e.g. 10 for BTC). If None (default), automatically inferred as average_bar_range / levels, rounded to a nice value. levels (int): Desired levels per bar for auto-inference (default 20). Only used when tick_size=None. value_area_pct (float): Percentage of volume defining the value area (default 0.70). aggressor_col (str | None): Tick column for aggressor classification (default "flags"). vol_col (str): Volume column to use (default "volume").
Raises:
| Type | Description |
|---|---|
ConfigError
|
If timeframe is not provided or is invalid. |
Example
self.fp = self.subscribe_footprint('BTCUSDT', timeframe='5m') self.fp = self.subscribe_footprint('BTCUSDT', 300_000)
Source code in src/tradetropy/models/strategy.py
add_indicator ¶
add_indicator(source: 'ColumnRef | list[ColumnRef] | OhlcProxy | TickProxy', indicator: Indicator, **plot_overrides) -> 'IndicatorProxy | MultiBandProxy'
Declares an indicator and returns the proxy to access its values.
source can be:
- ColumnRef — a single column (e.g. proxy.close_ref).
- list[ColumnRef] — multiple columns from the same proxy.
- OhlcProxy/TickProxy — the proxy directly; the indicator resolves
its columns via default_refs(proxy) (only
indicators that implement it, e.g.
VolumeProfile / RollingVolumeProfile).
Visualization parameters (optional, override the defaults defined in the indicator's plot_config):
plot : bool — False to skip rendering
overlay : bool | None — True=on OHLC, False=own panel
name : str | list[str] — legend label.
str → single entry controlling all dimensions.
list[str] → one entry per dimension.
panel_height : int — own panel height in px
panel_title : str | None — Y-axis title in own panel.
None → uses name (if str) or display_name().
zorder : int — rendering order (higher = on top)
color : str | list[str] — CSS color(s) per dimension
line_width : float | list[float] — line width per dimension
line_dash : str | list[str] — line style per dimension
line_alpha : float | list[float] — transparency per dimension
scatter : bool — True to render as points
marker : str | list[str] — marker shape per dimension
marker_size : int | list[int] — marker size per dimension
marker_alpha : float | list[float] — transparency per dimension
marker_fill : bool | list[bool] — fill per dimension
marker_line_width : float | list[float] — border width per dimension
reference_lines : list[dict] — horizontal reference lines
Source code in src/tradetropy/models/strategy.py
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 | |
use_tool ¶
use_tool(source: 'TickProxy | OhlcProxy', tool: 'Tool', *, start=None, end=None, plot: bool = True, **plot_overrides)
Run an on-demand analysis tool over source and return its snapshot.
A pull operation fully controlled by the strategy: call it inside
on_data() with the source, the tool instance and the range. Nothing
is precomputed and plotting is skipped in optimize/pool.
def on_data(self):
vp = self.use_tool(
self.ticks, FixedRangeVP(nodes="both"),
start=self.ts - 3_600_000, end=self.ts,
)
if vp and self.ticks.price[-1] > vp.vah:
self.sesh.buy("BTCUSDT", volume=1)
When plot is True the snapshot is stored for rendering; tool
snapshots are grouped into one legend entry per tool type (e.g. "VP",
"Fib"), so one legend click toggles all of that tool's drawings in both
backtest and livechart. Per-call **plot_overrides (e.g. name, color,
line_width) tweak the tool's plot config for that snapshot.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
TickProxy | OhlcProxy
|
Data the tool reads its slice from. |
required |
tool
|
Tool
|
A configured tool instance (e.g. FixedRangeVP(nodes=...)). |
required |
start
|
Range start (epoch ms, datetime, ISO str, or None=oldest row). |
None
|
|
end
|
Range end (epoch ms, datetime, ISO str, or None=newest row). |
None
|
|
plot
|
bool
|
If True, store the snapshot for rendering. Default True. |
True
|
**plot_overrides
|
Per-snapshot visualization overrides (e.g. color). |
{}
|
Returns:
| Type | Description |
|---|---|
|
The computed snapshot (e.g. VolumeProfileResult). Falsy when the |
|
|
requested range has no data in the source buffer. |
Source code in src/tradetropy/models/strategy.py
add_pattern_matcher ¶
add_pattern_matcher(base_pivot, pattern: 'Pattern | str', *, decorators: list | None = None, tag: str = 'pattern') -> 'PatternMatcherProxy'
Declares a pattern matcher and returns the proxy to use it in on_data().
The engine builds the PatternStore (once) before the backtest/live loop
and connects the proxy. In on_data() just access .last
(O(log n) in backtest, O(1) in live).
────────────────────────────────────────────────────────────────── ═══════════ DSL (STRING-BASED) ═════════════════════════════════ ──────────────────────────────────────────────────────────────────
You can pass a DSL string instead of instantiating Pattern manually.
The DSL is parsed automatically with parse_pattern():
self.setup = self.add_pattern_matcher(
base_pivot=self.cpivot,
decorators=[self.nbs],
pattern = """
L[nbs=boo]
H[nbs=neu] > $0
""",
tag="impulse",
)
Format of each line::
TYPE[?] [TAGS] [CONDITIONS]
TYPE H | L | any
Expected pivot type:
· 'H' → high pivots only
· 'L' → low pivots only
· 'any' → both (use when type doesn't matter but tags or
conditions do)
? — optional suffix (immediately after TYPE, no space) Marks the node as optional. The pattern can match whether or not the pivot exists.
Examples: H? L?[nbs=boo] any? > $0
TAGS — [key=val, key=val, ...] Filters the pivot must satisfy. Tags come from decorator indicators: · 'type' → from ConfirmedPivot ('H' / 'L') · 'nbs' → from NBS indicator ('neu', 'boo', 'shk', 'emp') · 'hhll' → from HHLL indicator ('HH', 'HL', 'LH', 'LL')
Alternative values can be specified with ``|``:
[nbs=neu|shk] → tag 'nbs' == 'neu' OR 'shk'
Examples:
[nbs=neu] → 'nbs' exactly 'neu'
[nbs=neu|shk] → 'nbs' is 'neu' or 'shk'
[nbs=neu, hhll=HH] → AND of two decorators
[hhll=LL] → only tag 'hhll'
CONDITIONS — optional Sequence of expressions separated by & (AND) or | (OR). Can be grouped with parentheses.
Operators::
> < >= <= == !=
Operands::
50000 → absolute number (price)
$0 → NodeRef(0, 'value') price of node 0
$0.value → same as $0
$0.index → bar index of node 0
$0.timestamp → timestamp of node 0
$0*1.02 → node 0 price × 1.02
$-1 → previous node price
$-1*0.98 → previous node price × 0.98
type(0) → type ('H'/'L') of node 0
type(-1) → type of previous node
Tag conditions (tag function)::
tag(nbs)==neu → current.tags['nbs'] == 'neu'
tag(nbs)!=shk → current.tags['nbs'] != 'shk'
tag(hhll)==HH → current.tags['hhll'] == 'HH'
tag(nbs)==neu|tag(nbs)==shk → OR between values (more
readable with [nbs=neu|shk] in TAGS)
Time conditions (@ prefix)::
@between(09:30, 16:00)
@between(09:30, 16:00, tz=America/New_York)
@after(14:00)
@after(09:30, tz=America/New_York)
@before(22:00)
@before(16:00, tz=Europe/London)
@weekday(monday, tuesday, wednesday)
@weekday(friday, tz=America/New_York)
@time_since($0.timestamp, min=2, max=168) # unit=h (default)
@time_since($0.timestamp, min=5, max=15, unit=m) # minutes
@time_since($0.timestamp, min=30, unit=s) # seconds
@hours_since($0.timestamp, min=2, max=168) # alias, always hours
@hours_since($-1.timestamp, min=0, max=24)
The lhs of the comparison uses the SAME attribute as the rhs:
> $0.value → current_price > node0_price
> $0.index → current_index > node0_index
> $0.timestamp → current_timestamp > node0_timestamp
Comments Lines starting with '#' are ignored.
Anchor end
If the last line of the DSL is a standalone $, anchor_end
is activated: the match is only valid if it ends at the LAST
confirmed pivot in the sequence (useful for detecting the exact
end of a pattern).
Complete DSL examples::
# Minimal — 2 nodes
pattern = """
L
H > $0
"""
# With NBS tags and price/time conditions
pattern = """
L[nbs=boo] @between(09:30, 16:00, tz=America/New_York)
H[nbs=neu] > $0 & @between(09:30, 16:00, tz=America/New_York)
L[nbs=boo] > $0 & @hours_since($1.timestamp, min=0, max=48)
H[nbs=neu] > $1
"""
# With optional node and anchor end
pattern = """
H[nbs=neu]
L?[nbs=boo] # optional pullback
H[nbs=neu] > $0
$
"""
# OR between grouped conditions
pattern = """
L
H > $0 & (@between(09:30, 16:00, tz=America/New_York) | @after(20:00))
"""
# NodeRef by index and multiplier
pattern = """
H[nbs=neu, hhll=HH]
L[nbs=boo, hhll=HL] > $0*0.95
H[nbs=neu, hhll=HH] > $1
"""
────────────────────────────────────────────────────────────────── ═══════════ OBJECT-BASED API ═══════════════════════════════════ ──────────────────────────────────────────────────────────────────
If you prefer the Python API, import the classes from
tradetropy.ta.pattern:
from tradetropy.ta.pattern import (
Pattern, PatternNode,
Condition, ConditionAnd, ConditionOr,
NodeRef, NodeTypeRef, TagCondition, TimeCondition,
)
── Pattern ──────────────────────────────────────────────────── Ordered sequence of PatternNodes with an identifying tag.
pattern = Pattern(
nodes=[...],
tag="pattern_name",
anchor_end=False, # optional
)
Properties
· length — total nodes (mandatory + optional) · min_length — mandatory nodes only · tag — pattern name · anchor_end — True only if the match must end at the last confirmed pivot
── PatternNode ──────────────────────────────────────────────── Describes a pivot within a pattern.
PatternNode(
type='H', # 'H' | 'L' | 'any'
tag_filters={'nbs': 'neu'}, # dict of required tags
conditions=[Condition(...)], # implicit AND list
optional=False, # True if optional
)
· type : expected type. 'any' accepts both H and L. · tag_filters : dict {decorator_name: expected_value}. The pivot must have ALL specified tags. E.g. {'nbs': 'neu', 'hhll': 'HH'}. {} = no filter. · conditions : list of ConditionExpr with implicit AND. For OR use ConditionOr. · optional : if True, this node may be present or absent in the match. The pattern is considered valid in both cases. NodeRefs pointing to an absent optional node return False safely (the candidate is discarded).
── Condition ────────────────────────────────────────────────── Atomic condition: compares the current node's attribute against an operand.
Condition('>', 3500) # absolute price
Condition('<', NodeRef(0, 'value')) # relative to node 0
Condition('>', NodeRef(0, 'value', 1.02)) # > 2% above node 0
Condition('>', NodeRef(-1, 'value')) # > previous node
Condition('>', NodeRef(0, 'index')) # current index > node 0 index
Condition('<', NodeRef(-1, 'timestamp')) # current ts < previous node ts
Condition('==', NodeTypeRef(0)) # same type as node 0
Condition('!=', NodeTypeRef(-1)) # opposite type from previous
The lhs is derived from the same attribute as the rhs (value, index, timestamp, or type for NodeTypeRef), ensuring coherent units.
── NodeRef ──────────────────────────────────────────────────── Reference to another node's attribute in the pattern.
NodeRef(position, attribute='value', multiplier=1.0)
· position : >= 0 → absolute ($0, $1, $2...) < 0 → relative ($-1 = previous node) · attribute : 'value' | 'index' | 'timestamp' · multiplier : factor applied to the referenced value. E.g. NodeRef(0, 'value', 0.98) → 98% of node 0's price
── NodeTypeRef ──────────────────────────────────────────────── Reference to the type ('H' or 'L') of another node.
NodeTypeRef(position)
Condition('==', NodeTypeRef(0)) # same type as node 0
Condition('!=', NodeTypeRef(-1)) # opposite type from previous
Only supports '==' and '!=' operators.
── TimeCondition ────────────────────────────────────────────── Time condition on the current node's timestamp.
'between'
TimeCondition('between', start='09:30', end='16:00',
tz='America/New_York')
Overnight range: start='22:00', end='04:00'
'after'
TimeCondition('after', start='14:00')
TimeCondition('after', start='09:30', tz='America/New_York')
'before'
TimeCondition('before', end='22:00')
TimeCondition('before', end='16:00', tz='Europe/London')
'weekday'
TimeCondition('weekday',
days=['monday','tuesday','wednesday'])
TimeCondition('weekday', days=['friday'],
tz='America/New_York')
Valid days: monday..sunday or mon..sun
'hours_since'
TimeCondition('hours_since',
ref=NodeRef(0, 'timestamp'),
min_hours=2, max_hours=48)
TimeCondition('hours_since',
ref=NodeRef(-1, 'timestamp'),
min_hours=0, max_hours=24)
max_hours=None = no upper limit
In the DSL this is written @time_since($N.timestamp,
min=..., max=..., unit=h|m|s) (minutes/seconds converted to
hours by the parser); @hours_since is the hours-only alias.
── TagCondition ──────────────────────────────────────────────── Condition on a tag value of the current node. Useful for OR between tags of different keys (nbs vs hhll).
TagCondition('nbs', '==', 'neu')
TagCondition('hhll', '==', 'HH')
TagCondition('nbs', '!=', 'shk')
# OR between tags of different keys
ConditionOr([
TagCondition('nbs', '==', 'neu'),
TagCondition('hhll', '==', 'HH'),
])
── ConditionAnd / ConditionOr ───────────────────────────────── Logical combinators for ConditionExpr.
# (A AND B) OR C
ConditionOr([
ConditionAnd([
Condition('>', 3500),
TimeCondition('between', '09:00', '10:00'),
]),
Condition('==', NodeTypeRef(0)),
])
────────────────────────────────────────────────────────────────── ═══════════ RUNTIME OBJECTS ═══════════════════════════════════ ──────────────────────────────────────────────────────────────────
── PatternMatcherProxy ──────────────────────────────────────── The proxy returned by add_pattern_matcher().
proxy = self.add_pattern_matcher(...)
match = proxy.last # MatchResult | None
Main property
.last : MatchResult | None
── MatchResult ──────────────────────────────────────────────── Result of a successful pattern match. It is frozen (immutable) and supports indexed access like a tuple.
match = self.setup.last
if match:
n0 = match[0] # PivotPoint of the first node
match[-1] # last node
len(match) # number of matched nodes
n0.value # pivot price
n0.index # bar index
n0.timestamp # ts_ms UTC epoch
n0.type # 'H' or 'L'
n0.tags # {"type": "H", "nbs": "neu"}
match.tag # pattern name
match.first # first node (PivotPoint)
match.last_node # last node (PivotPoint)
match.indices # [45, 48, 52]
match.values # [3500.0, 3200.0, 3600.0]
match.timestamps # [t1, t2, t3]
# Optional nodes:
match.matched_optional # {node_idx: bool, ...} or None
match.node_map # {node_idx: PivotPoint|None}
── PivotPoint ───────────────────────────────────────────────── Represents a confirmed pivot at runtime. It is frozen (immutable, hashable).
PivotPoint(
index=45, # bar index
timestamp=1234567890000.0, # ts_ms UTC
value=3500.0, # price
type='H', # 'H' | 'L'
tags={'type':'H', 'nbs':'neu'}, # combined tags
)
Shortcuts from MatchResult
match.indices → [n.index for n in match] match.values → [n.value for n in match] match.timestamps → [n.timestamp for n in match]
────────────────────────────────────────────────────────────────── ═══════════ PARAMETERS ════════════════════════════════════════ ──────────────────────────────────────────────────────────────────
base_pivot Proxy returned by add_indicator() for the base ConfirmedPivot. It provides the H/L pivot sequence and is mandatory.
decorators
Optional list of pivot indicator proxies such as NBS or HHLL.
They add tags to the base pivot sequence. All decorators must use
the same OhlcProxy as base_pivot.
pattern Can be: · A DSL string (see DSL section above). · A Pattern instance built with the object API.
If it's a string, the pattern tag is taken from the ``tag``
parameter. If it's a Pattern, ``pattern.tag`` is used.
tag
Pattern name. Used as the tag in MatchResult.
Only relevant when pattern is a DSL string;
if pattern is a Pattern object, this parameter is ignored.
Returns
PatternMatcherProxy — use .last in on_data() to get
the most recent MatchResult.
────────────────────────────────────────────────────────────────── ═══════════ COMPLETE EXAMPLES ═════════════════════════════════ ──────────────────────────────────────────────────────────────────
Example 1 — DSL string (recommended for readability)::
def init(self):
self.btc = self.subscribe_ohlc("BTCUSDT", timeframe='5m')
self.cpivot = self.add_indicator(
[self.btc.high_ref, self.btc.low_ref, self.btc.ts_ref],
ConfirmedPivot(swing=3),
)
self.nbs = self.add_indicator(
[self.btc.high_ref, self.btc.low_ref, self.btc.ts_ref],
NBS(swing=3),
)
self.setup = self.add_pattern_matcher(
base_pivot=self.cpivot,
decorators=[self.nbs],
pattern = """
# Base support — Booster in NY session
L[nbs=boo] @between(09:30, 16:00, tz=America/New_York)
# Bullish impulse — Neutralizer in same timeframe
H[nbs=neu] > $0 & @between(09:30, 16:00, tz=America/New_York)
# Second support within 48h
L[nbs=boo] > $0 & @hours_since($1.timestamp, min=0, max=48)
# Second impulse — Higher High
H[nbs=neu] > $1
""",
tag="bullish_impulse_ny",
)
def on_data(self):
match = self.setup.last
if match:
self.log.signal(
"L=%.2f H=%.2f HL=%.2f HH=%.2f",
match[0].value, match[1].value,
match[2].value, match[3].value,
)
Example 2 — Object API with optionals and OR::
from tradetropy.ta.pattern import (
Pattern, PatternNode,
Condition, ConditionOr,
NodeRef,
)
def init(self):
# ... ohlc and pivots setup ...
self.setup = self.add_pattern_matcher(
base_pivot=self.cpivot,
decorators=[self.nbs],
pattern = Pattern([
PatternNode('H', {'nbs': 'neu'}, []),
PatternNode('L', {'nbs': 'boo'}, [],
optional=True), # optional pullback
PatternNode('H', {'nbs': 'neu'}, [
Condition('>', NodeRef(0, 'value')),
ConditionOr([
Condition('>', 3700),
Condition('>', NodeRef(0, 'value', 1.02)),
]),
]),
], tag="hh_optional_pullback"),
)
def on_data(self):
match = self.setup.last
if match:
n0 = match[0]
n1 = match[1] # if optional matched, it's a PivotPoint
n2 = match[2]
# Was the optional node (position 1) present?
if match.matched_optional and match.matched_optional.get(1):
pullback = match.node_map[1]
self.log.info("Pullback at %.2f", pullback.value)
Example 3 — DSL with anchor_end and optional node::
def init(self):
self.setup = self.add_pattern_matcher(
base_pivot=self.cpivot,
decorators=[self.nbs],
pattern = """
H[nbs=neu]
L?[nbs=boo]
H[nbs=neu] > $0
$
""",
tag="anchored_setup",
)
def on_data(self):
match = self.setup.last
if match and match.last_node.index == self.cpivot.last_pivot_index:
# The match ends exactly at the last confirmed pivot
self.log.info("Pattern just completed: %s", match.values)
Example 4 — OR inline in tag_filters and TagCondition::
def init(self):
self.setup = self.add_pattern_matcher(
base_pivot=self.cpivot,
decorators=[self.nbs],
pattern = """
# OR inline: nbs='neu' OR 'shk' (same key)
H[nbs=neu|shk] > $0
# TagCondition: OR between different keys
L (tag(nbs)==boo | tag(hhll)==LL)
# Combined: TagCondition + price
H > $0 & (tag(nbs)==neu | tag(hhll)==HH)
""",
tag="mixed_tag_or",
)
def on_data(self):
match = self.setup.last
if match:
self.log.info(
"Match: tag=%s values=%s",
match.tag, match.values,
)
Source code in src/tradetropy/models/strategy.py
917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 | |
declare ¶
Called by the pool to scan subscriptions without executing init() logic. By default delegates to init(), which is correct for the vast majority of strategies where init() only declares proxies.
Override only if init() has heavy side effects (DB connections, file reads, expensive calculations, etc.) that should not run during the pool scan:
class MyStrategy(Strategy):
def declare(self):
# Declarations only — called during pool scan
self._tp = self.subscribe_ticks("BTCUSDT", window_size=50)
self._sma = self.add_indicator(self._tp.price_ref, SMA(3))
def init(self):
self.declare()
# Heavy logic — only runs in real execution
self.model = load_ml_model("weights.pkl")
self.db = connect_database()
Source code in src/tradetropy/models/strategy.py
init ¶
on_data ¶
Called per tick (BacktestEngine) or per bar (BacktestEngine). The broker already has updated prices when this executes.
on_stop ¶
Called when the engine stops cleanly. Useful for closing positions, saving state, final logging, etc.
Invoked in these cases: · Ctrl+C (KeyboardInterrupt) · Explicit engine.stop() · End of loop (kline mode with no more data)
Example
def on_stop(self): self.log.trading("Engine stopped — closing open positions") for p in self.sesh.positions(): self.sesh.position_close(p.ticket)
Source code in src/tradetropy/models/strategy.py
on_crash ¶
Called when an unhandled exception occurs in the feed loop. The engine has already stopped the loop when this method is called.
The exception is available as exc for logging or diagnosis.
If not overridden, the engine re-raises it after calling this.
Example
def on_crash(self, exc: Exception): self.log.error("Loop error: %s — %s", type(exc).name, exc) # notify via telegram, save state, etc.