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,62 @@
|
|
|
1
|
+
"""Independent deterministic OHLC fill-price policy."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from openbacktesting.broker.models import finite
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Slippage:
|
|
10
|
+
"""No slippage, fixed-price slippage, or basis-point slippage; apply once per fill."""
|
|
11
|
+
|
|
12
|
+
mode: str = "none"
|
|
13
|
+
value: float = 0
|
|
14
|
+
|
|
15
|
+
def __post_init__(self):
|
|
16
|
+
if self.mode not in ("none", "fixed", "bps"):
|
|
17
|
+
raise ValueError("slippage mode must be none/fixed/bps")
|
|
18
|
+
object.__setattr__(self, "value", finite(self.value, "slippage"))
|
|
19
|
+
if self.value < 0 or (self.mode == "none" and self.value != 0):
|
|
20
|
+
raise ValueError("Invalid slippage value")
|
|
21
|
+
|
|
22
|
+
def cost(self, price: float) -> float:
|
|
23
|
+
"""Return a nonnegative slippage distance per unit."""
|
|
24
|
+
if self.mode == "fixed":
|
|
25
|
+
return self.value
|
|
26
|
+
return price * self.value / 10000 if self.mode == "bps" else 0.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def match_price(request, *, triggered, open_, high, low):
|
|
30
|
+
"""Return the candidate reference price and trigger state without modifying the request."""
|
|
31
|
+
buy = request.side == "buy"
|
|
32
|
+
stop = request.stop if not triggered else None
|
|
33
|
+
if stop is not None:
|
|
34
|
+
if not (high >= stop if buy else low <= stop):
|
|
35
|
+
return None, False
|
|
36
|
+
triggered = True
|
|
37
|
+
if request.limit is not None:
|
|
38
|
+
limit = request.limit
|
|
39
|
+
if not (low <= limit if buy else high >= limit):
|
|
40
|
+
return None, triggered
|
|
41
|
+
if stop is not None and (limit <= stop if buy else limit >= stop):
|
|
42
|
+
return None, triggered
|
|
43
|
+
return (min(stop or open_, limit) if buy else max(stop or open_, limit)), triggered
|
|
44
|
+
return ((max(open_, stop) if buy else min(open_, stop)) if stop else open_), triggered
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def execution_price(base, side, *, spread, slippage, instrument, limit, bounds=None):
|
|
48
|
+
"""Apply costs and the grid before checking the limit; ordinary execution is constrained to the OHLC range."""
|
|
49
|
+
sign = 1 if side == "buy" else -1
|
|
50
|
+
price = base * (1 + sign * spread) + sign * slippage.cost(base)
|
|
51
|
+
price = instrument.price(price, side=side)
|
|
52
|
+
if limit is not None and (price > limit if side == "buy" else price < limit):
|
|
53
|
+
return None
|
|
54
|
+
if bounds is not None:
|
|
55
|
+
low, high = bounds
|
|
56
|
+
price = min(max(price, low), high)
|
|
57
|
+
price = instrument.price(price, side=side)
|
|
58
|
+
if not low <= price <= high:
|
|
59
|
+
return None
|
|
60
|
+
if limit is not None and (price > limit if side == "buy" else price < limit):
|
|
61
|
+
return None
|
|
62
|
+
return price
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Explicit asset units; trading rules are not inferred from market-data decimal places."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from decimal import ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_UP, Decimal, localcontext
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from openbacktesting.broker.models import finite
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class InstrumentSpec:
|
|
13
|
+
"""Actual asset quantity and price grids; currency is only a label."""
|
|
14
|
+
|
|
15
|
+
symbol: str = "asset"
|
|
16
|
+
quantity_step: float = 1
|
|
17
|
+
min_quantity: float | None = None
|
|
18
|
+
price_tick: float | None = None
|
|
19
|
+
min_notional: float = 0
|
|
20
|
+
currency: str | None = None
|
|
21
|
+
|
|
22
|
+
def __post_init__(self):
|
|
23
|
+
if not isinstance(self.symbol, str) or not self.symbol:
|
|
24
|
+
raise ValueError("symbol must be a nonempty string")
|
|
25
|
+
if self.currency is not None and not isinstance(self.currency, str):
|
|
26
|
+
raise TypeError("currency must be a string or None")
|
|
27
|
+
step = finite(self.quantity_step, "quantity_step", positive=True)
|
|
28
|
+
minimum = (
|
|
29
|
+
step
|
|
30
|
+
if self.min_quantity is None
|
|
31
|
+
else finite(self.min_quantity, "min_quantity", positive=True)
|
|
32
|
+
)
|
|
33
|
+
object.__setattr__(self, "quantity_step", step)
|
|
34
|
+
object.__setattr__(self, "min_quantity", minimum)
|
|
35
|
+
if self.price_tick is not None:
|
|
36
|
+
object.__setattr__(
|
|
37
|
+
self, "price_tick", finite(self.price_tick, "price_tick", positive=True)
|
|
38
|
+
)
|
|
39
|
+
object.__setattr__(self, "min_notional", finite(self.min_notional, "min_notional"))
|
|
40
|
+
if self.min_notional < 0:
|
|
41
|
+
raise ValueError("min_notional must not be negative")
|
|
42
|
+
|
|
43
|
+
def quantity(self, value, *, floor=False, opening=True) -> float:
|
|
44
|
+
"""Direct requests must lie on the grid; budgeted quantities may round toward zero."""
|
|
45
|
+
value = finite(value, "qty")
|
|
46
|
+
if value < 0:
|
|
47
|
+
raise ValueError("qty must not be negative")
|
|
48
|
+
lots = Decimal(str(value)) / Decimal(str(self.quantity_step))
|
|
49
|
+
whole = lots.to_integral_value(rounding=ROUND_DOWN)
|
|
50
|
+
if not floor and lots != whole:
|
|
51
|
+
raise ValueError("qty is not on the quantity_step grid")
|
|
52
|
+
result = float(whole * Decimal(str(self.quantity_step)))
|
|
53
|
+
if opening and result < self.min_quantity:
|
|
54
|
+
if floor:
|
|
55
|
+
return 0.0
|
|
56
|
+
raise ValueError("qty is below min_quantity")
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
def price(self, value, *, side=None) -> float:
|
|
60
|
+
"""Validate requested prices strictly; normalize execution prices in the adverse direction."""
|
|
61
|
+
value = finite(value, "price", positive=True)
|
|
62
|
+
if self.price_tick is None:
|
|
63
|
+
return value
|
|
64
|
+
ticks = Decimal(str(value)) / Decimal(str(self.price_tick))
|
|
65
|
+
rounded = ticks.to_integral_value(rounding=ROUND_CEILING if side == "buy" else ROUND_FLOOR)
|
|
66
|
+
if side is None and ticks != rounded:
|
|
67
|
+
raise ValueError("price is not on the price_tick grid")
|
|
68
|
+
return float(rounded * Decimal(str(self.price_tick)))
|
|
69
|
+
|
|
70
|
+
def validate_data(self, frame):
|
|
71
|
+
"""Trading market data must be ordered, unique, and have valid prices; explicit ticks must not be silently corrected."""
|
|
72
|
+
if not frame.index.is_monotonic_increasing or not frame.index.is_unique:
|
|
73
|
+
raise ValueError("Market-data index must be increasing and unique")
|
|
74
|
+
values = frame[["Open", "High", "Low", "Close"]].to_numpy(dtype=float)
|
|
75
|
+
if not np.isfinite(values).all() or (values <= 0).any():
|
|
76
|
+
raise ValueError("OHLC must contain finite positive prices")
|
|
77
|
+
if self.price_tick is not None:
|
|
78
|
+
for price in np.unique(values):
|
|
79
|
+
self.price(float(price))
|
|
80
|
+
|
|
81
|
+
def round_price_to_tick(self, value: float) -> float:
|
|
82
|
+
"""Round prices to the nearest explicit tick; half ticks round to the higher price.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
value: Finite positive price; booleans are not accepted.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Finite positive float passing this instrument's strict grid validation; does not change matching-rounding policy.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
TypeError: value is nonnumeric or a boolean.
|
|
92
|
+
ValueError: Missing tick, invalid input, zero result, or a valid grid value cannot be represented as a float.
|
|
93
|
+
"""
|
|
94
|
+
value = finite(value, "price", positive=True)
|
|
95
|
+
if self.price_tick is None:
|
|
96
|
+
raise ValueError("round_price_to_tick requires an explicit price_tick")
|
|
97
|
+
# Decimal exponents of two finite floats can differ by more than 600; prevent default precision from losing half ticks.
|
|
98
|
+
with localcontext() as context:
|
|
99
|
+
context.prec = 700
|
|
100
|
+
tick = Decimal(str(self.price_tick))
|
|
101
|
+
ticks = (Decimal(str(value)) / tick).to_integral_value(rounding=ROUND_HALF_UP)
|
|
102
|
+
exact = ticks * tick
|
|
103
|
+
result = float(exact)
|
|
104
|
+
if Decimal(str(result)) != exact:
|
|
105
|
+
raise ValueError("Rounded price cannot represent the grid exactly as a float")
|
|
106
|
+
return self.price(result)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Balance, actual fees, and position snapshots independent of strategies and result tables."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.broker.models import AccountSnapshot, PositionSnapshot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TradingLedger:
|
|
7
|
+
"""Record only actual executions; candidate calculations must not call apply."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, balance):
|
|
10
|
+
self.initial_balance = float(balance)
|
|
11
|
+
self.balance = float(balance)
|
|
12
|
+
self.realized_pnl_gross = 0.0
|
|
13
|
+
self.fees_paid = 0.0
|
|
14
|
+
|
|
15
|
+
def apply(self, realized_pnl_gross, fee):
|
|
16
|
+
self.realized_pnl_gross += realized_pnl_gross
|
|
17
|
+
self.fees_paid += fee
|
|
18
|
+
self.balance = self.initial_balance + self.realized_pnl_gross - self.fees_paid
|
|
19
|
+
|
|
20
|
+
def position(self, trades, price):
|
|
21
|
+
long_qty = sum(t.size for t in trades if t.size > 0)
|
|
22
|
+
short_qty = sum(-t.size for t in trades if t.size < 0)
|
|
23
|
+
long_cost = sum(t.size * t.entry_price for t in trades if t.size > 0)
|
|
24
|
+
short_cost = sum(-t.size * t.entry_price for t in trades if t.size < 0)
|
|
25
|
+
long_avg = long_cost / long_qty if long_qty else None
|
|
26
|
+
short_avg = short_cost / short_qty if short_qty else None
|
|
27
|
+
average = None if long_qty and short_qty else long_avg or short_avg
|
|
28
|
+
gross_pl = sum(t.size * (price - t.entry_price) for t in trades)
|
|
29
|
+
fees = sum(t._entry_commission for t in trades)
|
|
30
|
+
cost = long_cost + short_cost
|
|
31
|
+
return PositionSnapshot(
|
|
32
|
+
long_qty - short_qty,
|
|
33
|
+
long_qty + short_qty,
|
|
34
|
+
long_qty,
|
|
35
|
+
short_qty,
|
|
36
|
+
long_avg,
|
|
37
|
+
short_avg,
|
|
38
|
+
average,
|
|
39
|
+
(long_qty + short_qty) * price,
|
|
40
|
+
(long_qty - short_qty) * price,
|
|
41
|
+
gross_pl,
|
|
42
|
+
gross_pl - fees,
|
|
43
|
+
fees,
|
|
44
|
+
(gross_pl - fees) / cost if cost and average is not None else None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def account(self, position, margin):
|
|
48
|
+
equity = self.balance + position.unrealized_pnl_gross
|
|
49
|
+
used = position.gross_notional * margin
|
|
50
|
+
return AccountSnapshot(
|
|
51
|
+
self.balance,
|
|
52
|
+
equity,
|
|
53
|
+
self.realized_pnl_gross,
|
|
54
|
+
position.unrealized_pnl_gross,
|
|
55
|
+
self.fees_paid,
|
|
56
|
+
used,
|
|
57
|
+
max(0.0, equity - used),
|
|
58
|
+
position.gross_notional,
|
|
59
|
+
position.net_notional,
|
|
60
|
+
)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Trade requests, events, and accounting snapshots independent of strategies and execution loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, fields
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from functools import cache
|
|
9
|
+
from math import isfinite
|
|
10
|
+
from numbers import Real
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def finite(value, name: str, *, positive=False) -> float:
|
|
15
|
+
"""Reject booleans, nonnumeric values, and nonfinite values; return a normalized number."""
|
|
16
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
17
|
+
raise TypeError(f"{name} must be a finite number")
|
|
18
|
+
value = float(value)
|
|
19
|
+
if not isfinite(value) or (positive and value <= 0):
|
|
20
|
+
raise ValueError(f"{name} must be a {'positive ' if positive else ''}finite number")
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OrderStatus(StrEnum):
|
|
25
|
+
"""Observable order states; terminal states cannot reopen."""
|
|
26
|
+
|
|
27
|
+
SUBMITTED = "Submitted"
|
|
28
|
+
ACCEPTED = "Accepted"
|
|
29
|
+
PARTIALLY_FILLED = "PartiallyFilled"
|
|
30
|
+
FILLED = "Filled"
|
|
31
|
+
CANCELED = "Canceled"
|
|
32
|
+
EXPIRED = "Expired"
|
|
33
|
+
REJECTED = "Rejected"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def terminal(self) -> bool:
|
|
37
|
+
"""Whether the state is terminal."""
|
|
38
|
+
return self in (self.FILLED, self.CANCELED, self.EXPIRED, self.REJECTED)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ClosedTradeStats:
|
|
43
|
+
"""Cumulative closed portions as of the read; fees are allocated, and unrealized profit or loss is excluded.
|
|
44
|
+
|
|
45
|
+
Count closed portions rather than trade_id values; classify wins and losses by strictly positive or negative net profit or loss.
|
|
46
|
+
losing_amount is a nonnegative loss amount; average net profit or loss and the 0–1 win rate are None for empty records.
|
|
47
|
+
Values contain no runtime objects; saved snapshots are unaffected by later fills.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
closed_count: int = 0
|
|
51
|
+
win_count: int = 0
|
|
52
|
+
loss_count: int = 0
|
|
53
|
+
even_count: int = 0
|
|
54
|
+
winning_profit: float = 0.0
|
|
55
|
+
losing_amount: float = 0.0
|
|
56
|
+
net_profit: float = 0.0
|
|
57
|
+
average_profit: float | None = None
|
|
58
|
+
win_rate: float | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class OrderSizingSnapshot:
|
|
63
|
+
"""Sizing basis frozen at submission; budgets do not guarantee actual fill notional or fees.
|
|
64
|
+
|
|
65
|
+
mode is qty/amount/percent, source is explicit/default; percent value
|
|
66
|
+
uses percentage points (20 means 20%), and budget is None in qty mode.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
mode: Literal["qty", "amount", "percent"]
|
|
70
|
+
source: Literal["explicit", "default"]
|
|
71
|
+
value: float
|
|
72
|
+
equity: float
|
|
73
|
+
reference_price: float
|
|
74
|
+
budget: float | None
|
|
75
|
+
qty: float
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class NormalizedOrderRequest:
|
|
80
|
+
"""Immutable normalized request; tag is stored as JSON text, and reads return independent plain values."""
|
|
81
|
+
|
|
82
|
+
side: str
|
|
83
|
+
qty: float
|
|
84
|
+
limit: float | None = None
|
|
85
|
+
stop: float | None = None
|
|
86
|
+
sl: float | None = None
|
|
87
|
+
tp: float | None = None
|
|
88
|
+
reduce_only: bool = False
|
|
89
|
+
trade_id: str | None = None
|
|
90
|
+
expires_at: object = None
|
|
91
|
+
valid_bars: int | None = None
|
|
92
|
+
tag_json: str = "null"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def tag(self):
|
|
96
|
+
"""Return an independent tag copy; modifying it does not change the request."""
|
|
97
|
+
return json.loads(self.tag_json)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class OrderSnapshot:
|
|
102
|
+
"""Frozen order values at a state transition."""
|
|
103
|
+
|
|
104
|
+
order_id: str
|
|
105
|
+
status: OrderStatus
|
|
106
|
+
filled_qty: float
|
|
107
|
+
remaining_qty: float
|
|
108
|
+
avg_fill_price: float | None
|
|
109
|
+
reason: str | None
|
|
110
|
+
triggered: bool
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class OrderEvent:
|
|
115
|
+
"""Immutable order event with time and phase."""
|
|
116
|
+
|
|
117
|
+
sequence: int
|
|
118
|
+
bar_index: int
|
|
119
|
+
timestamp: object
|
|
120
|
+
phase: str
|
|
121
|
+
snapshot: OrderSnapshot
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class Fill:
|
|
126
|
+
"""An actual execution; fees are recorded exactly once here."""
|
|
127
|
+
|
|
128
|
+
fill_id: str
|
|
129
|
+
order_id: str
|
|
130
|
+
sequence: int
|
|
131
|
+
side: str
|
|
132
|
+
qty: float
|
|
133
|
+
price: float
|
|
134
|
+
fee: float
|
|
135
|
+
bar_index: int
|
|
136
|
+
timestamp: object
|
|
137
|
+
phase: str
|
|
138
|
+
reason: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class TradeAllocation:
|
|
143
|
+
"""Allocation of an execution quantity to entry lots or closed portions."""
|
|
144
|
+
|
|
145
|
+
allocation_id: str
|
|
146
|
+
fill_id: str
|
|
147
|
+
trade_id: str
|
|
148
|
+
entry_fill_id: str
|
|
149
|
+
kind: str
|
|
150
|
+
qty: float
|
|
151
|
+
fee: float
|
|
152
|
+
entry_fee: float
|
|
153
|
+
realized_pnl_gross: float
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class AccountSnapshot:
|
|
158
|
+
"""Single-currency linear margin account; balance is not securities settlement cash."""
|
|
159
|
+
|
|
160
|
+
balance: float
|
|
161
|
+
equity: float
|
|
162
|
+
realized_pnl_gross: float
|
|
163
|
+
unrealized_pnl_gross: float
|
|
164
|
+
fees_paid: float
|
|
165
|
+
margin_used: float
|
|
166
|
+
margin_available: float
|
|
167
|
+
gross_notional: float
|
|
168
|
+
net_notional: float
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class PositionSnapshot:
|
|
173
|
+
"""Net position, two-sided gross exposure, and explicit gross and net profit or loss."""
|
|
174
|
+
|
|
175
|
+
net_qty: float
|
|
176
|
+
gross_qty: float
|
|
177
|
+
long_qty: float
|
|
178
|
+
short_qty: float
|
|
179
|
+
long_avg_price: float | None
|
|
180
|
+
short_avg_price: float | None
|
|
181
|
+
avg_price: float | None
|
|
182
|
+
gross_notional: float
|
|
183
|
+
net_notional: float
|
|
184
|
+
unrealized_pnl_gross: float
|
|
185
|
+
unrealized_pnl_net: float
|
|
186
|
+
entry_fees_remaining: float
|
|
187
|
+
return_fraction: float | None
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def has_exposure(self) -> bool:
|
|
191
|
+
"""A two-sided net-zero position still has gross exposure."""
|
|
192
|
+
return self.gross_qty > 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class RunSummary:
|
|
197
|
+
"""Frozen summary for the finish hook; error text does not replace the original exception."""
|
|
198
|
+
|
|
199
|
+
reason: str
|
|
200
|
+
account: AccountSnapshot
|
|
201
|
+
error: str | None = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def record(value) -> dict:
|
|
205
|
+
"""Produce a value-only dictionary without exposing live trading objects."""
|
|
206
|
+
names = _field_names(type(value))
|
|
207
|
+
return dict(zip(names, map(value.__dict__.__getitem__, names)))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def columns(model) -> list[str]:
|
|
211
|
+
"""Empty result tables retain model fields."""
|
|
212
|
+
return list(_field_names(model))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@cache
|
|
216
|
+
def _field_names(model):
|
|
217
|
+
return tuple(item.name for item in fields(model))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def freeze_tag(value) -> str:
|
|
221
|
+
"""Freeze JSON-safe tags as text; reject nonstring keys and nonfinite numbers."""
|
|
222
|
+
|
|
223
|
+
def validate(item):
|
|
224
|
+
if isinstance(item, dict):
|
|
225
|
+
if not all(isinstance(key, str) for key in item):
|
|
226
|
+
raise TypeError("tag mapping keys must be strings")
|
|
227
|
+
for child in item.values():
|
|
228
|
+
validate(child)
|
|
229
|
+
elif isinstance(item, list):
|
|
230
|
+
for child in item:
|
|
231
|
+
validate(child)
|
|
232
|
+
elif item is not None and not isinstance(item, (str, bool, int, float)):
|
|
233
|
+
raise TypeError("tag supports only plain JSON values")
|
|
234
|
+
|
|
235
|
+
validate(value)
|
|
236
|
+
return json.dumps(value, ensure_ascii=False, allow_nan=False, sort_keys=True)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Read-only execution observations: independent sequence, effective price ranges, and lot extrema."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.broker.models import record
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _update_extrema(state, bar_index, favorable, adverse):
|
|
7
|
+
# Initialize at zero; strict comparisons exclude negative magnitudes and preserve the first position of tied extrema.
|
|
8
|
+
if favorable > state["mfe"]:
|
|
9
|
+
state["mfe"], state["mfeBar"] = favorable, bar_index
|
|
10
|
+
if adverse > state["mae"]:
|
|
11
|
+
state["mae"], state["maeBar"] = adverse, bar_index
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Observations:
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.rows = []
|
|
17
|
+
self.lots = {}
|
|
18
|
+
self.closed = {}
|
|
19
|
+
self.changed_bar = -1
|
|
20
|
+
|
|
21
|
+
def sample(
|
|
22
|
+
self,
|
|
23
|
+
broker,
|
|
24
|
+
price,
|
|
25
|
+
phase,
|
|
26
|
+
*,
|
|
27
|
+
ordered=True,
|
|
28
|
+
coverage="complete_under_bar_model",
|
|
29
|
+
facts=None,
|
|
30
|
+
range_eligible=False,
|
|
31
|
+
):
|
|
32
|
+
if facts is None:
|
|
33
|
+
position = broker.ledger.position(broker.trades, price)
|
|
34
|
+
facts = record(broker.ledger.account(position, broker.margin)) | record(position)
|
|
35
|
+
row = {
|
|
36
|
+
"observationId": f"observation-{len(self.rows) + 1}",
|
|
37
|
+
"observationSequence": len(self.rows) + 1,
|
|
38
|
+
"barIndex": broker._i,
|
|
39
|
+
"phase": phase,
|
|
40
|
+
"price": price,
|
|
41
|
+
"fillSequence": broker.fills[-1].sequence if broker.fills else 0,
|
|
42
|
+
"ordered": ordered,
|
|
43
|
+
"coverage": coverage,
|
|
44
|
+
"rangeEligible": range_eligible,
|
|
45
|
+
**facts,
|
|
46
|
+
}
|
|
47
|
+
self.rows.append(row)
|
|
48
|
+
return row
|
|
49
|
+
|
|
50
|
+
def mark(self, broker, trade, price, *, partial=False):
|
|
51
|
+
state = self.lots[trade.trade_id]
|
|
52
|
+
value = (price - trade.entry_price) * (1 if trade.size > 0 else -1)
|
|
53
|
+
_update_extrema(state, broker._i, value, -value)
|
|
54
|
+
state["partial"] |= partial
|
|
55
|
+
|
|
56
|
+
def before(self, broker, price, phase, conditional):
|
|
57
|
+
self.sample(
|
|
58
|
+
broker,
|
|
59
|
+
price,
|
|
60
|
+
"before_" + phase,
|
|
61
|
+
coverage="partial" if conditional else "complete_under_bar_model",
|
|
62
|
+
)
|
|
63
|
+
for trade in broker.trades:
|
|
64
|
+
self.mark(broker, trade, price, partial=conditional)
|
|
65
|
+
if conditional or phase == "close":
|
|
66
|
+
self.changed_bar = broker._i
|
|
67
|
+
|
|
68
|
+
def opened(self, broker, trade, conditional, phase):
|
|
69
|
+
self.lots[trade.trade_id] = {
|
|
70
|
+
"mfe": 0.0,
|
|
71
|
+
"mae": 0.0,
|
|
72
|
+
"mfeBar": broker._i,
|
|
73
|
+
"maeBar": broker._i,
|
|
74
|
+
"partial": conditional,
|
|
75
|
+
"firstFullBar": broker._i + int(conditional or phase != "execute"),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def close(self, trade, allocation_id):
|
|
79
|
+
self.closed[allocation_id] = self.lots[trade.trade_id].copy()
|
|
80
|
+
|
|
81
|
+
def mark_bar(self, broker):
|
|
82
|
+
high, low = (broker._data._current_value(n) for n in ("High", "Low"))
|
|
83
|
+
if high < low:
|
|
84
|
+
high, low = low, high
|
|
85
|
+
for trade in broker.trades:
|
|
86
|
+
state = self.lots[trade.trade_id]
|
|
87
|
+
if state["firstFullBar"] <= broker._i:
|
|
88
|
+
entry = trade.entry_price
|
|
89
|
+
if trade.size > 0:
|
|
90
|
+
favorable, adverse = high - entry, entry - low
|
|
91
|
+
else:
|
|
92
|
+
favorable, adverse = entry - low, high - entry
|
|
93
|
+
_update_extrema(state, broker._i, favorable, adverse)
|
|
94
|
+
else:
|
|
95
|
+
self.mark(broker, trade, broker.last_price)
|
|
96
|
+
|
|
97
|
+
def bar(self, broker):
|
|
98
|
+
self.mark_bar(broker)
|
|
99
|
+
self.sample(broker, broker.last_price, "bar", range_eligible=self.changed_bar != broker._i)
|