trigerr 0.7.0__tar.gz → 0.7.2__tar.gz
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.
- {trigerr-0.7.0/trigerr.egg-info → trigerr-0.7.2}/PKG-INFO +17 -13
- trigerr-0.7.2/README.md +58 -0
- trigerr-0.7.2/tests/test_bt_orders.py +109 -0
- trigerr-0.7.2/tests/test_exit_mutex.py +183 -0
- trigerr-0.7.2/tests/test_ledger_contract.py +33 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_live_orders_correlation.py +56 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/__init__.py +1 -1
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/__init__.py +1 -0
- trigerr-0.7.2/trigerr/framework/bt_state.py +44 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/compiler.py +8 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/data_feeds.py +7 -23
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/execution_core.py +203 -39
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/point_in_time.py +1 -2
- trigerr-0.7.2/trigerr/orders/backtest.py +115 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/orders/live.py +35 -10
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/orders/orders_utils.py +17 -0
- {trigerr-0.7.0 → trigerr-0.7.2/trigerr.egg-info}/PKG-INFO +17 -13
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr.egg-info/SOURCES.txt +3 -0
- trigerr-0.7.0/README.md +0 -54
- trigerr-0.7.0/tests/test_exit_mutex.py +0 -100
- trigerr-0.7.0/trigerr/orders/backtest.py +0 -56
- {trigerr-0.7.0 → trigerr-0.7.2}/LICENSE +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/pyproject.toml +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/setup.cfg +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_crypto_utils.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_data_market.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_historical_client_config.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_logging_utils.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_orders_schema.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_package.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_reference_symbol_details.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_schema_parity.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/tests/test_symbols.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/config.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/crypto_utils.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/custom_indicators.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/data/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/data/historical.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/data/market.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/data/reference.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/candle_transforms.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/catalog.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/clock.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/exit_engine.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/expressions.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/instrument.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/position_sizing.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/framework/tick_sources.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/jobs.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/orders/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/orders/virtual.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/symbols.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/__main__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/_typing.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/candle/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/candle/cdl_doji.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/candle/cdl_inside.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/candle/cdl_pattern.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/candle/cdl_z.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/candle/ha.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/core.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/custom.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/cycle/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/cycle/ebsw.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/cycle/reflex.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/ma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/maps.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/ao.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/apo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/bias.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/bop.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/brar.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/cci.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/cfo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/cg.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/cmo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/coppock.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/crsi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/cti.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/dm.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/er.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/eri.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/exhc.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/fisher.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/inertia.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/kdj.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/kst.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/macd.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/mom.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/pgo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/ppo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/psl.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/qqe.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/roc.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/rsi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/rsx.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/rvgi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/slope.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/smc.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/smi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/squeeze.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/squeeze_pro.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/stc.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/stoch.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/stochf.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/stochrsi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/tmo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/trix.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/tsi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/uo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/momentum/willr.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/alligator.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/alma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/dema.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/ema.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/fwma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/hilo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/hl2.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/hlc3.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/hma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/hwma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/ichimoku.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/jma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/kama.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/linreg.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/mama.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/mcgd.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/midpoint.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/midprice.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/ohlc4.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/pivots.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/pwma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/rma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/sinwma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/sma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/smma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/ssf.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/ssf3.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/supertrend.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/swma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/t3.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/tema.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/trima.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/vidya.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/wcp.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/wma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/overlap/zlma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/performance/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/performance/drawdown.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/performance/log_return.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/performance/percent_return.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/py.typed +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/entropy.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/kurtosis.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/mad.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/median.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/quantile.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/skew.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/stdev.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/tos_stdevall.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/variance.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/statistics/zscore.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/adx.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/alphatrend.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/amat.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/aroon.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/chop.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/cksp.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/decay.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/decreasing.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/dpo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/ht_trendline.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/increasing.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/long_run.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/psar.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/qstick.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/rwi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/short_run.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/trendflex.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/ttm_trend.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/vhf.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/vortex.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/trend/zigzag.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_candles.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_core.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_math.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_numba.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_signals.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_study.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_time.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/utils/_validate.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/aberration.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/accbands.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/atr.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/atrts.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/bbands.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/chandelier_exit.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/donchian.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/hwc.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/kc.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/massi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/natr.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/pdist.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/rvi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/thermo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/true_range.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volatility/ui.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/__init__.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/ad.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/adosc.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/aobv.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/cmf.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/efi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/eom.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/kvo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/mfi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/nvi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/obv.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/pvi.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/pvo.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/pvol.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/pvr.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/pvt.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/tsv.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/vhm.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/vp.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/vwap.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/ta/volume/vwma.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr/utils.py +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr.egg-info/dependency_links.txt +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr.egg-info/requires.txt +0 -0
- {trigerr-0.7.0 → trigerr-0.7.2}/trigerr.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trigerr
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Python tools for quantitative trading research and analysis.
|
|
5
5
|
Author: Anurag Singh Kushwah
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,18 +31,19 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
# Trigerr
|
|
33
33
|
|
|
34
|
-
Trigerr is a Python package for
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
Trigerr is a Python package for algorithmic trading — technical indicators,
|
|
35
|
+
OHLCV utilities, market-structure analysis, P&L calculations, reports,
|
|
36
|
+
in-memory backtesting, and network-connected data and order execution
|
|
37
|
+
(historical/live market data, backtest/virtual/live order engines, a
|
|
38
|
+
canonical-AST strategy framework). It is the successor to an earlier internal
|
|
39
|
+
package.
|
|
39
40
|
|
|
40
41
|
## Status
|
|
41
42
|
|
|
42
|
-
Trigerr is
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
Trigerr is at v0.7.1 and in production use on the Trigerr white-label trading
|
|
44
|
+
platform (live, virtual and backtest order flow; live market data over Redis
|
|
45
|
+
Streams). The public API is versioned but still evolving — pin a version in
|
|
46
|
+
production and read the changelog before upgrading.
|
|
46
47
|
|
|
47
48
|
Trigerr supports Python 3.10 and newer.
|
|
48
49
|
|
|
@@ -76,9 +77,12 @@ Do not use real trading credentials in examples, tests, or committed files.
|
|
|
76
77
|
|
|
77
78
|
## Roadmap
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
Near-term work is on the platform side rather than the SDK's own shape:
|
|
81
|
+
onboarding more brokers to live order execution (Dhan, then Upstox), and
|
|
82
|
+
extending the canonical-AST strategy framework (`trigerr.framework`) to more
|
|
83
|
+
strategy families. See `PLATFORM_TARGET_ARCHITECTURE.md` in the
|
|
84
|
+
`trigerr-trading-strategies` repo for the platform-wide plan this SDK
|
|
85
|
+
implements.
|
|
82
86
|
|
|
83
87
|
## License
|
|
84
88
|
|
trigerr-0.7.2/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Trigerr
|
|
2
|
+
|
|
3
|
+
Trigerr is a Python package for algorithmic trading — technical indicators,
|
|
4
|
+
OHLCV utilities, market-structure analysis, P&L calculations, reports,
|
|
5
|
+
in-memory backtesting, and network-connected data and order execution
|
|
6
|
+
(historical/live market data, backtest/virtual/live order engines, a
|
|
7
|
+
canonical-AST strategy framework). It is the successor to an earlier internal
|
|
8
|
+
package.
|
|
9
|
+
|
|
10
|
+
## Status
|
|
11
|
+
|
|
12
|
+
Trigerr is at v0.7.1 and in production use on the Trigerr white-label trading
|
|
13
|
+
platform (live, virtual and backtest order flow; live market data over Redis
|
|
14
|
+
Streams). The public API is versioned but still evolving — pin a version in
|
|
15
|
+
production and read the changelog before upgrading.
|
|
16
|
+
|
|
17
|
+
Trigerr supports Python 3.10 and newer.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install trigerr
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For local development from a checkout:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
python -m pytest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Current configuration API
|
|
33
|
+
|
|
34
|
+
The existing configuration helpers remain available while the integration
|
|
35
|
+
layer is migrated:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import trigerr
|
|
39
|
+
|
|
40
|
+
trigerr.set_api_key("your-api-key")
|
|
41
|
+
trigerr.set_data_url("https://data.example.com/")
|
|
42
|
+
trigerr.set_orders_url("https://orders.example.com/")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Do not use real trading credentials in examples, tests, or committed files.
|
|
46
|
+
|
|
47
|
+
## Roadmap
|
|
48
|
+
|
|
49
|
+
Near-term work is on the platform side rather than the SDK's own shape:
|
|
50
|
+
onboarding more brokers to live order execution (Dhan, then Upstox), and
|
|
51
|
+
extending the canonical-AST strategy framework (`trigerr.framework`) to more
|
|
52
|
+
strategy families. See `PLATFORM_TARGET_ARCHITECTURE.md` in the
|
|
53
|
+
`trigerr-trading-strategies` repo for the platform-wide plan this SDK
|
|
54
|
+
implements.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
Trigerr is released under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
""" The two pieces a backtest stands on: the in-memory state cursor and the bt order document.
|
|
2
|
+
|
|
3
|
+
A backtest's order must be the vt order — same keys, same stringification, read back through the same
|
|
4
|
+
orders list — because one execution core serves vt, lt and bt. It may differ only where a backtest
|
|
5
|
+
must: stamped from the replayed candle, no database, no alert, no swallowed errors.
|
|
6
|
+
"""
|
|
7
|
+
import datetime
|
|
8
|
+
import types
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import trigerr.orders.virtual as virtual
|
|
13
|
+
import trigerr.utils as utils
|
|
14
|
+
from trigerr.framework.bt_state import memory_state_cursor
|
|
15
|
+
from trigerr.orders.backtest import place_bt_leg_order
|
|
16
|
+
from trigerr.orders.orders_utils import acquire_exit_mutex, fetch_orders_list, release_exit_mutex, renew_exit_mutex
|
|
17
|
+
|
|
18
|
+
CANDLE = {"symbol": "NIFTY_50", "close": 101.5, "timestamp": datetime.datetime(2025, 6, 2, 9, 31, 7, 500)}
|
|
19
|
+
ORDER = dict(quantity=5, quantity_left=5, position_type="LONG", transaction_type="BUY", trade_action="ENTRY",
|
|
20
|
+
user_id="u1", strategy_id="s1", request_id="r1", market="IN", exchange="NSE", venue="XNSE",
|
|
21
|
+
data_key="UPSTOX:XNSE:NIFTY_50", group_id="r1:2025-06-02:1", leg_key="SPOT", lot_size=1,
|
|
22
|
+
params={"investment": 100000, "sl_price": 70.0})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------- memory_state_cursor
|
|
26
|
+
|
|
27
|
+
def test_memory_cursor_lists_behave_like_redis_lists():
|
|
28
|
+
c = memory_state_cursor()
|
|
29
|
+
assert c.rpush("k", "a") == 1 and c.rpush("k", "b", "c") == 3
|
|
30
|
+
assert c.lrange("k", 0, -1) == ["a", "b", "c"] and c.lrange("k", 1, 1) == ["b"]
|
|
31
|
+
assert c.llen("k") == 3 and c.llen("missing") == 0 and c.lrange("missing", 0, -1) == []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_memory_cursor_strings_and_set_nx():
|
|
35
|
+
c = memory_state_cursor()
|
|
36
|
+
assert c.set("m", "t1", nx=True, ex=120) is True
|
|
37
|
+
assert c.set("m", "t2", nx=True, ex=120) is None # already held
|
|
38
|
+
assert c.get("m") == "t1" and c.get("nope") is None
|
|
39
|
+
assert c.delete("m") == 1 and c.delete("m") == 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_memory_cursor_publish_and_expire_are_harmless():
|
|
43
|
+
c = memory_state_cursor()
|
|
44
|
+
c.rpush("k", "x")
|
|
45
|
+
assert c.publish("channel", "msg") == 0 and c.expire("k", 10) is True and c.expire("none", 10) is False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_exit_mutex_helpers_work_on_the_memory_cursor():
|
|
49
|
+
c = memory_state_cursor()
|
|
50
|
+
token = acquire_exit_mutex(c, "r1")
|
|
51
|
+
assert token and acquire_exit_mutex(c, "r1", wait_s=0) is None # mutually exclusive
|
|
52
|
+
renew_exit_mutex(c, "r1", token)
|
|
53
|
+
release_exit_mutex(c, "r1", "someone-elses-token") # must not free it
|
|
54
|
+
assert acquire_exit_mutex(c, "r1", wait_s=0) is None
|
|
55
|
+
release_exit_mutex(c, "r1", token)
|
|
56
|
+
assert acquire_exit_mutex(c, "r1", wait_s=0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------- place_bt_leg_order
|
|
60
|
+
|
|
61
|
+
def _vt_order_keys(monkeypatch):
|
|
62
|
+
monkeypatch.setattr(virtual, "send_order_alert", lambda alert: None)
|
|
63
|
+
db = {"vt_orders": types.SimpleNamespace(insert_one=lambda doc: types.SimpleNamespace(inserted_id="OID1"))}
|
|
64
|
+
cursor = memory_state_cursor()
|
|
65
|
+
orders = virtual.place_vt_order(app_db_cursor=db, redis_cursor=cursor, order_candle=CANDLE, **ORDER)
|
|
66
|
+
return set(orders[-1])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_bt_order_has_exactly_the_vt_order_shape(monkeypatch):
|
|
70
|
+
vt_keys = _vt_order_keys(monkeypatch)
|
|
71
|
+
bt = place_bt_leg_order(redis_cursor=memory_state_cursor(), order_candle=CANDLE, **ORDER)[-1]
|
|
72
|
+
assert vt_keys - {"db_order_id"} == set(bt) # the only vt-only field is the database id
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_bt_order_is_stamped_from_the_candle_and_keeps_its_fields():
|
|
76
|
+
order = place_bt_leg_order(redis_cursor=memory_state_cursor(), order_candle=CANDLE, **ORDER)[-1]
|
|
77
|
+
assert order["date"] == "2025-06-02 00:00:00"
|
|
78
|
+
assert order["order_timestamp"] == "2025-06-02 09:31:07" # microseconds dropped, like vt
|
|
79
|
+
assert order["day"] == "Monday" and order["symbol"] == "NIFTY_50"
|
|
80
|
+
assert order["trigger_price"] == 101.5 # no explicit trigger -> the candle close
|
|
81
|
+
assert order["sl_price"] == 70.0 and order["investment"] == 100000 # params folded in, like vt
|
|
82
|
+
assert order["group_id"] == "r1:2025-06-02:1" and order["leg_key"] == "SPOT" and order["venue"] == "XNSE"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_bt_order_accumulates_on_the_request_orders_list():
|
|
86
|
+
c = memory_state_cursor()
|
|
87
|
+
place_bt_leg_order(redis_cursor=c, order_candle=CANDLE, **ORDER)
|
|
88
|
+
second = place_bt_leg_order(redis_cursor=c, order_candle=CANDLE, **{**ORDER, "trade_action": "EXIT", "exit_type": "T1"})
|
|
89
|
+
assert [o["trade_action"] for o in second] == ["ENTRY", "EXIT"]
|
|
90
|
+
assert [o["trade_action"] for o in fetch_orders_list(c, "r1")] == ["ENTRY", "EXIT"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_bt_order_sends_no_alert(monkeypatch):
|
|
94
|
+
monkeypatch.setattr(utils, "send_order_alert", lambda *a, **k: (_ for _ in ()).throw(AssertionError("alert sent")))
|
|
95
|
+
monkeypatch.setattr(virtual, "send_order_alert", lambda *a, **k: (_ for _ in ()).throw(AssertionError("alert sent")))
|
|
96
|
+
place_bt_leg_order(redis_cursor=memory_state_cursor(), order_candle=CANDLE, **ORDER)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_bt_order_rejects_a_candle_without_a_datetime_timestamp():
|
|
100
|
+
with pytest.raises(TypeError, match="datetime"):
|
|
101
|
+
place_bt_leg_order(redis_cursor=memory_state_cursor(), order_candle={**CANDLE, "timestamp": "2025-06-02 09:31"},
|
|
102
|
+
**ORDER)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_bt_order_raises_when_nothing_was_stored():
|
|
106
|
+
broken = types.SimpleNamespace(rpush=lambda *a: 0, expire=lambda *a: 0, publish=lambda *a: 0,
|
|
107
|
+
lrange=lambda *a: [], llen=lambda k: 0)
|
|
108
|
+
with pytest.raises(RuntimeError, match="not stored"):
|
|
109
|
+
place_bt_leg_order(redis_cursor=broken, order_candle=CANDLE, **ORDER)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
""" Task B3 (spec §8.3 manual exits): the per-request exit mutex that
|
|
2
|
+
serializes everyone who places exit orders for a request — a manual exit
|
|
3
|
+
(OMS) and a running strategy's own exits — so a leg is never exited twice.
|
|
4
|
+
|
|
5
|
+
Offline: a tiny in-memory fake Redis (SET NX EX / GET / DELETE), no network.
|
|
6
|
+
"""
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from trigerr.orders import live
|
|
10
|
+
from trigerr.orders.orders_utils import (acquire_exit_mutex, release_exit_mutex, renew_exit_mutex,
|
|
11
|
+
EXIT_MUTEX_TTL_S)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeRedis:
|
|
15
|
+
""" SET NX EX / GET / DELETE / EXPIRE with real expiry against a settable
|
|
16
|
+
clock (self.now), so a test can let time pass past the mutex TTL. """
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._kv = {}
|
|
19
|
+
self._expires = {}
|
|
20
|
+
self.now = 0.0
|
|
21
|
+
|
|
22
|
+
def _alive(self, key):
|
|
23
|
+
if key in self._expires and self.now >= self._expires[key]:
|
|
24
|
+
self._kv.pop(key, None)
|
|
25
|
+
self._expires.pop(key, None)
|
|
26
|
+
return key in self._kv
|
|
27
|
+
|
|
28
|
+
def set(self, key, value, nx=False, ex=None):
|
|
29
|
+
if nx and self._alive(key):
|
|
30
|
+
return None
|
|
31
|
+
self._kv[key] = value
|
|
32
|
+
if ex is not None:
|
|
33
|
+
self._expires[key] = self.now + ex
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def get(self, key):
|
|
37
|
+
return self._kv.get(key) if self._alive(key) else None
|
|
38
|
+
|
|
39
|
+
def delete(self, key):
|
|
40
|
+
self._expires.pop(key, None)
|
|
41
|
+
return self._kv.pop(key, None) is not None
|
|
42
|
+
|
|
43
|
+
def expire(self, key, seconds):
|
|
44
|
+
if not self._alive(key):
|
|
45
|
+
return False
|
|
46
|
+
self._expires[key] = self.now + seconds
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_acquire_returns_a_token_when_free():
|
|
51
|
+
redis_cursor = FakeRedis()
|
|
52
|
+
token = acquire_exit_mutex(redis_cursor, "r1")
|
|
53
|
+
assert token is not None
|
|
54
|
+
assert redis_cursor.get("r1:exit_mutex") == token
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_acquire_waits_then_succeeds_once_the_holder_releases(monkeypatch):
|
|
58
|
+
redis_cursor = FakeRedis()
|
|
59
|
+
sleeps = []
|
|
60
|
+
monkeypatch.setattr(time, "sleep", lambda s: sleeps.append(s))
|
|
61
|
+
|
|
62
|
+
first_token = acquire_exit_mutex(redis_cursor, "r1")
|
|
63
|
+
|
|
64
|
+
# The real SET NX fails while the first holder has it; make the first
|
|
65
|
+
# retry fail, then release before the second retry succeeds.
|
|
66
|
+
attempts = {"n": 0}
|
|
67
|
+
real_set = redis_cursor.set
|
|
68
|
+
|
|
69
|
+
def flaky_set(key, value, nx=False, ex=None):
|
|
70
|
+
attempts["n"] += 1
|
|
71
|
+
if attempts["n"] == 1:
|
|
72
|
+
return None # still held on the first retry
|
|
73
|
+
redis_cursor.delete("r1:exit_mutex") # the first holder released it
|
|
74
|
+
return real_set(key, value, nx=nx, ex=ex)
|
|
75
|
+
|
|
76
|
+
monkeypatch.setattr(redis_cursor, "set", flaky_set)
|
|
77
|
+
second_token = acquire_exit_mutex(redis_cursor, "r1", wait_s=5)
|
|
78
|
+
assert second_token is not None
|
|
79
|
+
assert second_token != first_token
|
|
80
|
+
assert sleeps, "acquire_exit_mutex must sleep between retries"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_acquire_times_out_when_never_released(monkeypatch):
|
|
84
|
+
redis_cursor = FakeRedis()
|
|
85
|
+
acquire_exit_mutex(redis_cursor, "r1") # holds it forever
|
|
86
|
+
|
|
87
|
+
monkeypatch.setattr(time, "sleep", lambda s: None)
|
|
88
|
+
clock = {"t": 0.0}
|
|
89
|
+
monkeypatch.setattr(time, "monotonic", lambda: clock["t"])
|
|
90
|
+
|
|
91
|
+
# First monotonic() call sets the deadline; make time "pass" fully on the
|
|
92
|
+
# very first is-it-past-the-deadline check so the loop returns None
|
|
93
|
+
# immediately instead of actually looping wait_s/0.2 times.
|
|
94
|
+
real_monotonic = time.monotonic
|
|
95
|
+
calls = {"n": 0}
|
|
96
|
+
|
|
97
|
+
def fake_monotonic():
|
|
98
|
+
calls["n"] += 1
|
|
99
|
+
if calls["n"] == 1:
|
|
100
|
+
return 0.0 # deadline = 0.0 + wait_s
|
|
101
|
+
return 10 ** 6 # every later check is long past it
|
|
102
|
+
monkeypatch.setattr(time, "monotonic", fake_monotonic)
|
|
103
|
+
|
|
104
|
+
token = acquire_exit_mutex(redis_cursor, "r1", wait_s=1)
|
|
105
|
+
assert token is None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_release_deletes_only_when_token_still_matches():
|
|
109
|
+
redis_cursor = FakeRedis()
|
|
110
|
+
token = acquire_exit_mutex(redis_cursor, "r1")
|
|
111
|
+
release_exit_mutex(redis_cursor, "r1", "not-the-token")
|
|
112
|
+
assert redis_cursor.get("r1:exit_mutex") == token # untouched
|
|
113
|
+
|
|
114
|
+
release_exit_mutex(redis_cursor, "r1", token)
|
|
115
|
+
assert redis_cursor.get("r1:exit_mutex") is None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_release_is_a_noop_when_key_already_expired_or_gone():
|
|
119
|
+
redis_cursor = FakeRedis()
|
|
120
|
+
release_exit_mutex(redis_cursor, "r1", "whatever") # must not raise
|
|
121
|
+
assert redis_cursor.get("r1:exit_mutex") is None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# S5: renewal while an exit order waits for its fill
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def test_renew_extends_the_ttl_for_its_own_token():
|
|
129
|
+
redis_cursor = FakeRedis()
|
|
130
|
+
token = acquire_exit_mutex(redis_cursor, "r1")
|
|
131
|
+
redis_cursor.now = EXIT_MUTEX_TTL_S - 1
|
|
132
|
+
renew_exit_mutex(redis_cursor, "r1", token)
|
|
133
|
+
redis_cursor.now = EXIT_MUTEX_TTL_S + 60 # past the original expiry
|
|
134
|
+
assert redis_cursor.get("r1:exit_mutex") == token
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_renew_never_touches_someone_elses_mutex():
|
|
138
|
+
redis_cursor = FakeRedis()
|
|
139
|
+
other = acquire_exit_mutex(redis_cursor, "r1")
|
|
140
|
+
redis_cursor.now = EXIT_MUTEX_TTL_S - 1
|
|
141
|
+
renew_exit_mutex(redis_cursor, "r1", "stale-token")
|
|
142
|
+
redis_cursor.now = EXIT_MUTEX_TTL_S
|
|
143
|
+
assert redis_cursor.get("r1:exit_mutex") is None, f"{other} must expire on its own schedule"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_renew_does_not_resurrect_an_expired_mutex():
|
|
147
|
+
redis_cursor = FakeRedis()
|
|
148
|
+
token = acquire_exit_mutex(redis_cursor, "r1")
|
|
149
|
+
redis_cursor.now = EXIT_MUTEX_TTL_S + 1
|
|
150
|
+
renew_exit_mutex(redis_cursor, "r1", token)
|
|
151
|
+
assert redis_cursor.get("r1:exit_mutex") is None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_an_exit_poll_longer_than_the_ttl_keeps_the_mutex_held(monkeypatch):
|
|
155
|
+
""" A MARKET exit that sits OPEN for 5 minutes (> the 120 s TTL): with
|
|
156
|
+
on_wait renewing, nobody else can take the mutex until the fill. """
|
|
157
|
+
redis_cursor = FakeRedis()
|
|
158
|
+
token = acquire_exit_mutex(redis_cursor, "r1")
|
|
159
|
+
open_polls = {"n": 300}
|
|
160
|
+
seen_free = []
|
|
161
|
+
|
|
162
|
+
def fake_post(url, json, headers):
|
|
163
|
+
response = type("R", (), {})()
|
|
164
|
+
status = "OPEN" if open_polls["n"] > 0 else "COMPLETED"
|
|
165
|
+
open_polls["n"] -= 1
|
|
166
|
+
response.json = lambda: {"status": status, "timestamp": "2026-09-28 10:00:00", "average_price": 100.0}
|
|
167
|
+
return response
|
|
168
|
+
|
|
169
|
+
def fake_sleep(seconds):
|
|
170
|
+
redis_cursor.now += seconds
|
|
171
|
+
seen_free.append(acquire_exit_mutex(redis_cursor, "r1", wait_s=0) is not None)
|
|
172
|
+
|
|
173
|
+
monkeypatch.setattr(live.requests, "post", fake_post)
|
|
174
|
+
monkeypatch.setattr(live.time, "sleep", fake_sleep)
|
|
175
|
+
monkeypatch.setattr(live.time, "time", lambda: redis_cursor.now)
|
|
176
|
+
live.orders_url = "http://testserver/"
|
|
177
|
+
|
|
178
|
+
status, _ = live.poll_order_status("c1", "OID1", "NFO",
|
|
179
|
+
on_wait=lambda: renew_exit_mutex(redis_cursor, "r1", token))
|
|
180
|
+
assert status == "success"
|
|
181
|
+
assert redis_cursor.now >= 300
|
|
182
|
+
assert not any(seen_free), "the mutex must never be free while the exit is still OPEN"
|
|
183
|
+
assert redis_cursor.get("r1:exit_mutex") == token
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
""" The order ledger must account for every lot, not the last entry per symbol.
|
|
2
|
+
|
|
3
|
+
`check_open_orders` folds an orders list into open positions keyed by SYMBOL, and each ENTRY order overwrites the
|
|
4
|
+
previous one's fields for that symbol (quantity, quantity_left, trigger_price, group_id ...). Two entries in the
|
|
5
|
+
same instrument — pyramiding, a grid, an adjustment that re-opens a strike, two overlapping structures — therefore
|
|
6
|
+
report only the last lot, and a restart rebuilds only that one. No running strategy does this today; the
|
|
7
|
+
families on the roadmap do.
|
|
8
|
+
"""
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from trigerr.orders.orders_utils import check_open_orders
|
|
12
|
+
|
|
13
|
+
SYMBOL = "NIFTY_50_24000_CE_2026-08-27"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _entry(group, quantity, price, minute):
|
|
17
|
+
return {"symbol": SYMBOL, "trade_action": "ENTRY", "quantity": quantity, "quantity_left": quantity,
|
|
18
|
+
"trigger_price": price, "order_timestamp": f"2025-06-02 09:{minute:02d}:00", "position_type": "LONG",
|
|
19
|
+
"group_id": group, "leg_key": "CE", "data_key": f"UPSTOX:XNSE:{SYMBOL}", "venue": "XNSE", "lot_size": 25}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_one_entry_is_one_open_position():
|
|
23
|
+
open_positions = check_open_orders([_entry("r1:2025-06-02:1", 10, 100.0, 16)])
|
|
24
|
+
assert {s: p["quantity_left"] for s, p in open_positions.items()} == {SYMBOL: 10}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.xfail(strict=True, raises=AssertionError,
|
|
28
|
+
reason="S6: open positions are keyed by symbol and the second ENTRY overwrites the first, so a "
|
|
29
|
+
"second lot in the same instrument hides the first (15 open lots are reported as 5)")
|
|
30
|
+
def test_two_entries_in_the_same_instrument_are_both_open():
|
|
31
|
+
orders = [_entry("r1:2025-06-02:1", 10, 100.0, 16), _entry("r1:2025-06-02:2", 5, 110.0, 30)]
|
|
32
|
+
open_positions = check_open_orders(orders)
|
|
33
|
+
assert sum(p["quantity_left"] for p in open_positions.values()) == 15
|
|
@@ -127,3 +127,59 @@ def test_validate_credential_never_sends_credential_values():
|
|
|
127
127
|
|
|
128
128
|
sent_body = mock_post.call_args.kwargs["json"]
|
|
129
129
|
assert set(sent_body.keys()) == {"credential_id"}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# S6: idempotency key -- one per call, reused only for transport retries
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def test_place_live_order_sends_idempotency_key_at_top_level():
|
|
137
|
+
with patch("trigerr.orders.live.requests.post") as mock_post:
|
|
138
|
+
mock_post.return_value = _mock_response({"status": "SUCCESS"})
|
|
139
|
+
live.place_live_order(credential_id="cred-1", order_details={"symbol": "INFY"})
|
|
140
|
+
|
|
141
|
+
sent_body = mock_post.call_args.kwargs["json"]
|
|
142
|
+
assert sent_body["idempotency_key"]
|
|
143
|
+
assert "idempotency_key" not in sent_body["order_params"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_transport_retry_resends_the_same_key():
|
|
147
|
+
""" The OMS may have placed the order before the connection dropped: the
|
|
148
|
+
retry must carry the same key so the OMS replays instead of re-placing. """
|
|
149
|
+
with patch("trigerr.orders.live.requests.post") as mock_post, patch("trigerr.orders.live.time.sleep"):
|
|
150
|
+
mock_post.side_effect = [live.requests.ConnectionError("reset"),
|
|
151
|
+
_mock_response({"status": "SUCCESS", "order_id": "OID1"})]
|
|
152
|
+
response = live.place_live_order(credential_id="cred-1", order_details={"symbol": "INFY"})
|
|
153
|
+
|
|
154
|
+
assert response["order_id"] == "OID1"
|
|
155
|
+
keys = [c.kwargs["json"]["idempotency_key"] for c in mock_post.call_args_list]
|
|
156
|
+
assert len(keys) == 2 and keys[0] == keys[1]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_each_call_gets_a_fresh_key():
|
|
160
|
+
""" The OMS keeps an ERROR result under its key for 24 h: a later retry of
|
|
161
|
+
a rejected order must not reuse the key, or it can never be placed. """
|
|
162
|
+
with patch("trigerr.orders.live.requests.post") as mock_post:
|
|
163
|
+
mock_post.return_value = _mock_response({"status": "ERROR"})
|
|
164
|
+
live.place_live_order(credential_id="cred-1", order_details={"symbol": "INFY"})
|
|
165
|
+
live.place_live_order(credential_id="cred-1", order_details={"symbol": "INFY"})
|
|
166
|
+
|
|
167
|
+
keys = [c.kwargs["json"]["idempotency_key"] for c in mock_post.call_args_list]
|
|
168
|
+
assert keys[0] != keys[1]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_transport_retries_are_bounded_then_return_none():
|
|
172
|
+
with patch("trigerr.orders.live.requests.post") as mock_post, patch("trigerr.orders.live.time.sleep"):
|
|
173
|
+
mock_post.side_effect = live.requests.ConnectionError("down")
|
|
174
|
+
assert live.place_live_order(credential_id="cred-1", order_details={"symbol": "INFY"}) is None
|
|
175
|
+
assert mock_post.call_count == 3
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_poll_order_status_calls_on_wait_once_per_open_poll():
|
|
179
|
+
responses = [_mock_response({"status": "OPEN"}), _mock_response({"status": "OPEN"}),
|
|
180
|
+
_mock_response({"status": "COMPLETED", "timestamp": "2026-09-28 10:00:00"})]
|
|
181
|
+
waits = []
|
|
182
|
+
with patch("trigerr.orders.live.requests.post", side_effect=responses), patch("trigerr.orders.live.time.sleep"):
|
|
183
|
+
status, _ = live.poll_order_status("cred-1", "OID1", "NFO", on_wait=lambda: waits.append(1))
|
|
184
|
+
assert status == "success"
|
|
185
|
+
assert len(waits) == 2
|
|
@@ -11,6 +11,7 @@ from trigerr.framework.data_feeds import resolve_feeds
|
|
|
11
11
|
from trigerr.framework.point_in_time import truncate_feeds_to_moment
|
|
12
12
|
from trigerr.framework.clock import iterate_clock_bt, evaluate_should_trade_today
|
|
13
13
|
from trigerr.framework.tick_sources import clock_feed_ticks_live
|
|
14
|
+
from trigerr.framework.bt_state import memory_state_cursor
|
|
14
15
|
from trigerr.framework.execution_core import (
|
|
15
16
|
wait_for_entry_signal, monitor_open_position, enter_legs, place_entry_order_for_leg,
|
|
16
17
|
place_exit_order_for_leg, convert_leg_orders_to_trade, rebuild_legs_from_open_orders,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
""" The state a backtest runs against: an in-process stand-in for the tenant's state Redis.
|
|
2
|
+
|
|
3
|
+
The execution core keeps a request's orders list, its exit mutex and its pub/sub in
|
|
4
|
+
ctx["state_cursor"]. A live or virtual run hands it the real state Redis; a backtest hands it this
|
|
5
|
+
instead, so a backtest cannot read or write a real Redis key even by mistake — a stray real call
|
|
6
|
+
raises instead of reaching production. It implements only the commands the core and the
|
|
7
|
+
orders_utils helpers actually use, and nothing expires (a run is one process).
|
|
8
|
+
|
|
9
|
+
A plain dict behind closures, not a class, like the rest of the framework. """
|
|
10
|
+
|
|
11
|
+
import types
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def memory_state_cursor():
|
|
15
|
+
store = {} # key -> str (strings) or list (lists)
|
|
16
|
+
|
|
17
|
+
def rpush(key, *values):
|
|
18
|
+
store.setdefault(key, []).extend(values)
|
|
19
|
+
return len(store[key])
|
|
20
|
+
|
|
21
|
+
def lrange(key, start, stop):
|
|
22
|
+
items = store.get(key, [])
|
|
23
|
+
return list(items[start:] if stop == -1 else items[start:stop + 1])
|
|
24
|
+
|
|
25
|
+
def llen(key):
|
|
26
|
+
return len(store.get(key, []))
|
|
27
|
+
|
|
28
|
+
def set_(key, value, nx=False, ex=None):
|
|
29
|
+
if nx and key in store:
|
|
30
|
+
return None
|
|
31
|
+
store[key] = value
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def get(key):
|
|
35
|
+
value = store.get(key)
|
|
36
|
+
return value if isinstance(value, str) else None
|
|
37
|
+
|
|
38
|
+
def delete(key):
|
|
39
|
+
return 1 if store.pop(key, None) is not None else 0
|
|
40
|
+
|
|
41
|
+
return types.SimpleNamespace(
|
|
42
|
+
rpush=rpush, lrange=lrange, llen=llen, set=set_, get=get, delete=delete,
|
|
43
|
+
expire=lambda key, seconds: key in store,
|
|
44
|
+
publish=lambda channel, message: 0)
|
|
@@ -302,6 +302,14 @@ def validate(ast):
|
|
|
302
302
|
elif ast["clock"] not in ast["feeds"]:
|
|
303
303
|
errors.append(f"clock feed '{ast['clock']}' is not declared in feeds")
|
|
304
304
|
|
|
305
|
+
from trigerr.framework.data_feeds import DATA_FEED_KINDS # lazy: data_feeds imports this module
|
|
306
|
+
for name, spec in ast["feeds"].items():
|
|
307
|
+
if not isinstance(spec, dict) or "derive" in spec or "kind" not in spec:
|
|
308
|
+
continue
|
|
309
|
+
kind = spec["kind"]
|
|
310
|
+
if kind not in DATA_FEED_KINDS and not (isinstance(kind, str) and kind.startswith("$")):
|
|
311
|
+
errors.append(f"feed '{name}' has unknown kind '{kind}' (known: {', '.join(sorted(DATA_FEED_KINDS))})")
|
|
312
|
+
|
|
305
313
|
subscriptions = infer_subscriptions(ast)
|
|
306
314
|
for feed_name in subscriptions:
|
|
307
315
|
if feed_name not in ast["feeds"]:
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
""" DATA_FEED_KINDS: one resolver per data source a strategy can declare in
|
|
2
2
|
its FEEDS block. Each resolver is `fetch_historical(feed_spec, ctx) -> rows`
|
|
3
|
-
— a list of dict candles
|
|
4
|
-
collection/model kinds.
|
|
3
|
+
— a list of dict candles.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
an engine-side adapter).
|
|
5
|
+
Resolvers call the SDK's own HTTP-based historical fetchers directly — pure,
|
|
6
|
+
no Redis/Mongo, which is what keeps this package infra-free (Decision #8: the
|
|
7
|
+
SDK is the pure core; live/DB access is an engine-side adapter). There is
|
|
8
|
+
deliberately no kind that reads a Mongo collection or loads a model file by
|
|
9
|
+
name: market data comes from the Data API, and a new source is added as a
|
|
10
|
+
named kind (one resolver), never as a pass-through to storage.
|
|
13
11
|
|
|
14
12
|
`resolve_feeds` is the entry point: it resolves every declared feed — base
|
|
15
13
|
feeds via a DATA_FEED_KINDS resolver, derived feeds via a CANDLE_TRANSFORMS
|
|
@@ -76,16 +74,6 @@ def _fetch_options(feed_spec, ctx):
|
|
|
76
74
|
return _apply_declared_indicators(rows, feed_spec)
|
|
77
75
|
|
|
78
76
|
|
|
79
|
-
def _fetch_mongo_collection(feed_spec, ctx):
|
|
80
|
-
return ctx["collections"][feed_spec["name"]]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _fetch_pickled_model(feed_spec, ctx):
|
|
84
|
-
""" A model is a single object, not a candle series — a "ref" op can't
|
|
85
|
-
read it; a native node's plugin reads ctx["feeds"][name] directly. """
|
|
86
|
-
return ctx["models"][feed_spec["path"]]
|
|
87
|
-
|
|
88
|
-
|
|
89
77
|
DATA_FEED_KINDS = {
|
|
90
78
|
"eod": {"fetch_historical": _fetch_eod,
|
|
91
79
|
"spec": {"label": "End-of-day candles", "params": ["symbol", "lookback_days", "exchange"]}},
|
|
@@ -97,10 +85,6 @@ DATA_FEED_KINDS = {
|
|
|
97
85
|
"options": {"fetch_historical": _fetch_options,
|
|
98
86
|
"spec": {"label": "Option candles",
|
|
99
87
|
"params": ["symbol", "option_type", "strike_price", "expiry", "granularity"]}},
|
|
100
|
-
"mongo_collection": {"fetch_historical": _fetch_mongo_collection,
|
|
101
|
-
"spec": {"label": "Mongo collection (e.g. nse_pre_open)", "params": ["name"]}},
|
|
102
|
-
"pickled_model": {"fetch_historical": _fetch_pickled_model,
|
|
103
|
-
"spec": {"label": "Pickled ML model", "params": ["path"]}},
|
|
104
88
|
}
|
|
105
89
|
|
|
106
90
|
|