What a Strategy Needs to Be Told About a Settled Bet

Half the strategies in keeks are pure functions. Kelly takes a probability and a bankroll and returns a fraction; it has no memory and needs none, because everything it uses is in the arguments.

The other half want to adapt. DynamicBankrollManagement scales its stake by recent performance, which means it has to be told how recent performance went. That is a feedback interface, and it is a small piece of API design with more decisions in it than it looks.

The interface

def record_result(self, won, return_pct=None):
    if return_pct is None:
        return_pct = self.payoff if won else -self.loss
    self.results.append(return_pct)
    if len(self.results) > self.window_size:
        self.results.pop(0)

The simulator calls it duck-typed, on whatever strategy it was handed:

record_result = getattr(strategy, "record_result", None)
if callable(record_result):
    record_result(won, return_pct)

A strategy that implements the method adapts. A strategy that does not is static, and neither one needs to know about the other. That is the right shape, and it makes the first point of the post obvious once you can test it.

“Dynamic” is a property of the data flow

The hook is duck-typed, so you can remove it from an instance and watch what a strategy is without its feedback:

strategy = DynamicBankrollManagement(base_fraction=0.05, payoff=1.0, loss=1.0, transaction_cost=0.0)
strategy.record_result = None      # now nothing calls back

Median final bankroll over 40 seeds, 1,000 bets:

true edgewith feedbackwithoutratio
p = 0.523,1323,1600.99x
p = 0.5573,13663,6211.15x
p = 0.6014,639,4894,953,3692.96x

Same class, same parameters, same seeds. The only difference is whether anything told it what happened, and at a strong edge that is worth three times the final bankroll.

It is also worth noticing where the feedback is worth nothing. At a 2% edge the loop is a wash, because there is barely any signal in a window of ten results to adapt to. Adaptive sizing pays when there is something to adapt to, which is not always.

The three decisions

Report the outcome or the return? Both, and they can disagree.

won is a boolean. return_pct is the net change in the bankroll, computed after transaction costs. Under a flat fee those two come apart: a small stake can win the bet and still lose money once the fee is taken. Running 400 bets with a $5 flat fee and small stakes:

settled bets seen: 150, wins: 76, wins with a negative return: 14
example: won=True, return_pct=-0.011418

Eighteen percent of the winning bets lost money. A strategy tracking win rate sees a hot streak; a strategy tracking returns sees a drawdown. Both are reading the same 76 bets correctly, and which one you build on decides how the strategy behaves.

Note the fallback in that method: if the caller omits return_pct, it reconstructs it as payoff if won else -loss, which is the gross result. That is a reasonable default for a caller that genuinely has no cost information, and it is the wrong number whenever costs exist. If you are writing a simulator, pass the real net.

What about bets you didn’t place? The simulator only settles when the strategy asked for a positive stake:

if proportion > 0:
    ...

so a skipped bet is invisible to the feedback loop. That is a defensible default, since a strategy that declined has nothing to learn from an outcome it did not take a position on. It also means a strategy’s window of “recent results” is a window over bets, not over opportunities, and a strategy that skips heavily is adapting on a much older window than the trial count suggests. Worth knowing before you tune a window size in trials.

Whose job is the history? In keeks the strategy keeps its own window and the simulator keeps the bankroll’s. Neither can see the other’s, which means the strategy cannot accidentally depend on bankroll state it was not given, and the simulator does not have to know what the strategy considers relevant. The cost is that a strategy which wants a longer memory has to hold it itself.

The general shape

If a component’s behaviour is supposed to depend on what happened, the interface that tells it what happened is part of the design, not plumbing. It is worth writing down explicitly:

  • What it is told (outcome, net return, both) and in what units
  • When it is told (every opportunity, or only acted-upon ones)
  • What happens if nobody tells it (be static, or fail loudly)

keeks answers those three with: both and net-of-costs, only acted-upon bets, and silently static. All three are reasonable. The point is that they are three separate choices, and a class named Dynamic that never receives a callback is just a fixed fraction with extra steps.

Running it

from keeks.bankroll import BankRoll
from keeks.binary_strategies import DynamicBankrollManagement
from keeks.simulators.repeated_binary import RepeatedBinarySimulator

bank = BankRoll(initial_funds=1000.0, percent_bettable=1.0, max_draw_down=1.0)
strategy = DynamicBankrollManagement(base_fraction=0.05, payoff=1.0, loss=1.0, transaction_cost=0.0)
sim = RepeatedBinarySimulator(payoff=1.0, loss=1.0, transaction_costs=0.0,
                              probability=0.60, trials=1000, seed=0)
sim.evaluate_strategy(strategy, bank)
print(bank.total_funds, strategy.results)     # the strategy's own window

pip install keeks==0.6.0.