Skip to main content

exitkit

Swap your exit policy the way you swap your entry signal.

tests PyPI Python License: MIT

Twenty-seven exit models in six families behind one interface. Entry logic is well served by open-source backtesting libraries; exit logic usually is not — most ship one stop and one target and leave the rest to you.

Same entry signal, five exit policies

One entry rule (10/30 SMA crossover), five exit policies, backtesting.py's sample data. The 30-day time limit holds a 22% drawdown through 2008–09 where the others reach 65%. Regenerate it with examples/plot_exit_policies.py.

pip install exitkit

Thirty seconds

import time
from exitkit import StopLossExitModel, Position, SignalOutput

position = Position(
    position_id="p1",
    entry_time=time.time() - 3600,
    entry_signal=SignalOutput(direction=1, meta={"implied_vol": 0.18}),
    entry_price=400.0,
    quantity=10,
)

model = StopLossExitModel(stop_loss_pct=0.02, trailing=True)

for signal in model.generate_exit_signals(
    [position], {"spot_price": 391.0, "implied_vol": 0.21}
):
    print(signal.exit_reason, signal.meta["loss_pct"], signal.confidence)
stop_loss -0.0225 1.0

Every model takes (positions, market_data) and returns SignalOutput objects carrying the position they close and why. Swapping policy is swapping the constructor.

Works with backtesting.py

pip install exitkit[backtesting]
from backtesting import Backtest, Strategy
from exitkit import StopLossExitModel, FixedTimeExitModel
from exitkit.adapters.backtesting_py import ExitMixin

class SmaCross(ExitMixin, Strategy):
    exit_models = [StopLossExitModel(0.05), FixedTimeExitModel(24 * 30)]

    def next(self):
        self.apply_exits()          # close whatever the policy says to close
        if crossover(self.s1, self.s2) and not self.position:
            self.buy()

Same entry signal, six exit policies, on backtesting.py's own sample data (examples/compare_exit_policies.py):

exit policy           return %  trades  max DD %  Sharpe
none (hold)              326.1       1     -65.3    0.47
stop 2%                   47.6       8     -64.6    0.15
stop 5%                  283.9       2     -65.3    0.44
take profit 10%          187.5      10     -64.0    0.39
time limit 30d           208.3      31     -22.3    0.68
stop 5% + tp 10%         100.0      29     -28.3    0.49

One dataset and one entry rule, so read it as an illustration rather than a finding — but it is the comparison the library exists to make cheap. Holding time is measured against the bar's clock, not the wall clock, so a replay ages positions by simulated time rather than by whenever you happened to run it.

The six families

Family Models
stop_loss fixed, adaptive, volatility-scaled, time-decayed
take_profit fixed, partial, adaptive, scaling, momentum-aware
time_based fixed horizon, time decay, adaptive, market hours, performance-conditioned
volatility breakout, regime, mean-reversion, clustering
signal_reversal reversal, strength decay, divergence, consistency
convergence single-target (three variants) and multi-target
from exitkit import FAMILIES

for name, models in FAMILIES.items():
    print(name, [m.__name__ for m in models])

FAMILIES is also how the test suite exercises every model uniformly — adding a model puts it under the whole battery automatically.

Missing market data raises

The one opinion this library holds. Required fields are checked at the boundary and name what is absent:

model.generate_exit_signals([position], {"implied_vol": 0.21})
MissingMarketData: market data is missing 'spot_price'; got: implied_vol.
Exit models require this field - supply it rather than letting a default stand
in, which silently fabricates the decision.

None, NaN and unparseable values count as missing. 0.0 does not.

Where this fits

exitkit decides when to close. It does not fetch data, route orders, or run a backtest loop — hand it positions and market data from whatever you already use.

If you want Use
A full backtest engine backtesting.py, vectorbt
One trailing stop, built in backtesting.py's TrailingStrategy
Intrabar stop/target fills wickra-backtest
Many exit policies to compare exitkit

SignalOutput is a plain dataclass, so wiring it into an existing engine is a translation layer, not an adoption.

Why this exists

The catalogue was extracted from a private options-research program. Writing the test suite surfaced three defects that had survived in running code, all fixed here with regression tests named after them.

Thirty-six fabricated market-data fallbacks. Every model read its inputs as market_data.get('spot_price', 350.0) or .get('implied_vol', 0.2). A caller who omitted a field did not get an error — they got an exit decision computed against an invented price. The volatility default is quieter still: it appears in ratio denominators, so a missing value produces a vol ratio of exactly 1.0, which reads as "no change" rather than "no data". That is why the boundary check above exists.

Time-based exits could not fire. check_time_exit read position.get_holding_hours(), which divided holding_period — a field only ever assigned inside Position.update_pnl(). A model that did not first mark the position saw zero hours held, so a position held nine hours against a two-hour limit did not exit. Holding time is now derived from entry_time; a derived quantity should not depend on another call's side effect.

MarketHoursExitModel had never run. The module called time.time() without importing time, so every invocation raised NameError. It also defaulted its timestamp to the wall clock, which meant a backtest evaluated market hours against whenever you happened to run it — a run at 02:00 would hold everything. That one was found by CI, which runs in a different timezone than the author's laptop, and is why the suite now pins fixed instants.

The feature-window argument was also removed from the required position in the signature: it was the first parameter of every model and not one of them read it.

Tests

pip install -e ".[test]"
pytest -q

141 tests. Four are parametrized across all twenty-seven models, so each must construct, refuse to decide on empty market data, run on complete data, and return nothing when there are no positions.

Licence

MIT. See CHANGELOG.md and CONTRIBUTING.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

exitkit-0.1.0.tar.gz (311.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

exitkit-0.1.0-py3-none-any.whl (32.3 kB view details)

Uploaded Python 3

File details

Details for the file exitkit-0.1.0.tar.gz.

File metadata

  • Download URL: exitkit-0.1.0.tar.gz
  • Upload date:
  • Size: 311.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for exitkit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 be5ef0ebcc467fe0c7f8e50d7209ad665bf12f17a8ef36c2567f044929dd4046
MD5 d83fa4b5d9082ef3974724e9460c630e
BLAKE2b-256 aedf3f0184b086912b339f72ae41ffb78f3ca3b7c4f46c01e9225ef6903a6890

See more details on using hashes here.

Provenance

The following attestation bundles were made for exitkit-0.1.0.tar.gz:

Publisher: publish.yml on charlieyanhx/exitkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exitkit-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: exitkit-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 32.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for exitkit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eae45b1d5e8254d502916f6eb11a7ba6c3beacfa28058b8b2819b30ead9ed367
MD5 38fdae1b1c8e93d165a6eaa0cf35e714
BLAKE2b-256 2a4aabd6fb2c2bc0e3b82033f6f5240df99be2a3f7fde3eed782d6336f4d0ce4

See more details on using hashes here.

Provenance

The following attestation bundles were made for exitkit-0.1.0-py3-none-any.whl:

Publisher: publish.yml on charlieyanhx/exitkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page