openbacktesting 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openbacktesting/__init__.py +85 -0
- openbacktesting/aggregation.py +8 -0
- openbacktesting/broker/__init__.py +54 -0
- openbacktesting/broker/commission.py +57 -0
- openbacktesting/broker/core.py +1122 -0
- openbacktesting/broker/execution.py +62 -0
- openbacktesting/broker/instrument.py +106 -0
- openbacktesting/broker/ledger.py +60 -0
- openbacktesting/broker/models.py +236 -0
- openbacktesting/broker/observations.py +99 -0
- openbacktesting/broker/requests.py +192 -0
- openbacktesting/broker/sizing.py +76 -0
- openbacktesting/colors.py +175 -0
- openbacktesting/data/__init__.py +20 -0
- openbacktesting/data/aggregation.py +39 -0
- openbacktesting/data/context.py +30 -0
- openbacktesting/data/core.py +218 -0
- openbacktesting/data/generators.py +54 -0
- openbacktesting/data/strategy_data.py +103 -0
- openbacktesting/data/timeframe.py +69 -0
- openbacktesting/engine.py +34 -0
- openbacktesting/helpers.py +77 -0
- openbacktesting/indicator_api.py +10 -0
- openbacktesting/indicator_methods.py +123 -0
- openbacktesting/indicator_types.py +19 -0
- openbacktesting/indicators/__init__.py +7 -0
- openbacktesting/indicators/api.py +97 -0
- openbacktesting/indicators/calculus.py +75 -0
- openbacktesting/indicators/candles/__init__.py +0 -0
- openbacktesting/indicators/candles/cdl_doji.py +17 -0
- openbacktesting/indicators/candles/cdl_engulfing.py +14 -0
- openbacktesting/indicators/candles/cdl_eveningstar.py +15 -0
- openbacktesting/indicators/candles/cdl_hammer.py +14 -0
- openbacktesting/indicators/candles/cdl_harami.py +14 -0
- openbacktesting/indicators/candles/cdl_inside.py +14 -0
- openbacktesting/indicators/candles/cdl_marubozu.py +14 -0
- openbacktesting/indicators/candles/cdl_morningstar.py +15 -0
- openbacktesting/indicators/candles/cdl_shootingstar.py +14 -0
- openbacktesting/indicators/candles/cdl_z.py +28 -0
- openbacktesting/indicators/candles/ha.py +19 -0
- openbacktesting/indicators/catalog.json +8348 -0
- openbacktesting/indicators/common.py +94 -0
- openbacktesting/indicators/cycles/__init__.py +0 -0
- openbacktesting/indicators/cycles/dsp.py +10 -0
- openbacktesting/indicators/cycles/ebsw.py +10 -0
- openbacktesting/indicators/cycles/ht_dcperiod.py +10 -0
- openbacktesting/indicators/cycles/ht_dcphase.py +10 -0
- openbacktesting/indicators/cycles/ht_phasor.py +17 -0
- openbacktesting/indicators/cycles/ht_sine.py +17 -0
- openbacktesting/indicators/cycles/ht_trendmode.py +10 -0
- openbacktesting/indicators/cycles/msw.py +19 -0
- openbacktesting/indicators/methods.py +5762 -0
- openbacktesting/indicators/momentum/__init__.py +0 -0
- openbacktesting/indicators/momentum/ac.py +9 -0
- openbacktesting/indicators/momentum/ao.py +10 -0
- openbacktesting/indicators/momentum/apo.py +10 -0
- openbacktesting/indicators/momentum/bias.py +10 -0
- openbacktesting/indicators/momentum/bop.py +10 -0
- openbacktesting/indicators/momentum/brar.py +26 -0
- openbacktesting/indicators/momentum/cci.py +10 -0
- openbacktesting/indicators/momentum/cfo.py +10 -0
- openbacktesting/indicators/momentum/cg.py +10 -0
- openbacktesting/indicators/momentum/cmo.py +10 -0
- openbacktesting/indicators/momentum/coppock.py +10 -0
- openbacktesting/indicators/momentum/crsi.py +28 -0
- openbacktesting/indicators/momentum/cti.py +10 -0
- openbacktesting/indicators/momentum/dm.py +17 -0
- openbacktesting/indicators/momentum/er.py +10 -0
- openbacktesting/indicators/momentum/eri.py +17 -0
- openbacktesting/indicators/momentum/fisher.py +17 -0
- openbacktesting/indicators/momentum/fosc.py +10 -0
- openbacktesting/indicators/momentum/gator.py +13 -0
- openbacktesting/indicators/momentum/ibs.py +7 -0
- openbacktesting/indicators/momentum/inertia.py +22 -0
- openbacktesting/indicators/momentum/kdj.py +18 -0
- openbacktesting/indicators/momentum/kst.py +30 -0
- openbacktesting/indicators/momentum/lrsi.py +10 -0
- openbacktesting/indicators/momentum/macd.py +27 -0
- openbacktesting/indicators/momentum/macdext.py +27 -0
- openbacktesting/indicators/momentum/macdfix.py +18 -0
- openbacktesting/indicators/momentum/mom.py +10 -0
- openbacktesting/indicators/momentum/pgo.py +10 -0
- openbacktesting/indicators/momentum/pmo.py +13 -0
- openbacktesting/indicators/momentum/po.py +10 -0
- openbacktesting/indicators/momentum/ppo.py +20 -0
- openbacktesting/indicators/momentum/psl.py +12 -0
- openbacktesting/indicators/momentum/pvo.py +20 -0
- openbacktesting/indicators/momentum/qqe.py +31 -0
- openbacktesting/indicators/momentum/rci.py +20 -0
- openbacktesting/indicators/momentum/rmi.py +7 -0
- openbacktesting/indicators/momentum/roc.py +10 -0
- openbacktesting/indicators/momentum/rocp.py +10 -0
- openbacktesting/indicators/momentum/rocr.py +10 -0
- openbacktesting/indicators/momentum/rocr100.py +10 -0
- openbacktesting/indicators/momentum/rsi.py +10 -0
- openbacktesting/indicators/momentum/rsx.py +10 -0
- openbacktesting/indicators/momentum/rvgi.py +26 -0
- openbacktesting/indicators/momentum/slope.py +17 -0
- openbacktesting/indicators/momentum/smc_sweep.py +18 -0
- openbacktesting/indicators/momentum/smi.py +21 -0
- openbacktesting/indicators/momentum/smi_stochastic.py +15 -0
- openbacktesting/indicators/momentum/squeeze.py +47 -0
- openbacktesting/indicators/momentum/squeeze_pro.py +53 -0
- openbacktesting/indicators/momentum/stc.py +20 -0
- openbacktesting/indicators/momentum/stoch.py +19 -0
- openbacktesting/indicators/momentum/stochf.py +19 -0
- openbacktesting/indicators/momentum/stochrsi.py +19 -0
- openbacktesting/indicators/momentum/td_seq.py +17 -0
- openbacktesting/indicators/momentum/trix.py +19 -0
- openbacktesting/indicators/momentum/trixh.py +20 -0
- openbacktesting/indicators/momentum/tsi.py +26 -0
- openbacktesting/indicators/momentum/uo.py +22 -0
- openbacktesting/indicators/momentum/vwmacd.py +20 -0
- openbacktesting/indicators/momentum/wavetrend.py +15 -0
- openbacktesting/indicators/momentum/willr.py +10 -0
- openbacktesting/indicators/overlap/__init__.py +0 -0
- openbacktesting/indicators/overlap/alligator.py +13 -0
- openbacktesting/indicators/overlap/alma.py +33 -0
- openbacktesting/indicators/overlap/anchored_vwap.py +50 -0
- openbacktesting/indicators/overlap/anchored_vwap_bands.py +13 -0
- openbacktesting/indicators/overlap/avgprice.py +10 -0
- openbacktesting/indicators/overlap/dema.py +10 -0
- openbacktesting/indicators/overlap/ema.py +12 -0
- openbacktesting/indicators/overlap/frama.py +29 -0
- openbacktesting/indicators/overlap/fwma.py +10 -0
- openbacktesting/indicators/overlap/hilo.py +26 -0
- openbacktesting/indicators/overlap/hl2.py +10 -0
- openbacktesting/indicators/overlap/hlc3.py +10 -0
- openbacktesting/indicators/overlap/hma.py +10 -0
- openbacktesting/indicators/overlap/ht_trendline.py +10 -0
- openbacktesting/indicators/overlap/hwma.py +10 -0
- openbacktesting/indicators/overlap/ichimoku.py +44 -0
- openbacktesting/indicators/overlap/jma.py +10 -0
- openbacktesting/indicators/overlap/kama.py +10 -0
- openbacktesting/indicators/overlap/linreg.py +15 -0
- openbacktesting/indicators/overlap/linregangle.py +10 -0
- openbacktesting/indicators/overlap/linregintercept.py +10 -0
- openbacktesting/indicators/overlap/linregslope.py +10 -0
- openbacktesting/indicators/overlap/ma.py +10 -0
- openbacktesting/indicators/overlap/ma_envelopes.py +16 -0
- openbacktesting/indicators/overlap/mama.py +17 -0
- openbacktesting/indicators/overlap/mavp.py +17 -0
- openbacktesting/indicators/overlap/mcgd.py +10 -0
- openbacktesting/indicators/overlap/medprice.py +10 -0
- openbacktesting/indicators/overlap/midpoint.py +10 -0
- openbacktesting/indicators/overlap/midprice.py +10 -0
- openbacktesting/indicators/overlap/mmar.py +21 -0
- openbacktesting/indicators/overlap/ohlc4.py +10 -0
- openbacktesting/indicators/overlap/pwma.py +10 -0
- openbacktesting/indicators/overlap/rainbow.py +25 -0
- openbacktesting/indicators/overlap/rma.py +10 -0
- openbacktesting/indicators/overlap/sinwma.py +10 -0
- openbacktesting/indicators/overlap/sma.py +14 -0
- openbacktesting/indicators/overlap/ssf.py +10 -0
- openbacktesting/indicators/overlap/supertrend.py +21 -0
- openbacktesting/indicators/overlap/swma.py +10 -0
- openbacktesting/indicators/overlap/t3.py +10 -0
- openbacktesting/indicators/overlap/tema.py +10 -0
- openbacktesting/indicators/overlap/trima.py +10 -0
- openbacktesting/indicators/overlap/tsf.py +10 -0
- openbacktesting/indicators/overlap/typprice.py +10 -0
- openbacktesting/indicators/overlap/vidya.py +10 -0
- openbacktesting/indicators/overlap/vwap.py +10 -0
- openbacktesting/indicators/overlap/vwma.py +10 -0
- openbacktesting/indicators/overlap/wcp.py +10 -0
- openbacktesting/indicators/overlap/wma.py +10 -0
- openbacktesting/indicators/overlap/zlma.py +10 -0
- openbacktesting/indicators/performance/__init__.py +0 -0
- openbacktesting/indicators/performance/drawdown.py +18 -0
- openbacktesting/indicators/performance/log_return.py +10 -0
- openbacktesting/indicators/performance/percent_return.py +10 -0
- openbacktesting/indicators/predicates.py +88 -0
- openbacktesting/indicators/registry.py +571 -0
- openbacktesting/indicators/runtime.py +290 -0
- openbacktesting/indicators/statistics/__init__.py +0 -0
- openbacktesting/indicators/statistics/beta.py +10 -0
- openbacktesting/indicators/statistics/correl.py +10 -0
- openbacktesting/indicators/statistics/entropy.py +10 -0
- openbacktesting/indicators/statistics/kurtosis.py +10 -0
- openbacktesting/indicators/statistics/mad.py +10 -0
- openbacktesting/indicators/statistics/md.py +10 -0
- openbacktesting/indicators/statistics/median.py +10 -0
- openbacktesting/indicators/statistics/mode.py +48 -0
- openbacktesting/indicators/statistics/quantile.py +10 -0
- openbacktesting/indicators/statistics/skew.py +10 -0
- openbacktesting/indicators/statistics/stderr.py +10 -0
- openbacktesting/indicators/statistics/stdev.py +10 -0
- openbacktesting/indicators/statistics/variance.py +10 -0
- openbacktesting/indicators/statistics/zscore.py +10 -0
- openbacktesting/indicators/trend/__init__.py +0 -0
- openbacktesting/indicators/trend/adx.py +28 -0
- openbacktesting/indicators/trend/adxr.py +28 -0
- openbacktesting/indicators/trend/amat.py +19 -0
- openbacktesting/indicators/trend/aroon.py +18 -0
- openbacktesting/indicators/trend/atr_stop.py +33 -0
- openbacktesting/indicators/trend/chop.py +20 -0
- openbacktesting/indicators/trend/cksp.py +17 -0
- openbacktesting/indicators/trend/cpr.py +57 -0
- openbacktesting/indicators/trend/decay.py +10 -0
- openbacktesting/indicators/trend/decreasing.py +18 -0
- openbacktesting/indicators/trend/dpo.py +12 -0
- openbacktesting/indicators/trend/dx.py +19 -0
- openbacktesting/indicators/trend/edecay.py +10 -0
- openbacktesting/indicators/trend/increasing.py +18 -0
- openbacktesting/indicators/trend/long_run.py +10 -0
- openbacktesting/indicators/trend/minus_dm.py +10 -0
- openbacktesting/indicators/trend/pivot_points.py +89 -0
- openbacktesting/indicators/trend/pivothigh.py +23 -0
- openbacktesting/indicators/trend/pivotlow.py +23 -0
- openbacktesting/indicators/trend/plus_dm.py +10 -0
- openbacktesting/indicators/trend/pmax.py +18 -0
- openbacktesting/indicators/trend/psar.py +19 -0
- openbacktesting/indicators/trend/qstick.py +10 -0
- openbacktesting/indicators/trend/sarext.py +35 -0
- openbacktesting/indicators/trend/short_run.py +10 -0
- openbacktesting/indicators/trend/tsignals.py +26 -0
- openbacktesting/indicators/trend/ttm_trend.py +16 -0
- openbacktesting/indicators/trend/vhf.py +10 -0
- openbacktesting/indicators/trend/volatility_stop.py +33 -0
- openbacktesting/indicators/trend/vortex.py +17 -0
- openbacktesting/indicators/trend/xsignals.py +29 -0
- openbacktesting/indicators/types.py +344 -0
- openbacktesting/indicators/utility/__init__.py +0 -0
- openbacktesting/indicators/utility/barssince.py +21 -0
- openbacktesting/indicators/utility/change.py +7 -0
- openbacktesting/indicators/utility/cross.py +12 -0
- openbacktesting/indicators/utility/crossover.py +11 -0
- openbacktesting/indicators/utility/crossunder.py +11 -0
- openbacktesting/indicators/utility/cum.py +7 -0
- openbacktesting/indicators/utility/falling.py +10 -0
- openbacktesting/indicators/utility/highest.py +7 -0
- openbacktesting/indicators/utility/highestbars.py +29 -0
- openbacktesting/indicators/utility/lowest.py +7 -0
- openbacktesting/indicators/utility/lowestbars.py +29 -0
- openbacktesting/indicators/utility/percentile_linear_interpolation.py +12 -0
- openbacktesting/indicators/utility/percentile_nearest_rank.py +16 -0
- openbacktesting/indicators/utility/percentrank.py +7 -0
- openbacktesting/indicators/utility/range.py +8 -0
- openbacktesting/indicators/utility/rising.py +10 -0
- openbacktesting/indicators/utility/rolling_count.py +12 -0
- openbacktesting/indicators/utility/rolling_sum.py +7 -0
- openbacktesting/indicators/utility/running_max.py +7 -0
- openbacktesting/indicators/utility/running_min.py +7 -0
- openbacktesting/indicators/utility/shift.py +7 -0
- openbacktesting/indicators/utility/valuewhen.py +25 -0
- openbacktesting/indicators/volatility/__init__.py +0 -0
- openbacktesting/indicators/volatility/aberration.py +21 -0
- openbacktesting/indicators/volatility/accbands.py +20 -0
- openbacktesting/indicators/volatility/adr.py +7 -0
- openbacktesting/indicators/volatility/atr.py +12 -0
- openbacktesting/indicators/volatility/avolume.py +10 -0
- openbacktesting/indicators/volatility/bbands.py +20 -0
- openbacktesting/indicators/volatility/ce.py +25 -0
- openbacktesting/indicators/volatility/cvi.py +10 -0
- openbacktesting/indicators/volatility/donchian.py +20 -0
- openbacktesting/indicators/volatility/hvol.py +10 -0
- openbacktesting/indicators/volatility/hwc.py +27 -0
- openbacktesting/indicators/volatility/kc.py +27 -0
- openbacktesting/indicators/volatility/massi.py +10 -0
- openbacktesting/indicators/volatility/natr.py +19 -0
- openbacktesting/indicators/volatility/pdist.py +10 -0
- openbacktesting/indicators/volatility/rvi.py +21 -0
- openbacktesting/indicators/volatility/starc.py +11 -0
- openbacktesting/indicators/volatility/thermo.py +28 -0
- openbacktesting/indicators/volatility/true_range.py +31 -0
- openbacktesting/indicators/volatility/ui.py +10 -0
- openbacktesting/indicators/volume/__init__.py +0 -0
- openbacktesting/indicators/volume/ad.py +10 -0
- openbacktesting/indicators/volume/adosc.py +19 -0
- openbacktesting/indicators/volume/aobv.py +31 -0
- openbacktesting/indicators/volume/cmf.py +12 -0
- openbacktesting/indicators/volume/efi.py +12 -0
- openbacktesting/indicators/volume/emv.py +10 -0
- openbacktesting/indicators/volume/eom.py +19 -0
- openbacktesting/indicators/volume/iii.py +9 -0
- openbacktesting/indicators/volume/kvo.py +28 -0
- openbacktesting/indicators/volume/marketfi.py +10 -0
- openbacktesting/indicators/volume/mfi.py +12 -0
- openbacktesting/indicators/volume/nvi.py +26 -0
- openbacktesting/indicators/volume/obv.py +10 -0
- openbacktesting/indicators/volume/pvi.py +26 -0
- openbacktesting/indicators/volume/pvol.py +10 -0
- openbacktesting/indicators/volume/pvr.py +10 -0
- openbacktesting/indicators/volume/pvt.py +10 -0
- openbacktesting/indicators/volume/rvol.py +8 -0
- openbacktesting/indicators/volume/vfi.py +18 -0
- openbacktesting/indicators/volume/vosc.py +10 -0
- openbacktesting/indicators/volume/wad.py +10 -0
- openbacktesting/indicators/volume/wvad.py +9 -0
- openbacktesting/inputs.py +51 -0
- openbacktesting/lib.py +31 -0
- openbacktesting/logs.py +129 -0
- openbacktesting/optimization.py +8 -0
- openbacktesting/optimizer/__init__.py +25 -0
- openbacktesting/optimizer/_semantics.py +155 -0
- openbacktesting/optimizer/identity.py +45 -0
- openbacktesting/optimizer/models.py +92 -0
- openbacktesting/optimizer/plan.py +129 -0
- openbacktesting/optimizer/portfolio-result.schema.json +4984 -0
- openbacktesting/optimizer/query.py +257 -0
- openbacktesting/optimizer/runner.py +98 -0
- openbacktesting/optimizer/storage.py +261 -0
- openbacktesting/optimizer/validation.py +64 -0
- openbacktesting/parallel.py +9 -0
- openbacktesting/plotting/__init__.py +23 -0
- openbacktesting/plotting/collector.py +509 -0
- openbacktesting/plotting/encoding.py +102 -0
- openbacktesting/plotting/types.py +92 -0
- openbacktesting/portfolio/__init__.py +15 -0
- openbacktesting/portfolio/analysis.py +245 -0
- openbacktesting/portfolio/builder.py +381 -0
- openbacktesting/portfolio/catalog.json +1404 -0
- openbacktesting/portfolio/helpers.py +92 -0
- openbacktesting/portfolio/metrics.py +186 -0
- openbacktesting/portfolio/models.py +63 -0
- openbacktesting/portfolio/periods.py +164 -0
- openbacktesting/portfolio/rates.py +14 -0
- openbacktesting/portfolio/snapshot.py +111 -0
- openbacktesting/portfolio/stats.py +272 -0
- openbacktesting/portfolio/trades.py +136 -0
- openbacktesting/portfolio/trading.py +215 -0
- openbacktesting/py.typed +0 -0
- openbacktesting/runtime.py +330 -0
- openbacktesting/simulation/__init__.py +17 -0
- openbacktesting/simulation/collection.py +13 -0
- openbacktesting/simulation/config.py +241 -0
- openbacktesting/simulation/core.py +528 -0
- openbacktesting/simulation/optimization.py +326 -0
- openbacktesting/simulation/parallel.py +145 -0
- openbacktesting/simulation/variants.py +107 -0
- openbacktesting/strategy/__init__.py +19 -0
- openbacktesting/strategy/bar_state.py +14 -0
- openbacktesting/strategy/base.py +810 -0
- openbacktesting/strategy/declaration.py +90 -0
- openbacktesting/strategy/helpers.py +198 -0
- openbacktesting/strategy/inputs.py +460 -0
- openbacktesting/strategy/sizing.py +80 -0
- openbacktesting/strategy/templates.py +160 -0
- openbacktesting/strategy_data.py +10 -0
- openbacktesting/test/BTCUSD.csv +157 -0
- openbacktesting/test/EURUSD.csv +5001 -0
- openbacktesting/test/GOOG.csv +2149 -0
- openbacktesting/test/__init__.py +29 -0
- openbacktesting/test/__main__.py +11 -0
- openbacktesting/test/_test.py +1312 -0
- openbacktesting/util.py +31 -0
- openbacktesting-0.2.0.dist-info/METADATA +151 -0
- openbacktesting-0.2.0.dist-info/RECORD +350 -0
- openbacktesting-0.2.0.dist-info/WHEEL +4 -0
- openbacktesting-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
"""Native strategy interface and indicator registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABCMeta, abstractmethod
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from difflib import get_close_matches
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from typing import Literal, cast, overload
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from openbacktesting.broker.core import Order, OrderGroup, Position, Trade, _Broker
|
|
15
|
+
from openbacktesting.broker.instrument import InstrumentSpec
|
|
16
|
+
from openbacktesting.broker.models import (
|
|
17
|
+
AccountSnapshot,
|
|
18
|
+
ClosedTradeStats,
|
|
19
|
+
Fill,
|
|
20
|
+
OrderEvent,
|
|
21
|
+
RunSummary,
|
|
22
|
+
)
|
|
23
|
+
from openbacktesting.broker.requests import OrderRequest, _Expiry, _JsonValue
|
|
24
|
+
from openbacktesting.broker.sizing import UNSET, _SizeIntent, select_size
|
|
25
|
+
from openbacktesting.data.core import _Data
|
|
26
|
+
from openbacktesting.data.strategy_data import DATA_NAMES, StrategyData
|
|
27
|
+
from openbacktesting.data.timeframe import Timeframe
|
|
28
|
+
from openbacktesting.helpers import _as_str, _set_legacy_module, try_
|
|
29
|
+
from openbacktesting.indicators.methods import TechnicalAnalysis
|
|
30
|
+
from openbacktesting.indicators.runtime import compute
|
|
31
|
+
from openbacktesting.indicators.types import IndicatorCursor, IndicatorError, IndicatorHandle
|
|
32
|
+
from openbacktesting.logs import StrategyLog
|
|
33
|
+
from openbacktesting.plotting.collector import Plotting
|
|
34
|
+
from openbacktesting.strategy.bar_state import BarState
|
|
35
|
+
from openbacktesting.strategy.inputs import ResolvedInputs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Strategy(metaclass=ABCMeta):
|
|
39
|
+
"""Strategy base class: Register indicators in init and place orders from currently visible history in next.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
A trading strategy base class. Extend this class and
|
|
43
|
+
override methods
|
|
44
|
+
`openbacktesting.engine.Strategy.init` and
|
|
45
|
+
`openbacktesting.engine.Strategy.next` to define
|
|
46
|
+
your own strategy.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
_input_strategy_base = True
|
|
50
|
+
_reserved_data_names = DATA_NAMES | {
|
|
51
|
+
"plotting",
|
|
52
|
+
"timeframe",
|
|
53
|
+
"bar_index",
|
|
54
|
+
"bar_state",
|
|
55
|
+
"log",
|
|
56
|
+
"closed_trade_stats",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def __init__(self, broker, data, params):
|
|
60
|
+
for cls in type(self).__mro__:
|
|
61
|
+
if cls is not Strategy:
|
|
62
|
+
conflicts = (
|
|
63
|
+
DATA_NAMES
|
|
64
|
+
| {
|
|
65
|
+
"ta",
|
|
66
|
+
"plotting",
|
|
67
|
+
"timeframe",
|
|
68
|
+
"bar_index",
|
|
69
|
+
"bar_state",
|
|
70
|
+
"log",
|
|
71
|
+
"closed_trade_stats",
|
|
72
|
+
}
|
|
73
|
+
).intersection(cls.__dict__)
|
|
74
|
+
if conflicts:
|
|
75
|
+
name = min(conflicts)
|
|
76
|
+
raise AttributeError(f"Strategy.{name} is a reserved attribute; rename the field or method with the same name")
|
|
77
|
+
self._bar_context: tuple[int, BarState] | None = None
|
|
78
|
+
self._indicators = []
|
|
79
|
+
self._indicator_cursor = IndicatorCursor(len(data))
|
|
80
|
+
self._broker: _Broker = broker
|
|
81
|
+
self._data: _Data = data
|
|
82
|
+
self._strategy_data = StrategyData(data, self._indicator_cursor)
|
|
83
|
+
self._ta = TechnicalAnalysis(self._register_indicator)
|
|
84
|
+
self._params = self._check_params(params)
|
|
85
|
+
|
|
86
|
+
def __setattr__(self, name, value):
|
|
87
|
+
if name in DATA_NAMES | {
|
|
88
|
+
"plotting",
|
|
89
|
+
"timeframe",
|
|
90
|
+
"bar_index",
|
|
91
|
+
"bar_state",
|
|
92
|
+
"log",
|
|
93
|
+
"closed_trade_stats",
|
|
94
|
+
}:
|
|
95
|
+
raise AttributeError(f"Strategy.{name} is a reserved attribute; rename the field or parameter with the same name")
|
|
96
|
+
super().__setattr__(name, value)
|
|
97
|
+
|
|
98
|
+
def __delattr__(self, name):
|
|
99
|
+
if name in DATA_NAMES | {
|
|
100
|
+
"plotting",
|
|
101
|
+
"timeframe",
|
|
102
|
+
"bar_index",
|
|
103
|
+
"bar_state",
|
|
104
|
+
"log",
|
|
105
|
+
"closed_trade_stats",
|
|
106
|
+
}:
|
|
107
|
+
raise AttributeError(f"Strategy.{name} is a reserved attribute and cannot be deleted; rename the field with the same name")
|
|
108
|
+
super().__delattr__(name)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _check_reserved_params(params):
|
|
112
|
+
conflicts = (
|
|
113
|
+
DATA_NAMES
|
|
114
|
+
| {"ta", "plotting", "timeframe", "bar_index", "bar_state", "log", "closed_trade_stats"}
|
|
115
|
+
).intersection(params)
|
|
116
|
+
if conflicts:
|
|
117
|
+
name = min(conflicts)
|
|
118
|
+
raise AttributeError(f"Strategy.{name} is a reserved attribute; rename the parameter with the same name")
|
|
119
|
+
|
|
120
|
+
def _enter_bar(self, index: int, total: int):
|
|
121
|
+
self._bar_context = (index, BarState(index == 0, index == total - 1))
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def bar_index(self) -> int:
|
|
125
|
+
"""Current zero-based data position; unavailable in init, on_finish, and after the run ends."""
|
|
126
|
+
if self._bar_context is None:
|
|
127
|
+
raise RuntimeError("bar_index: no current execution bar")
|
|
128
|
+
return self._bar_context[0]
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def bar_state(self) -> BarState:
|
|
132
|
+
"""Frozen first/last snapshot for the current data position; early termination does not relabel the current position as the last bar."""
|
|
133
|
+
if self._bar_context is None:
|
|
134
|
+
raise RuntimeError("bar_state: no current execution bar")
|
|
135
|
+
return self._bar_context[1]
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def timeframe(self) -> Timeframe | None:
|
|
139
|
+
"""This chart's timeframe; None without low-level context, with no inference from market data."""
|
|
140
|
+
context = getattr(self._data, "_context", None)
|
|
141
|
+
return context.timeframe if context is not None else None
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def log(self) -> StrategyLog:
|
|
145
|
+
"""Structured logs for the current run; use info, warning, and error inside author callbacks."""
|
|
146
|
+
return self._log
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def plotting(self) -> Plotting:
|
|
150
|
+
"""Pure plotting-data entry point for the current run; declare in init and write during runtime callbacks."""
|
|
151
|
+
return self._plotting
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def open(self) -> IndicatorHandle:
|
|
155
|
+
"""Open-price series for the current run; [-1] is the current value."""
|
|
156
|
+
return self._strategy_data.get("open")
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def high(self) -> IndicatorHandle:
|
|
160
|
+
"""High-price series for the current run."""
|
|
161
|
+
return self._strategy_data.get("high")
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def low(self) -> IndicatorHandle:
|
|
165
|
+
"""Low-price series for the current run."""
|
|
166
|
+
return self._strategy_data.get("low")
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def close(self) -> IndicatorHandle:
|
|
170
|
+
"""Close-price series for the current run, usable directly with self.ta in init."""
|
|
171
|
+
return self._strategy_data.get("close")
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def volume(self) -> IndicatorHandle:
|
|
175
|
+
"""Volume series; missing Volume remains NaN."""
|
|
176
|
+
return self._strategy_data.get("volume")
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def hl2(self) -> IndicatorHandle:
|
|
180
|
+
"""(High + Low) / 2, computed lazily per run."""
|
|
181
|
+
return self._strategy_data.get("hl2")
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def hlc3(self) -> IndicatorHandle:
|
|
185
|
+
"""(High + Low + Close) / 3。"""
|
|
186
|
+
return self._strategy_data.get("hlc3")
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def hlcc4(self) -> IndicatorHandle:
|
|
190
|
+
"""(High + Low + Close + Close) / 4, excluding Open."""
|
|
191
|
+
return self._strategy_data.get("hlcc4")
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def data_index(self) -> pd.Index:
|
|
195
|
+
"""Currently visible index; saved pandas objects do not update with the cursor."""
|
|
196
|
+
return self.data.index
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def df(self) -> pd.DataFrame:
|
|
200
|
+
"""Currently visible original market-data table; no guarantee of deep copying or content isolation."""
|
|
201
|
+
return self.data.df
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def pip(self) -> float:
|
|
205
|
+
"""Retains full-sample decimal-precision estimation; does not represent an exchange minimum step."""
|
|
206
|
+
return self.data.pip
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def ta(self) -> TechnicalAnalysis:
|
|
210
|
+
"""Indicator entry point for the current run, such as ta.ema(length=20) or ta.macd().histogram."""
|
|
211
|
+
return self._ta
|
|
212
|
+
|
|
213
|
+
@ta.setter
|
|
214
|
+
def ta(self, value):
|
|
215
|
+
raise AttributeError("Strategy.ta is a reserved attribute; rename the field or parameter with the same name")
|
|
216
|
+
|
|
217
|
+
def __repr__(self):
|
|
218
|
+
return "<Strategy " + str(self) + ">"
|
|
219
|
+
|
|
220
|
+
def __str__(self):
|
|
221
|
+
params = ",".join(
|
|
222
|
+
f"{i[0]}={i[1]}" for i in zip(self._params.keys(), map(_as_str, self._params.values()))
|
|
223
|
+
)
|
|
224
|
+
if params:
|
|
225
|
+
params = "(" + params + ")"
|
|
226
|
+
return f"{self.__class__.__name__}{params}"
|
|
227
|
+
|
|
228
|
+
def _check_params(self, params):
|
|
229
|
+
if isinstance(params, ResolvedInputs):
|
|
230
|
+
self._resolved_inputs = params
|
|
231
|
+
return params.overrides()
|
|
232
|
+
self._check_reserved_params(params)
|
|
233
|
+
for k, v in params.items():
|
|
234
|
+
if not hasattr(self, k):
|
|
235
|
+
suggestions = get_close_matches(
|
|
236
|
+
k, (attr for attr in dir(self) if not attr.startswith("_"))
|
|
237
|
+
)
|
|
238
|
+
hint = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
|
|
239
|
+
raise AttributeError(
|
|
240
|
+
f"Strategy '{self.__class__.__name__}' is missing parameter '{k}'. "
|
|
241
|
+
"Strategy class should define parameters as class variables before they "
|
|
242
|
+
"can be optimized or run with." + hint
|
|
243
|
+
)
|
|
244
|
+
setattr(self, k, v)
|
|
245
|
+
return params
|
|
246
|
+
|
|
247
|
+
def ind(
|
|
248
|
+
self,
|
|
249
|
+
func: Callable,
|
|
250
|
+
*args,
|
|
251
|
+
name=None,
|
|
252
|
+
**kwargs,
|
|
253
|
+
) -> IndicatorHandle:
|
|
254
|
+
"""Compute custom indicators in bulk during initialization and return read-only handles read through the backtest cursor.
|
|
255
|
+
|
|
256
|
+
Register the same function with different parameters or reuse it across strategies. Each registration executes the
|
|
257
|
+
function without sharing a custom-computation cache; prefer ``self.ta`` for built-in formulas.
|
|
258
|
+
Functions receive complete history in init; authors must ensure computations depend only on current and past data.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
func: Computation function returning one- or two-dimensional array-like output with time-axis length equal to market data.
|
|
262
|
+
Series/DataFrame indices must exactly match market data; arrays align by position.
|
|
263
|
+
*args: Positional arguments passed unchanged to the computation function.
|
|
264
|
+
name: Display name or sequence of multi-output channel names, with length equal to the channel count.
|
|
265
|
+
Defaults to the function and arguments; format fields in names are filled from computation arguments.
|
|
266
|
+
**kwargs: Keyword arguments passed unchanged to the computation function.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Read-only IndicatorHandle. Access named channels with ``result['middle']``;
|
|
270
|
+
``np.asarray()``, ``.s``, and ``.df`` return prefix snapshots at read time.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
IndicatorError: Registration outside init, changed market-data source, or misaligned pandas index.
|
|
274
|
+
TypeError: name is not a string or string sequence.
|
|
275
|
+
ValueError: Output shape, length, or channel-name count does not match.
|
|
276
|
+
RuntimeError: User-function computation failed, with the original exception retained as cause.
|
|
277
|
+
|
|
278
|
+
Note:
|
|
279
|
+
ready defaults to all True; valid depends on finite values. next runs on every bar starting with the first;
|
|
280
|
+
authors must check history length and valid for the channels they use. Invalid indicators do not suppress callbacks.
|
|
281
|
+
More complex output masks are not public parameters of this entry point.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
if "warmup" in kwargs:
|
|
285
|
+
raise TypeError("warmup has been removed; next runs on every bar, so check indicator valid explicitly")
|
|
286
|
+
if self._indicator_cursor.phase != "init":
|
|
287
|
+
raise IndicatorError("invalid-phase", "custom", "Indicators may be registered only in init()")
|
|
288
|
+
self._strategy_data.validate()
|
|
289
|
+
|
|
290
|
+
def _format_name(name: str) -> str:
|
|
291
|
+
return name.format(
|
|
292
|
+
*map(_as_str, args), **dict(zip(kwargs.keys(), map(_as_str, kwargs.values())))
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if name is None:
|
|
296
|
+
params = ",".join(filter(None, map(_as_str, chain(args, kwargs.values()))))
|
|
297
|
+
func_name = _as_str(func)
|
|
298
|
+
name = f"{func_name}({params})" if params else f"{func_name}"
|
|
299
|
+
elif isinstance(name, str):
|
|
300
|
+
name = _format_name(name)
|
|
301
|
+
elif try_(lambda: all(isinstance(item, str) for item in name), False):
|
|
302
|
+
name = [_format_name(item) for item in name]
|
|
303
|
+
else:
|
|
304
|
+
raise TypeError(
|
|
305
|
+
f"Unexpected `name=` type {type(name)}; expected `str` or `Sequence[str]`"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
value = func(*args, **kwargs)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
raise RuntimeError(f'Indicator "{name}" error. See traceback above.') from e
|
|
312
|
+
|
|
313
|
+
if isinstance(value, (pd.Series, pd.DataFrame)) and not value.index.equals(self.data.index):
|
|
314
|
+
raise IndicatorError("index-mismatch", name, "Indicator index must match the market-data index")
|
|
315
|
+
if isinstance(value, pd.DataFrame):
|
|
316
|
+
value = value.values.T
|
|
317
|
+
|
|
318
|
+
if value is not None:
|
|
319
|
+
value = try_(lambda: np.asarray(value, order="C"), None)
|
|
320
|
+
is_arraylike = bool(value is not None and value.shape)
|
|
321
|
+
|
|
322
|
+
# Optionally flip the array if the user returned e.g. `df.values`
|
|
323
|
+
if is_arraylike and value.shape[0] == len(self._data):
|
|
324
|
+
value = value.T
|
|
325
|
+
|
|
326
|
+
if isinstance(name, list) and (np.atleast_2d(value).shape[0] != len(name)):
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"Length of `name=` ({len(name)}) must agree with the number "
|
|
329
|
+
f"of arrays the indicator returns ({value.shape[0]})."
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if not is_arraylike or not 1 <= value.ndim <= 2 or value.shape[-1] != len(self._data):
|
|
333
|
+
raise ValueError(
|
|
334
|
+
"Indicators must return (optionally a tuple of) numpy.arrays of same "
|
|
335
|
+
f'length as `data` (data shape: {self._data.Close.shape}; indicator "{name}" '
|
|
336
|
+
f"shape: {getattr(value, 'shape', '')}, returned value: {value})"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
value = IndicatorHandle(
|
|
340
|
+
value,
|
|
341
|
+
name=name,
|
|
342
|
+
index=self.data.index,
|
|
343
|
+
cursor=self._indicator_cursor,
|
|
344
|
+
metadata={"kind": "custom", "name": name, "cache": "disabled"},
|
|
345
|
+
)
|
|
346
|
+
self._indicators.append(value)
|
|
347
|
+
return value
|
|
348
|
+
|
|
349
|
+
def _register_indicator(
|
|
350
|
+
self, indicator, *, inputs=None, backend=None, required_outputs=None, **parameters
|
|
351
|
+
):
|
|
352
|
+
"""Register built-in indicators internally for self.ta; the owned catalog defines output names and parameters."""
|
|
353
|
+
if self._indicator_cursor.phase != "init":
|
|
354
|
+
raise IndicatorError("invalid-phase", indicator, "Indicators may be registered only in init()")
|
|
355
|
+
self._strategy_data.validate()
|
|
356
|
+
value = compute(
|
|
357
|
+
indicator,
|
|
358
|
+
self.data.df,
|
|
359
|
+
parameters=parameters,
|
|
360
|
+
inputs=inputs,
|
|
361
|
+
backend=backend,
|
|
362
|
+
required_outputs=required_outputs,
|
|
363
|
+
cache=self._data._indicator_cache,
|
|
364
|
+
cursor=self._indicator_cursor,
|
|
365
|
+
)
|
|
366
|
+
self._indicators.append(value)
|
|
367
|
+
return value
|
|
368
|
+
|
|
369
|
+
@abstractmethod
|
|
370
|
+
def init(self):
|
|
371
|
+
"""Strategy initialization entry point for precomputation and indicator registration; complete historical data is accessible.
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
Initialize the strategy.
|
|
375
|
+
Override this method.
|
|
376
|
+
Declare indicators (with `openbacktesting.engine.Strategy.ind`).
|
|
377
|
+
Precompute what needs to be precomputed or can be precomputed
|
|
378
|
+
in a vectorized fashion before the strategy starts.
|
|
379
|
+
|
|
380
|
+
If you extend composable strategies from `openbacktesting.lib`,
|
|
381
|
+
make sure to call:
|
|
382
|
+
|
|
383
|
+
super().init()
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
@abstractmethod
|
|
387
|
+
def next(self):
|
|
388
|
+
"""Per-bar strategy callback, starting at input bar 0 and called once for each normally reached bar.
|
|
389
|
+
|
|
390
|
+
Called even with insufficient or invalid indicator history; check length before [-2] and valid before consuming indicators.
|
|
391
|
+
bar_index/bar_state represent the current input position and frozen first/last state.
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
Main strategy runtime method, called as each new
|
|
395
|
+
`openbacktesting.engine.Strategy.data`
|
|
396
|
+
instance (row; full candlestick bar) becomes available.
|
|
397
|
+
This is the main method where strategy decisions
|
|
398
|
+
upon data precomputed in `openbacktesting.engine.Strategy.init`
|
|
399
|
+
take place.
|
|
400
|
+
|
|
401
|
+
If you extend composable strategies from `openbacktesting.lib`,
|
|
402
|
+
make sure to call:
|
|
403
|
+
|
|
404
|
+
super().next()
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
def on_start(self):
|
|
408
|
+
"""One notification after initialization completes; order submission is prohibited here."""
|
|
409
|
+
|
|
410
|
+
def on_finish(self, summary: RunSummary):
|
|
411
|
+
"""Receive a frozen finish summary, including exceptional termination; trading is prohibited."""
|
|
412
|
+
|
|
413
|
+
def on_order(self, event: OrderEvent):
|
|
414
|
+
"""Receive frozen order events; new requests execute no earlier than the next bar."""
|
|
415
|
+
|
|
416
|
+
def on_fill(self, fill: Fill):
|
|
417
|
+
"""Receive actual fill facts; callbacks do not recursively trigger matching."""
|
|
418
|
+
|
|
419
|
+
def _order_size(self, qty, amount, percent, reduce_only):
|
|
420
|
+
self._broker._permission()
|
|
421
|
+
settings = getattr(self._data, "_default_order_size", None)
|
|
422
|
+
default = None
|
|
423
|
+
if settings is not None:
|
|
424
|
+
default = _SizeIntent(
|
|
425
|
+
"qty" if settings.mode == "fixed" else "percent",
|
|
426
|
+
settings.value if settings.mode == "fixed" else settings.value * 100,
|
|
427
|
+
"default",
|
|
428
|
+
)
|
|
429
|
+
return select_size(
|
|
430
|
+
qty=qty, amount=amount, percent=percent, default=default, reduce_only=reduce_only
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
@overload
|
|
434
|
+
def buy(
|
|
435
|
+
self,
|
|
436
|
+
*,
|
|
437
|
+
qty: float,
|
|
438
|
+
limit: float | None = None,
|
|
439
|
+
stop: float | None = None,
|
|
440
|
+
sl: float | None = None,
|
|
441
|
+
tp: float | None = None,
|
|
442
|
+
tag: _JsonValue = None,
|
|
443
|
+
reduce_only: bool = False,
|
|
444
|
+
expires_at: _Expiry | None = None,
|
|
445
|
+
valid_bars: int | None = None,
|
|
446
|
+
) -> Order:
|
|
447
|
+
"""Explicit asset quantity; reduce-only is allowed."""
|
|
448
|
+
|
|
449
|
+
@overload
|
|
450
|
+
def buy(
|
|
451
|
+
self,
|
|
452
|
+
*,
|
|
453
|
+
amount: float,
|
|
454
|
+
limit: float | None = None,
|
|
455
|
+
stop: float | None = None,
|
|
456
|
+
sl: float | None = None,
|
|
457
|
+
tp: float | None = None,
|
|
458
|
+
tag: _JsonValue = None,
|
|
459
|
+
reduce_only: Literal[False] = False,
|
|
460
|
+
expires_at: _Expiry | None = None,
|
|
461
|
+
valid_bars: int | None = None,
|
|
462
|
+
) -> Order:
|
|
463
|
+
"""Explicit notional amount; reduce-only is not allowed."""
|
|
464
|
+
|
|
465
|
+
@overload
|
|
466
|
+
def buy(
|
|
467
|
+
self,
|
|
468
|
+
*,
|
|
469
|
+
percent: float,
|
|
470
|
+
limit: float | None = None,
|
|
471
|
+
stop: float | None = None,
|
|
472
|
+
sl: float | None = None,
|
|
473
|
+
tp: float | None = None,
|
|
474
|
+
tag: _JsonValue = None,
|
|
475
|
+
reduce_only: Literal[False] = False,
|
|
476
|
+
expires_at: _Expiry | None = None,
|
|
477
|
+
valid_bars: int | None = None,
|
|
478
|
+
) -> Order:
|
|
479
|
+
"""Explicit equity percentage; reduce-only is not allowed."""
|
|
480
|
+
|
|
481
|
+
@overload
|
|
482
|
+
def buy(
|
|
483
|
+
self,
|
|
484
|
+
*,
|
|
485
|
+
limit: float | None = None,
|
|
486
|
+
stop: float | None = None,
|
|
487
|
+
sl: float | None = None,
|
|
488
|
+
tp: float | None = None,
|
|
489
|
+
tag: _JsonValue = None,
|
|
490
|
+
reduce_only: Literal[False] = False,
|
|
491
|
+
expires_at: _Expiry | None = None,
|
|
492
|
+
valid_bars: int | None = None,
|
|
493
|
+
) -> Order:
|
|
494
|
+
"""Omit all sizing choices to use the configured default."""
|
|
495
|
+
|
|
496
|
+
def buy(
|
|
497
|
+
self,
|
|
498
|
+
*,
|
|
499
|
+
qty: float = cast(float, UNSET),
|
|
500
|
+
amount: float = cast(float, UNSET),
|
|
501
|
+
percent: float = cast(float, UNSET),
|
|
502
|
+
limit: float | None = None,
|
|
503
|
+
stop: float | None = None,
|
|
504
|
+
sl: float | None = None,
|
|
505
|
+
tp: float | None = None,
|
|
506
|
+
tag: _JsonValue = None,
|
|
507
|
+
reduce_only: bool = False,
|
|
508
|
+
expires_at: _Expiry | None = None,
|
|
509
|
+
valid_bars: int | None = None,
|
|
510
|
+
) -> Order:
|
|
511
|
+
"""Submit an incremental order; choose one of qty/amount/percent, where percent=20 means 20% of equity.
|
|
512
|
+
|
|
513
|
+
Omitting all choices uses the default quantity. Amount is a single-currency notional budget excluding fees;
|
|
514
|
+
convert and freeze it using Close at request time, with rejection still possible at execution. Reduce-only requires explicit qty.
|
|
515
|
+
"""
|
|
516
|
+
return self._broker.submit_order(
|
|
517
|
+
self._order_size(qty, amount, percent, reduce_only),
|
|
518
|
+
side="buy",
|
|
519
|
+
limit=limit,
|
|
520
|
+
stop=stop,
|
|
521
|
+
sl=sl,
|
|
522
|
+
tp=tp,
|
|
523
|
+
tag=tag,
|
|
524
|
+
reduce_only=reduce_only,
|
|
525
|
+
expires_at=expires_at,
|
|
526
|
+
valid_bars=valid_bars,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
@overload
|
|
530
|
+
def sell(
|
|
531
|
+
self,
|
|
532
|
+
*,
|
|
533
|
+
qty: float,
|
|
534
|
+
limit: float | None = None,
|
|
535
|
+
stop: float | None = None,
|
|
536
|
+
sl: float | None = None,
|
|
537
|
+
tp: float | None = None,
|
|
538
|
+
tag: _JsonValue = None,
|
|
539
|
+
reduce_only: bool = False,
|
|
540
|
+
expires_at: _Expiry | None = None,
|
|
541
|
+
valid_bars: int | None = None,
|
|
542
|
+
) -> Order:
|
|
543
|
+
"""Explicit asset quantity; reduce-only is allowed."""
|
|
544
|
+
|
|
545
|
+
@overload
|
|
546
|
+
def sell(
|
|
547
|
+
self,
|
|
548
|
+
*,
|
|
549
|
+
amount: float,
|
|
550
|
+
limit: float | None = None,
|
|
551
|
+
stop: float | None = None,
|
|
552
|
+
sl: float | None = None,
|
|
553
|
+
tp: float | None = None,
|
|
554
|
+
tag: _JsonValue = None,
|
|
555
|
+
reduce_only: Literal[False] = False,
|
|
556
|
+
expires_at: _Expiry | None = None,
|
|
557
|
+
valid_bars: int | None = None,
|
|
558
|
+
) -> Order:
|
|
559
|
+
"""Explicit notional amount; reduce-only is not allowed."""
|
|
560
|
+
|
|
561
|
+
@overload
|
|
562
|
+
def sell(
|
|
563
|
+
self,
|
|
564
|
+
*,
|
|
565
|
+
percent: float,
|
|
566
|
+
limit: float | None = None,
|
|
567
|
+
stop: float | None = None,
|
|
568
|
+
sl: float | None = None,
|
|
569
|
+
tp: float | None = None,
|
|
570
|
+
tag: _JsonValue = None,
|
|
571
|
+
reduce_only: Literal[False] = False,
|
|
572
|
+
expires_at: _Expiry | None = None,
|
|
573
|
+
valid_bars: int | None = None,
|
|
574
|
+
) -> Order:
|
|
575
|
+
"""Explicit equity percentage; reduce-only is not allowed."""
|
|
576
|
+
|
|
577
|
+
@overload
|
|
578
|
+
def sell(
|
|
579
|
+
self,
|
|
580
|
+
*,
|
|
581
|
+
limit: float | None = None,
|
|
582
|
+
stop: float | None = None,
|
|
583
|
+
sl: float | None = None,
|
|
584
|
+
tp: float | None = None,
|
|
585
|
+
tag: _JsonValue = None,
|
|
586
|
+
reduce_only: Literal[False] = False,
|
|
587
|
+
expires_at: _Expiry | None = None,
|
|
588
|
+
valid_bars: int | None = None,
|
|
589
|
+
) -> Order:
|
|
590
|
+
"""Omit all sizing choices to use the configured default."""
|
|
591
|
+
|
|
592
|
+
def sell(
|
|
593
|
+
self,
|
|
594
|
+
*,
|
|
595
|
+
qty: float = cast(float, UNSET),
|
|
596
|
+
amount: float = cast(float, UNSET),
|
|
597
|
+
percent: float = cast(float, UNSET),
|
|
598
|
+
limit: float | None = None,
|
|
599
|
+
stop: float | None = None,
|
|
600
|
+
sl: float | None = None,
|
|
601
|
+
tp: float | None = None,
|
|
602
|
+
tag: _JsonValue = None,
|
|
603
|
+
reduce_only: bool = False,
|
|
604
|
+
expires_at: _Expiry | None = None,
|
|
605
|
+
valid_bars: int | None = None,
|
|
606
|
+
) -> Order:
|
|
607
|
+
"""Submit an incremental order; choose one of qty/amount/percent, where percent=20 means 20% of equity.
|
|
608
|
+
|
|
609
|
+
Omitting all choices uses the default quantity. Amount is a single-currency notional budget excluding fees;
|
|
610
|
+
convert and freeze it using Close at request time, with rejection still possible at execution. Reduce-only requires explicit qty.
|
|
611
|
+
"""
|
|
612
|
+
return self._broker.submit_order(
|
|
613
|
+
self._order_size(qty, amount, percent, reduce_only),
|
|
614
|
+
side="sell",
|
|
615
|
+
limit=limit,
|
|
616
|
+
stop=stop,
|
|
617
|
+
sl=sl,
|
|
618
|
+
tp=tp,
|
|
619
|
+
tag=tag,
|
|
620
|
+
reduce_only=reduce_only,
|
|
621
|
+
expires_at=expires_at,
|
|
622
|
+
valid_bars=valid_bars,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
@property
|
|
626
|
+
def account(self) -> AccountSnapshot:
|
|
627
|
+
"""Frozen snapshot of current balance, equity, actual fees, and margin."""
|
|
628
|
+
return self._broker.account
|
|
629
|
+
|
|
630
|
+
@property
|
|
631
|
+
def instrument(self) -> InstrumentSpec:
|
|
632
|
+
"""Asset rules for this run."""
|
|
633
|
+
return self._broker.instrument
|
|
634
|
+
|
|
635
|
+
def close_position(self, portion: float | None = None) -> tuple[Order, ...]:
|
|
636
|
+
"""Exit each lot; return an empty tuple when flat, while self.close remains market data."""
|
|
637
|
+
return self.position.close(portion)
|
|
638
|
+
|
|
639
|
+
def submit_oco(self, *, first: OrderRequest, second: OrderRequest) -> OrderGroup:
|
|
640
|
+
"""Atomically submit two entry requests; validate both first, prioritize first, and return a queryable group."""
|
|
641
|
+
return self._broker.submit_oco(first=first, second=second)
|
|
642
|
+
|
|
643
|
+
def order_target_quantity(
|
|
644
|
+
self,
|
|
645
|
+
*,
|
|
646
|
+
target: float,
|
|
647
|
+
limit: float | None = None,
|
|
648
|
+
stop: float | None = None,
|
|
649
|
+
sl: float | None = None,
|
|
650
|
+
tp: float | None = None,
|
|
651
|
+
tag: _JsonValue = None,
|
|
652
|
+
reduce_only: bool = False,
|
|
653
|
+
expires_at: _Expiry | None = None,
|
|
654
|
+
valid_bars: int | None = None,
|
|
655
|
+
) -> Order | None:
|
|
656
|
+
"""Generate a difference order from the actual net position; always validate conditions and reuse identical active targets.
|
|
657
|
+
|
|
658
|
+
target is the target asset quantity; return None when no order is needed. Only netting mode is supported.
|
|
659
|
+
All arguments must be supplied by name; limit/stop/sl/tp are absolute prices.
|
|
660
|
+
Validate prices, expiration, and tags even with no difference; do not invent a buy/sell direction to check protection relationships.
|
|
661
|
+
"""
|
|
662
|
+
return self._broker.order_target(
|
|
663
|
+
target,
|
|
664
|
+
{
|
|
665
|
+
"limit": limit,
|
|
666
|
+
"stop": stop,
|
|
667
|
+
"sl": sl,
|
|
668
|
+
"tp": tp,
|
|
669
|
+
"tag": tag,
|
|
670
|
+
"reduce_only": reduce_only,
|
|
671
|
+
"expires_at": expires_at,
|
|
672
|
+
"valid_bars": valid_bars,
|
|
673
|
+
},
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
def order_target_value(
|
|
677
|
+
self,
|
|
678
|
+
*,
|
|
679
|
+
target: float,
|
|
680
|
+
limit: float | None = None,
|
|
681
|
+
stop: float | None = None,
|
|
682
|
+
sl: float | None = None,
|
|
683
|
+
tp: float | None = None,
|
|
684
|
+
tag: _JsonValue = None,
|
|
685
|
+
reduce_only: bool = False,
|
|
686
|
+
expires_at: _Expiry | None = None,
|
|
687
|
+
valid_bars: int | None = None,
|
|
688
|
+
) -> Order | None:
|
|
689
|
+
"""Convert target net notional to quantity using Close at request time; explicit conditions match quantity targets."""
|
|
690
|
+
from openbacktesting.broker.models import finite
|
|
691
|
+
|
|
692
|
+
return self.order_target_quantity(
|
|
693
|
+
target=finite(target, "target") / self.close[-1],
|
|
694
|
+
limit=limit,
|
|
695
|
+
stop=stop,
|
|
696
|
+
sl=sl,
|
|
697
|
+
tp=tp,
|
|
698
|
+
tag=tag,
|
|
699
|
+
reduce_only=reduce_only,
|
|
700
|
+
expires_at=expires_at,
|
|
701
|
+
valid_bars=valid_bars,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
def order_target_percent(
|
|
705
|
+
self,
|
|
706
|
+
*,
|
|
707
|
+
target: float,
|
|
708
|
+
limit: float | None = None,
|
|
709
|
+
stop: float | None = None,
|
|
710
|
+
sl: float | None = None,
|
|
711
|
+
tp: float | None = None,
|
|
712
|
+
tag: _JsonValue = None,
|
|
713
|
+
reduce_only: bool = False,
|
|
714
|
+
expires_at: _Expiry | None = None,
|
|
715
|
+
valid_bars: int | None = None,
|
|
716
|
+
) -> Order | None:
|
|
717
|
+
"""Determine target net notional from equity percentage at request time; no continuous rebalancing or guaranteed fill weight."""
|
|
718
|
+
from openbacktesting.broker.models import finite
|
|
719
|
+
|
|
720
|
+
if self.equity <= 0:
|
|
721
|
+
raise ValueError("Target percentage requires positive equity")
|
|
722
|
+
return self.order_target_value(
|
|
723
|
+
target=finite(target, "target") * self.equity,
|
|
724
|
+
limit=limit,
|
|
725
|
+
stop=stop,
|
|
726
|
+
sl=sl,
|
|
727
|
+
tp=tp,
|
|
728
|
+
tag=tag,
|
|
729
|
+
reduce_only=reduce_only,
|
|
730
|
+
expires_at=expires_at,
|
|
731
|
+
valid_bars=valid_bars,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
@property
|
|
735
|
+
def equity(self) -> float:
|
|
736
|
+
"""Current account equity, including cash and position value.
|
|
737
|
+
|
|
738
|
+
Current account equity (cash plus assets)."""
|
|
739
|
+
return self._broker.equity
|
|
740
|
+
|
|
741
|
+
@property
|
|
742
|
+
def data(self) -> _Data:
|
|
743
|
+
"""Currently visible original market-data entry point; full history in init and only the current prefix in next.
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
Price data, roughly as passed into
|
|
747
|
+
`openbacktesting.engine.Backtest.__init__`,
|
|
748
|
+
but with two significant exceptions:
|
|
749
|
+
|
|
750
|
+
* `data` is _not_ a DataFrame, but a custom structure
|
|
751
|
+
that serves customized numpy arrays for reasons of performance
|
|
752
|
+
and convenience. Besides OHLCV columns, `.index` and length,
|
|
753
|
+
it offers `.pip` property, the smallest price unit of change.
|
|
754
|
+
* Within `openbacktesting.engine.Strategy.init`, `data` arrays
|
|
755
|
+
are available in full length, as passed into
|
|
756
|
+
`openbacktesting.engine.Backtest.__init__`
|
|
757
|
+
(for precomputing indicators and such). However, within
|
|
758
|
+
`openbacktesting.engine.Strategy.next`, `data` arrays are
|
|
759
|
+
only as long as the current iteration, simulating gradual
|
|
760
|
+
price point revelation. In each call of
|
|
761
|
+
`openbacktesting.engine.Strategy.next` (iteratively called by
|
|
762
|
+
`openbacktesting.engine.Backtest` internally),
|
|
763
|
+
the last array value (e.g. `data.Close[-1]`)
|
|
764
|
+
is always the **most recent** value.
|
|
765
|
+
* If you need data arrays (e.g. `data.Close`) to be indexed
|
|
766
|
+
**Pandas series**, you can call their `.s` accessor
|
|
767
|
+
(e.g. `data.Close.s`). If you need the whole of data
|
|
768
|
+
as a **DataFrame**, use `.df` accessor (i.e. `data.df`).
|
|
769
|
+
"""
|
|
770
|
+
return self._data
|
|
771
|
+
|
|
772
|
+
@property
|
|
773
|
+
def position(self) -> Position:
|
|
774
|
+
"""Current net-position aggregate, supporting size/profit reads and closure requests.
|
|
775
|
+
|
|
776
|
+
Instance of `openbacktesting.engine.Position`."""
|
|
777
|
+
return self._broker.position
|
|
778
|
+
|
|
779
|
+
@property
|
|
780
|
+
def orders(self) -> tuple[Order, ...]:
|
|
781
|
+
"""Orders awaiting execution; cancel may be called on these orders.
|
|
782
|
+
|
|
783
|
+
List of orders (see `Order`) waiting for execution."""
|
|
784
|
+
return tuple(self._broker.orders)
|
|
785
|
+
|
|
786
|
+
@property
|
|
787
|
+
def trades(self) -> tuple[Trade, ...]:
|
|
788
|
+
"""Currently held trades, distinct from settled closed_trades.
|
|
789
|
+
|
|
790
|
+
List of active trades (see `Trade`)."""
|
|
791
|
+
return tuple(self._broker.trades)
|
|
792
|
+
|
|
793
|
+
@property
|
|
794
|
+
def closed_trade_stats(self) -> ClosedTradeStats:
|
|
795
|
+
"""Read frozen statistics for closed portions, including allocated fees and excluding unrealized profit or loss.
|
|
796
|
+
|
|
797
|
+
Reads do not scan history; available throughout the lifecycle and after the run ends.
|
|
798
|
+
same_close results are not visible before execution; saved values do not change with later fills.
|
|
799
|
+
"""
|
|
800
|
+
return self._broker._closed_stats
|
|
801
|
+
|
|
802
|
+
@property
|
|
803
|
+
def closed_trades(self) -> tuple[Trade, ...]:
|
|
804
|
+
"""Completed trades for individual-trade analysis.
|
|
805
|
+
|
|
806
|
+
List of settled trades (see `Trade`)."""
|
|
807
|
+
return tuple(self._broker.closed_trades)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
_set_legacy_module(Strategy, "openbacktesting.engine")
|