keeks 0.2.0__tar.gz → 0.3.0__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.
- keeks-0.3.0/CHANGELOG.md +50 -0
- keeks-0.3.0/CLAUDE.md +202 -0
- {keeks-0.2.0 → keeks-0.3.0}/Makefile +1 -1
- {keeks-0.2.0 → keeks-0.3.0}/PKG-INFO +31 -2
- {keeks-0.2.0 → keeks-0.3.0}/README.md +30 -1
- {keeks-0.2.0 → keeks-0.3.0}/docs/source/binary_strategies.rst +8 -0
- keeks-0.3.0/examples/output/st_petersburg_paradox.csv +5 -0
- keeks-0.3.0/examples/output/st_petersburg_paradox.png +0 -0
- keeks-0.3.0/examples/output/strategy_comparison.csv +14 -0
- keeks-0.3.0/examples/output/strategy_comparison.png +0 -0
- keeks-0.3.0/examples/st_petersburg_paradox.py +244 -0
- keeks-0.2.0/examples/st_petersburg_comparison.py → keeks-0.3.0/examples/strategy_comparison.py +71 -24
- {keeks-0.2.0 → keeks-0.3.0}/keeks/bankroll.py +21 -6
- {keeks-0.2.0 → keeks-0.3.0}/keeks/binary_strategies/__init__.py +3 -0
- {keeks-0.2.0 → keeks-0.3.0}/keeks/binary_strategies/base.py +51 -0
- {keeks-0.2.0 → keeks-0.3.0}/keeks/binary_strategies/kelly.py +126 -0
- {keeks-0.2.0 → keeks-0.3.0}/keeks/binary_strategies/simple.py +396 -2
- {keeks-0.2.0 → keeks-0.3.0}/keeks/simulators/random_binary.py +19 -11
- {keeks-0.2.0 → keeks-0.3.0}/keeks/simulators/random_uncertain_binary.py +22 -14
- {keeks-0.2.0 → keeks-0.3.0}/keeks/simulators/repeated_binary.py +18 -11
- keeks-0.3.0/keeks/utils.py +146 -0
- {keeks-0.2.0 → keeks-0.3.0}/pyproject.toml +1 -1
- keeks-0.3.0/tests/test_bankruptcy_protection.py +176 -0
- keeks-0.3.0/tests/test_merton_share.py +322 -0
- keeks-0.3.0/tests/test_strategy_entry_price.py +366 -0
- keeks-0.3.0/tests/test_utility_functions.py +263 -0
- keeks-0.2.0/.cursor/rules/project_overview.mdc +0 -133
- keeks-0.2.0/.cursor/rules/pytest_standards.mdc +0 -48
- keeks-0.2.0/.cursor/rules/python_standards.mdc +0 -68
- keeks-0.2.0/.cursor/rules/sphinx_docs_standards.mdc +0 -62
- keeks-0.2.0/.cursor/rules/testing.mdc +0 -53
- keeks-0.2.0/CHANGELOG.md +0 -19
- keeks-0.2.0/examples/README.md +0 -50
- keeks-0.2.0/examples/output/st_petersburg_strategies_comparison.csv +0 -11
- keeks-0.2.0/examples/output/st_petersburg_strategies_comparison.png +0 -0
- keeks-0.2.0/keeks/utils.py +0 -9
- {keeks-0.2.0 → keeks-0.3.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/.github/workflows/docs.yml +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/.github/workflows/publish-pypi.yml +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/.github/workflows/test.yml +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/.gitignore +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/LICENSE.md +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/Makefile +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/README.md +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/make.bat +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/requirements.txt +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/source/bankroll.rst +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/source/conf.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/source/getting_started.rst +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/source/index.rst +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/source/simulators.rst +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/docs/source/utils.rst +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/keeks/__init__.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/keeks/simulators/__init__.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/__init__.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_Bankroll.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_cppi_strategy.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_drawdown_adjusted_kelly.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_dynamic_bankroll_management.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_fixed_fraction_strategy.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_kelly_criterion.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_naive_strategy.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/tests/test_optimal_f.py +0 -0
- {keeks-0.2.0 → keeks-0.3.0}/version.py +0 -0
keeks-0.3.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
v0.3.0
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
**New Features:**
|
|
5
|
+
* Added MertonShare strategy based on Merton's portfolio problem with CRRA utility
|
|
6
|
+
* MertonShare supports configurable risk aversion parameter (gamma)
|
|
7
|
+
* Added CRRA utility functions to keeks.utils: `crra_utility()`, `expected_utility()`, `find_indifference_price()`
|
|
8
|
+
* Added `calculate_max_entry_price()` method to ALL 9 strategies:
|
|
9
|
+
- **Utility-based**: KellyCriterion, MertonShare, OptimalF use CRRA utility
|
|
10
|
+
- **Kelly variants**: FractionalKelly, DrawdownAdjustedKelly scale Kelly's price
|
|
11
|
+
- **Rule-based**: FixedFraction, CPPI, Dynamic apply mechanical rules
|
|
12
|
+
- **Risk-neutral**: NaiveStrategy pays expected value
|
|
13
|
+
* Added St. Petersburg paradox example demonstrating all 9 strategies
|
|
14
|
+
* Updated strategy comparison example to include MertonShare with different risk aversion levels
|
|
15
|
+
* Added comprehensive tests for MertonShare strategy (16 test cases)
|
|
16
|
+
* Added comprehensive tests for utility functions (18 test cases)
|
|
17
|
+
* Added comprehensive tests for strategy entry price methods (17 test cases)
|
|
18
|
+
* Updated documentation to include MertonShare, utility functions, and entry price methods
|
|
19
|
+
* Example outputs now include both CSV and PNG visualizations
|
|
20
|
+
|
|
21
|
+
**Critical Bug Fixes:**
|
|
22
|
+
* Fixed bankruptcy protection - bankrolls can no longer go negative
|
|
23
|
+
* Fixed transaction cost bug in simulators - costs now correctly increase losses instead of decreasing them
|
|
24
|
+
* Fixed transaction costs being charged on zero bets - costs only apply when actually betting
|
|
25
|
+
* Fixed CPPI hardcoded 1% edge threshold - now works with realistic 0.5-2% edges
|
|
26
|
+
* Added bankruptcy checks to all simulators - simulations now stop when bankroll is depleted
|
|
27
|
+
* Fixed drawdown check ordering - bankruptcy is now checked before drawdown limits
|
|
28
|
+
* Fixed ruin rate detection in examples - now properly detects early termination
|
|
29
|
+
* Added 9 comprehensive bankruptcy protection tests
|
|
30
|
+
* Updated example parameters to realistic professional bettor scenario (52% win, 0.95x payoff, 0.4% edge)
|
|
31
|
+
|
|
32
|
+
v0.2.0
|
|
33
|
+
======
|
|
34
|
+
|
|
35
|
+
* no longer allowing negative bets
|
|
36
|
+
|
|
37
|
+
v0.1.0
|
|
38
|
+
======
|
|
39
|
+
|
|
40
|
+
* modernizing, added new methods, examples fixes
|
|
41
|
+
|
|
42
|
+
v0.0.2
|
|
43
|
+
======
|
|
44
|
+
|
|
45
|
+
* adding CI, docs, and testing
|
|
46
|
+
|
|
47
|
+
v0.0.1
|
|
48
|
+
======
|
|
49
|
+
|
|
50
|
+
* first release
|
keeks-0.3.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Keeks is a Python library for optimal bankroll allocation and betting strategies, focusing on the Kelly Criterion and its variants. The library provides tools for:
|
|
8
|
+
- Bankroll management with risk protection
|
|
9
|
+
- Mathematical betting strategies (Kelly, Fractional Kelly, etc.)
|
|
10
|
+
- Simulation of betting scenarios with different probability distributions
|
|
11
|
+
|
|
12
|
+
## Development Commands
|
|
13
|
+
|
|
14
|
+
This project uses `uv` as the package manager. All commands are available via the Makefile.
|
|
15
|
+
|
|
16
|
+
### Setup
|
|
17
|
+
```bash
|
|
18
|
+
make setup # Install uv and create virtual environment
|
|
19
|
+
make install-dev # Install package with development dependencies
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Testing
|
|
23
|
+
```bash
|
|
24
|
+
make test # Run pytest
|
|
25
|
+
make test-cov # Run tests with coverage report
|
|
26
|
+
make test-all # Run tests on all Python versions using tox
|
|
27
|
+
|
|
28
|
+
# Run a single test file
|
|
29
|
+
uv run pytest tests/test_kelly_criterion.py
|
|
30
|
+
|
|
31
|
+
# Run a specific test
|
|
32
|
+
uv run pytest tests/test_kelly_criterion.py::test_basic_functionality
|
|
33
|
+
|
|
34
|
+
# Custom pytest arguments
|
|
35
|
+
make test PYTEST_ARGS="tests/test_bankroll.py -v"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Code Quality
|
|
39
|
+
```bash
|
|
40
|
+
make lint # Run ruff checks
|
|
41
|
+
make lint-fix # Auto-fix linting issues (includes unsafe fixes)
|
|
42
|
+
make format # Format code with ruff
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Documentation
|
|
46
|
+
```bash
|
|
47
|
+
make docs # Build Sphinx documentation and open in Chrome
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Other
|
|
51
|
+
```bash
|
|
52
|
+
make build # Build package distributions
|
|
53
|
+
make clean # Clean build artifacts
|
|
54
|
+
make examples # Run example scripts (st_petersburg_comparison.py)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
### Four-Component Design
|
|
60
|
+
|
|
61
|
+
The library follows a four-component architecture:
|
|
62
|
+
|
|
63
|
+
1. **BankRoll** (`keeks/bankroll.py`): Central state manager
|
|
64
|
+
- Tracks total funds and bettable funds (via `percent_bettable`)
|
|
65
|
+
- Maintains transaction history
|
|
66
|
+
- Enforces risk limits via `max_draw_down` parameter
|
|
67
|
+
- Raises `RuinError` when drawdown limit exceeded
|
|
68
|
+
- Key methods: `deposit()`, `withdraw()`, `bet()`, `add_funds()`, `remove_funds()`
|
|
69
|
+
|
|
70
|
+
2. **Strategies** (`keeks/binary_strategies/`): Decision engines
|
|
71
|
+
- All inherit from `BaseStrategy` abstract class
|
|
72
|
+
- Must implement `evaluate(probability, current_bankroll)` returning bet fraction
|
|
73
|
+
- Strategy constructors take: `payoff`, `loss`, `transaction_cost`
|
|
74
|
+
- Available strategies: `KellyCriterion`, `FractionalKellyCriterion`, `DrawdownAdjustedKelly`, `OptimalF`, `FixedFractionStrategy`, `CPPIStrategy`, `DynamicBankrollManagement`, `MertonShare`, `NaiveStrategy`
|
|
75
|
+
|
|
76
|
+
3. **Simulators** (`keeks/simulators/`): Test harnesses
|
|
77
|
+
- `RepeatedBinarySimulator`: Fixed probability across all trials
|
|
78
|
+
- `RandomBinarySimulator`: Normally distributed probabilities
|
|
79
|
+
- `RandomUncertainBinarySimulator`: Adds uncertainty to outcomes
|
|
80
|
+
- All simulators have `evaluate_strategy(strategy, bankroll)` method that runs trials and updates bankroll in-place
|
|
81
|
+
|
|
82
|
+
4. **Utility Functions** (`keeks/utils.py`): Decision theory tools
|
|
83
|
+
- `crra_utility(wealth, risk_aversion)`: Calculate CRRA utility values
|
|
84
|
+
- `expected_utility(outcomes, probabilities, current_wealth, entry_price, risk_aversion)`: Calculate expected utility of a gamble
|
|
85
|
+
- `find_indifference_price(outcomes, probabilities, current_wealth, risk_aversion)`: Find maximum price willing to pay for a gamble
|
|
86
|
+
- Used for one-time decision problems (e.g., St. Petersburg paradox) vs. repeated betting strategies
|
|
87
|
+
|
|
88
|
+
### Key Design Patterns
|
|
89
|
+
|
|
90
|
+
- **Strategy Pattern**: Betting strategies are interchangeable implementations of `BaseStrategy`
|
|
91
|
+
- **State Management**: `BankRoll` maintains all financial state; strategies are stateless (except for state-dependent variants)
|
|
92
|
+
- **In-Place Updates**: Simulators modify the `BankRoll` object directly rather than returning new values
|
|
93
|
+
- **Safety-First**: Strategies use `get_max_safe_bet()` to prevent negative bankroll scenarios
|
|
94
|
+
|
|
95
|
+
### Important Constraints
|
|
96
|
+
|
|
97
|
+
- **No negative bets**: All strategies validated in `BaseStrategy.__init__()` to ensure `payoff > 0`, `loss >= 0`, `transaction_cost >= 0`
|
|
98
|
+
- **Drawdown protection**: `BankRoll.max_draw_down` enforces maximum loss per transaction; raises `RuinError` when violated
|
|
99
|
+
- **Test isolation**: NEVER add test-specific handling or special case logic in library code (see python_standards.mdc)
|
|
100
|
+
|
|
101
|
+
## Code Standards
|
|
102
|
+
|
|
103
|
+
### Python Style
|
|
104
|
+
- Line length: 88 characters (ruff/Black default)
|
|
105
|
+
- Use snake_case for functions/variables, CamelCase for classes
|
|
106
|
+
- Type hints preferred for public APIs
|
|
107
|
+
- Docstrings: NumPy/Google style with parameters, returns, and examples
|
|
108
|
+
|
|
109
|
+
### Testing Standards
|
|
110
|
+
- Tests in `tests/` directory, named `test_*.py`
|
|
111
|
+
- Use `pytest.approx()` for floating-point comparisons
|
|
112
|
+
- Random seed (`random.seed(42)`) for reproducible simulations
|
|
113
|
+
- Target 80%+ code coverage
|
|
114
|
+
- Run specific tests: `uv run pytest tests/test_file.py::test_name`
|
|
115
|
+
|
|
116
|
+
### Import Organization
|
|
117
|
+
1. Standard library (random, abc)
|
|
118
|
+
2. Third-party (numpy, matplotlib, pandas, pytest)
|
|
119
|
+
3. Local imports (keeks modules)
|
|
120
|
+
|
|
121
|
+
## Common Patterns
|
|
122
|
+
|
|
123
|
+
### Testing a Strategy
|
|
124
|
+
```python
|
|
125
|
+
# Pattern used throughout tests
|
|
126
|
+
bankroll = BankRoll(initial_funds=1000, percent_bettable=0.5, max_draw_down=0.3)
|
|
127
|
+
strategy = KellyCriterion(payoff=1.0, loss=1.0, transaction_cost=0.01)
|
|
128
|
+
simulator = RepeatedBinarySimulator(
|
|
129
|
+
payoff=1.0, loss=1.0, transaction_costs=0.01,
|
|
130
|
+
probability=0.55, trials=1000
|
|
131
|
+
)
|
|
132
|
+
simulator.evaluate_strategy(strategy, bankroll)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Strategy Implementation
|
|
136
|
+
```python
|
|
137
|
+
class CustomStrategy(BaseStrategy):
|
|
138
|
+
def evaluate(self, probability: float, current_bankroll: float) -> float:
|
|
139
|
+
# Calculate bet fraction (0.0 to 1.0)
|
|
140
|
+
fraction = ... # your logic
|
|
141
|
+
# Always respect safety limits
|
|
142
|
+
return min(fraction, self.get_max_safe_bet(current_bankroll))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Using MertonShare Strategy
|
|
146
|
+
```python
|
|
147
|
+
# MertonShare uses CRRA utility (Constant Relative Risk Aversion)
|
|
148
|
+
# Formula: f* = μ / (γ × σ²) where:
|
|
149
|
+
# μ = expected return
|
|
150
|
+
# γ = risk aversion coefficient (1=aggressive, 2=moderate, 5=conservative)
|
|
151
|
+
# σ² = variance of returns
|
|
152
|
+
|
|
153
|
+
from keeks.binary_strategies.simple import MertonShare
|
|
154
|
+
|
|
155
|
+
# Moderate risk aversion (γ=2.0 is empirically common)
|
|
156
|
+
strategy = MertonShare(
|
|
157
|
+
payoff=1.0,
|
|
158
|
+
loss=1.0,
|
|
159
|
+
transaction_cost=0.01,
|
|
160
|
+
risk_aversion=2.0, # Higher values = more conservative
|
|
161
|
+
min_probability=0.5,
|
|
162
|
+
max_fraction=1.0
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# MertonShare is typically more conservative than Kelly Criterion
|
|
166
|
+
# Useful for risk-averse investors or when parameter uncertainty is high
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Using Utility Functions for One-Time Decisions
|
|
170
|
+
```python
|
|
171
|
+
# For one-time gamble decisions (not repeated betting)
|
|
172
|
+
from keeks.utils import find_indifference_price
|
|
173
|
+
|
|
174
|
+
# St. Petersburg paradox example: what would you pay to play?
|
|
175
|
+
# Generate outcomes and probabilities for St. Petersburg game
|
|
176
|
+
outcomes = [2**n for n in range(1, 31)] # Payouts: $2, $4, $8, ..., $2^30
|
|
177
|
+
probabilities = [(0.5)**n for n in range(1, 31)] # Probs: 0.5, 0.25, 0.125, ...
|
|
178
|
+
|
|
179
|
+
max_price = find_indifference_price(
|
|
180
|
+
outcomes=outcomes,
|
|
181
|
+
probabilities=probabilities,
|
|
182
|
+
current_wealth=10000,
|
|
183
|
+
risk_aversion=2.0, # Moderate risk aversion
|
|
184
|
+
tolerance=0.01
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
print(f"Maximum willing to pay: ${max_price:.2f}")
|
|
188
|
+
# Output: Maximum willing to pay: $12.80
|
|
189
|
+
|
|
190
|
+
# For comparison, Kelly bettor (γ=1.0) would pay ~$14.24
|
|
191
|
+
# Conservative bettor (γ=5.0) would pay ~$11.26
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Project-Specific Notes
|
|
195
|
+
|
|
196
|
+
- **Version**: Managed in `pyproject.toml` (currently 0.3.0)
|
|
197
|
+
- **Python Support**: 3.8, 3.9, 3.10, 3.11
|
|
198
|
+
- **Documentation site**: [keeks.mcginniscommawill.com](https://keeks.mcginniscommawill.com)
|
|
199
|
+
- **Educational focus**: Library includes disclaimer about educational use only
|
|
200
|
+
- **Examples**:
|
|
201
|
+
- `strategy_comparison.py`: Compares all strategies with favorable odds
|
|
202
|
+
- `st_petersburg_paradox.py`: True St. Petersburg paradox - shows rational payment amounts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keeks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A python library for bankroll allocation strategies
|
|
5
5
|
Project-URL: Homepage, https://github.com/wdm0006/keeks
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/wdm0006/keeks/issues
|
|
@@ -168,11 +168,40 @@ Keeks implements several betting strategies:
|
|
|
168
168
|
dynamic = DynamicBankrollManagement(base_fraction=0.1, payoff=1.0, loss=1.0, window_size=10)
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
8. **
|
|
171
|
+
8. **Merton Share (CRRA Utility)**: Based on Merton's portfolio problem with constant relative risk aversion
|
|
172
|
+
```python
|
|
173
|
+
from keeks.binary_strategies.simple import MertonShare
|
|
174
|
+
merton = MertonShare(payoff=1.0, loss=1.0, transaction_cost=0.01, risk_aversion=2.0)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
9. **Naive Strategy**: A simple strategy that bets the full amount when expected value is positive
|
|
172
178
|
```python
|
|
173
179
|
naive = NaiveStrategy(payoff=1.0, loss=1.0, transaction_cost=0.01)
|
|
174
180
|
```
|
|
175
181
|
|
|
182
|
+
### Utility Functions
|
|
183
|
+
|
|
184
|
+
For one-time decision problems (e.g., "What should I pay for this opportunity?"), keeks provides CRRA utility functions:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from keeks.utils import find_indifference_price
|
|
188
|
+
|
|
189
|
+
# Calculate maximum price you'd pay for a gamble
|
|
190
|
+
# Example: St. Petersburg paradox
|
|
191
|
+
outcomes = [2**n for n in range(1, 31)]
|
|
192
|
+
probabilities = [(0.5)**n for n in range(1, 31)]
|
|
193
|
+
|
|
194
|
+
max_price = find_indifference_price(
|
|
195
|
+
outcomes=outcomes,
|
|
196
|
+
probabilities=probabilities,
|
|
197
|
+
current_wealth=10000,
|
|
198
|
+
risk_aversion=2.0 # 1.0=Kelly, 2.0=moderate, 5.0=conservative
|
|
199
|
+
)
|
|
200
|
+
# Returns: ~$12.80 despite infinite expected value!
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
See [examples/st_petersburg_paradox.py](examples/st_petersburg_paradox.py) for a complete demonstration.
|
|
204
|
+
|
|
176
205
|
### Simulators
|
|
177
206
|
|
|
178
207
|
Test your strategies with different simulators:
|
|
@@ -133,11 +133,40 @@ Keeks implements several betting strategies:
|
|
|
133
133
|
dynamic = DynamicBankrollManagement(base_fraction=0.1, payoff=1.0, loss=1.0, window_size=10)
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
8. **
|
|
136
|
+
8. **Merton Share (CRRA Utility)**: Based on Merton's portfolio problem with constant relative risk aversion
|
|
137
|
+
```python
|
|
138
|
+
from keeks.binary_strategies.simple import MertonShare
|
|
139
|
+
merton = MertonShare(payoff=1.0, loss=1.0, transaction_cost=0.01, risk_aversion=2.0)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
9. **Naive Strategy**: A simple strategy that bets the full amount when expected value is positive
|
|
137
143
|
```python
|
|
138
144
|
naive = NaiveStrategy(payoff=1.0, loss=1.0, transaction_cost=0.01)
|
|
139
145
|
```
|
|
140
146
|
|
|
147
|
+
### Utility Functions
|
|
148
|
+
|
|
149
|
+
For one-time decision problems (e.g., "What should I pay for this opportunity?"), keeks provides CRRA utility functions:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from keeks.utils import find_indifference_price
|
|
153
|
+
|
|
154
|
+
# Calculate maximum price you'd pay for a gamble
|
|
155
|
+
# Example: St. Petersburg paradox
|
|
156
|
+
outcomes = [2**n for n in range(1, 31)]
|
|
157
|
+
probabilities = [(0.5)**n for n in range(1, 31)]
|
|
158
|
+
|
|
159
|
+
max_price = find_indifference_price(
|
|
160
|
+
outcomes=outcomes,
|
|
161
|
+
probabilities=probabilities,
|
|
162
|
+
current_wealth=10000,
|
|
163
|
+
risk_aversion=2.0 # 1.0=Kelly, 2.0=moderate, 5.0=conservative
|
|
164
|
+
)
|
|
165
|
+
# Returns: ~$12.80 despite infinite expected value!
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
See [examples/st_petersburg_paradox.py](examples/st_petersburg_paradox.py) for a complete demonstration.
|
|
169
|
+
|
|
141
170
|
### Simulators
|
|
142
171
|
|
|
143
172
|
Test your strategies with different simulators:
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Bankroll,Kelly ($),Kelly (%),Half Kelly ($),Half Kelly (%),Drawdown Kelly ($),Drawdown Kelly (%),Optimal F ($),Optimal F (%),Merton (γ=2.0) ($),Merton (γ=2.0) (%),Merton (γ=5.0) ($),Merton (γ=5.0) (%),Naive ($),Naive (%),Fixed 5% ($),Fixed 5% (%),Dynamic 10% ($),Dynamic 10% (%)
|
|
2
|
+
$1,$0.50,49.609%,$0.25,24.805%,$0.20,19.844%,$0.50,49.609%,$0.50,49.609%,$0.50,49.609%,$1.00,100.000%,$0.05,5.000%,$0.10,10.000%
|
|
3
|
+
$10,$4.97,49.658%,$2.48,24.829%,$1.99,19.863%,$4.97,49.658%,$3.96,39.600%,$3.10,31.006%,$10.00,100.000%,$0.50,5.000%,$1.00,10.000%
|
|
4
|
+
$100,$7.79,7.791%,$3.90,3.896%,$3.12,3.116%,$7.79,7.791%,$6.49,6.491%,$5.20,5.197%,$100.00,100.000%,$5.00,5.000%,$10.00,10.000%
|
|
5
|
+
"$1,000",$10.95,1.095%,$5.48,0.548%,$4.38,0.438%,$10.95,1.095%,$9.55,0.955%,$8.06,0.806%,$1000.00,100.000%,$50.00,5.000%,$100.00,10.000%
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Strategy,Mean,Median,Min,Max,Std Dev,Ruin Rate
|
|
2
|
+
Merton (γ=1.0),$1301.11,$1255.87,$623.56,$3112.29,$379.23,0.0%
|
|
3
|
+
Kelly Criterion,$1316.52,$1250.61,$539.09,$3052.69,$397.43,0.0%
|
|
4
|
+
Naive Strategy,$1272.19,$1215.58,$566.97,$2641.01,$351.19,0.0%
|
|
5
|
+
Optimal F,$1279.55,$1215.03,$393.22,$2839.17,$415.17,0.0%
|
|
6
|
+
Merton (γ=2.0),$1102.08,$1095.51,$757.45,$1739.15,$154.93,0.0%
|
|
7
|
+
Half Kelly,$1102.39,$1080.04,$721.58,$1767.90,$165.63,0.0%
|
|
8
|
+
Drawdown Kelly,$1065.94,$1064.01,$722.03,$1578.80,$126.19,0.0%
|
|
9
|
+
Quarter Kelly,$1022.51,$1015.90,$787.98,$1276.53,$78.62,0.0%
|
|
10
|
+
Merton (γ=5.0),$1012.83,$1010.68,$869.55,$1243.26,$60.75,0.0%
|
|
11
|
+
CPPI,$961.07,$960.53,$928.36,$996.13,$12.10,0.0%
|
|
12
|
+
Fixed 5%,$13028.93,$72.95,$0.00,$1087677.26,$80321.85,33.0%
|
|
13
|
+
Dynamic,$1875.02,$13.62,$0.00,$145023.91,$9762.06,41.2%
|
|
14
|
+
Fixed 10%,$98.25,$0.00,$0.00,$48451.32,$2164.67,98.8%
|
|
Binary file
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
True St. Petersburg Paradox: What Would You Pay to Play?
|
|
3
|
+
|
|
4
|
+
The St. Petersburg paradox is a probability theory paradox:
|
|
5
|
+
- Flip a fair coin repeatedly until it comes up tails
|
|
6
|
+
- Payout is $2^n where n is the number of flips
|
|
7
|
+
- Expected value is infinite: E[X] = Σ(1/2^n × 2^n) = Σ(1) = ∞
|
|
8
|
+
|
|
9
|
+
The paradox: Despite infinite expected value, no rational person would pay much to play.
|
|
10
|
+
|
|
11
|
+
This example calculates the maximum entry price different betting strategies would pay
|
|
12
|
+
to play one round of the St. Petersburg game at various wealth levels.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
import matplotlib
|
|
18
|
+
matplotlib.use("Agg") # Use non-interactive backend
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from keeks.binary_strategies import KellyCriterion
|
|
25
|
+
from keeks.binary_strategies.kelly import DrawdownAdjustedKelly, FractionalKellyCriterion
|
|
26
|
+
from keeks.binary_strategies.simple import (
|
|
27
|
+
CPPIStrategy,
|
|
28
|
+
DynamicBankrollManagement,
|
|
29
|
+
FixedFractionStrategy,
|
|
30
|
+
MertonShare,
|
|
31
|
+
NaiveStrategy,
|
|
32
|
+
OptimalF,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_st_petersburg_outcomes(max_flips=30):
|
|
37
|
+
"""
|
|
38
|
+
Generate outcomes and probabilities for St. Petersburg game.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
max_flips : int
|
|
43
|
+
Maximum coin flips to consider (practical cap for infinite series)
|
|
44
|
+
Note: Limited to ~1000 due to float64 overflow at 2^1024
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
tuple
|
|
49
|
+
(outcomes, probabilities) arrays
|
|
50
|
+
"""
|
|
51
|
+
import numpy as np
|
|
52
|
+
|
|
53
|
+
outcomes = []
|
|
54
|
+
probabilities = []
|
|
55
|
+
|
|
56
|
+
for n in range(1, max_flips + 1):
|
|
57
|
+
prob = (0.5) ** n # Probability of exactly n flips
|
|
58
|
+
payout = 2.0 ** n # Payout is 2^n (use 2.0 to keep as float)
|
|
59
|
+
outcomes.append(payout)
|
|
60
|
+
probabilities.append(prob)
|
|
61
|
+
|
|
62
|
+
return np.array(outcomes, dtype=float), np.array(probabilities, dtype=float)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
"""Calculate maximum payment for St. Petersburg game across different scenarios."""
|
|
67
|
+
|
|
68
|
+
print("=" * 80)
|
|
69
|
+
print("ST. PETERSBURG PARADOX: WHAT WOULD YOU PAY TO PLAY?")
|
|
70
|
+
print("=" * 80)
|
|
71
|
+
print()
|
|
72
|
+
print("The Game:")
|
|
73
|
+
print(" - Flip a fair coin repeatedly until tails")
|
|
74
|
+
print(" - Payout is $2^n where n is the number of flips")
|
|
75
|
+
print(" - Expected value is INFINITE")
|
|
76
|
+
print()
|
|
77
|
+
print("The Question:")
|
|
78
|
+
print(" Given your wealth, what's the maximum you'd rationally pay to play once?")
|
|
79
|
+
print()
|
|
80
|
+
print("=" * 80)
|
|
81
|
+
print()
|
|
82
|
+
|
|
83
|
+
# Different bankroll levels to test
|
|
84
|
+
bankrolls = [1, 10, 100, 1_000]
|
|
85
|
+
|
|
86
|
+
# Create strategy instances - all 9 strategies!
|
|
87
|
+
strategies = {
|
|
88
|
+
# Utility-based strategies
|
|
89
|
+
"Kelly": KellyCriterion(
|
|
90
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0
|
|
91
|
+
),
|
|
92
|
+
"Half Kelly": FractionalKellyCriterion(
|
|
93
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0, fraction=0.5
|
|
94
|
+
),
|
|
95
|
+
"Drawdown Kelly": DrawdownAdjustedKelly(
|
|
96
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0, max_acceptable_drawdown=0.2
|
|
97
|
+
),
|
|
98
|
+
"Optimal F": OptimalF(
|
|
99
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0, win_rate=0.55
|
|
100
|
+
),
|
|
101
|
+
"Merton (γ=2.0)": MertonShare(
|
|
102
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0, risk_aversion=2.0
|
|
103
|
+
),
|
|
104
|
+
"Merton (γ=5.0)": MertonShare(
|
|
105
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0, risk_aversion=5.0
|
|
106
|
+
),
|
|
107
|
+
# Rule-based strategies
|
|
108
|
+
"Naive": NaiveStrategy(
|
|
109
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0
|
|
110
|
+
),
|
|
111
|
+
"Fixed 5%": FixedFractionStrategy(
|
|
112
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0, fraction=0.05
|
|
113
|
+
),
|
|
114
|
+
"Dynamic 10%": DynamicBankrollManagement(
|
|
115
|
+
payoff=1.0, loss=1.0, transaction_cost=0.0, base_fraction=0.1, window_size=10
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Get St. Petersburg outcomes
|
|
120
|
+
# Using 1,000 flips gives EV = $1,000, approaching the infinite EV of the true paradox
|
|
121
|
+
# Each flip contributes exactly $1 to expected value: (1/2^n) × 2^n = 1
|
|
122
|
+
# True paradox has infinite flips → infinite EV, but we cap at 1000 due to float64 limits
|
|
123
|
+
outcomes, probabilities = get_st_petersburg_outcomes(max_flips=1_000)
|
|
124
|
+
|
|
125
|
+
print("Calculating optimal entry prices for each strategy...")
|
|
126
|
+
print()
|
|
127
|
+
|
|
128
|
+
# Calculate results
|
|
129
|
+
all_results = []
|
|
130
|
+
|
|
131
|
+
for bankroll in bankrolls:
|
|
132
|
+
row = {"Bankroll": f"${bankroll:,}"}
|
|
133
|
+
|
|
134
|
+
for strategy_name, strategy in strategies.items():
|
|
135
|
+
# Use the strategy's calculate_max_entry_price method
|
|
136
|
+
max_price = strategy.calculate_max_entry_price(
|
|
137
|
+
outcomes=outcomes,
|
|
138
|
+
probabilities=probabilities,
|
|
139
|
+
current_wealth=bankroll
|
|
140
|
+
)
|
|
141
|
+
row[f"{strategy_name}"] = max_price
|
|
142
|
+
row[f"{strategy_name} (%BR)"] = (max_price / bankroll) * 100
|
|
143
|
+
|
|
144
|
+
all_results.append(row)
|
|
145
|
+
|
|
146
|
+
# Create DataFrame
|
|
147
|
+
df_results = pd.DataFrame(all_results)
|
|
148
|
+
|
|
149
|
+
# Display price table
|
|
150
|
+
print("\nMAXIMUM ENTRY PRICE BY STRATEGY AND WEALTH LEVEL:")
|
|
151
|
+
print("=" * 80)
|
|
152
|
+
print()
|
|
153
|
+
|
|
154
|
+
# Create price-only table for display
|
|
155
|
+
price_cols = ["Bankroll"] + list(strategies.keys())
|
|
156
|
+
df_prices = df_results[price_cols].copy()
|
|
157
|
+
|
|
158
|
+
# Format prices as currency
|
|
159
|
+
for col in strategies.keys():
|
|
160
|
+
df_prices[col] = df_results[col].apply(lambda x: f"${x:.2f}")
|
|
161
|
+
|
|
162
|
+
print(df_prices.to_string(index=False))
|
|
163
|
+
|
|
164
|
+
# Create visualization
|
|
165
|
+
print("Creating visualization...")
|
|
166
|
+
|
|
167
|
+
# Single chart: Entry price by strategy by bankroll
|
|
168
|
+
fig, ax = plt.subplots(figsize=(16, 10))
|
|
169
|
+
|
|
170
|
+
# Prepare data for plotting
|
|
171
|
+
bankroll_labels = [f"${b:,}" for b in bankrolls]
|
|
172
|
+
x = np.arange(len(bankroll_labels))
|
|
173
|
+
n_strategies = len(strategies)
|
|
174
|
+
width = 0.08 # Width of bars
|
|
175
|
+
|
|
176
|
+
# Define colors for different strategy types
|
|
177
|
+
colors = {
|
|
178
|
+
'Kelly': '#1f77b4',
|
|
179
|
+
'Half Kelly': '#aec7e8',
|
|
180
|
+
'Drawdown Kelly': '#7fbfe8',
|
|
181
|
+
'Optimal F': '#4a90c4',
|
|
182
|
+
'Merton (γ=2.0)': '#ff7f0e',
|
|
183
|
+
'Merton (γ=5.0)': '#ffbb78',
|
|
184
|
+
'Naive': '#2ca02c',
|
|
185
|
+
'Fixed 5%': '#d62728',
|
|
186
|
+
'Dynamic 10%': '#9467bd',
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Plot bars for each strategy
|
|
190
|
+
for i, strategy_name in enumerate(strategies.keys()):
|
|
191
|
+
prices = [all_results[j][strategy_name] for j in range(len(bankrolls))]
|
|
192
|
+
offset = (i - n_strategies / 2 + 0.5) * width
|
|
193
|
+
ax.bar(x + offset, prices, width, label=strategy_name,
|
|
194
|
+
color=colors.get(strategy_name, f'C{i}'))
|
|
195
|
+
|
|
196
|
+
ax.set_xlabel('Bankroll Level', fontsize=12, fontweight='bold')
|
|
197
|
+
ax.set_ylabel('Maximum Entry Price ($, log scale)', fontsize=12, fontweight='bold')
|
|
198
|
+
ax.set_title('St. Petersburg Paradox: Maximum Entry Price by Strategy\n' +
|
|
199
|
+
'(Despite Infinite Expected Value, Agents Pay Finite Amounts)',
|
|
200
|
+
fontsize=14, fontweight='bold', pad=20)
|
|
201
|
+
ax.set_xticks(x)
|
|
202
|
+
ax.set_xticklabels(bankroll_labels, rotation=0)
|
|
203
|
+
ax.set_yscale('log')
|
|
204
|
+
ax.legend(loc='upper left', fontsize=9, ncol=2)
|
|
205
|
+
ax.grid(axis='y', alpha=0.3, linestyle='--', which='both')
|
|
206
|
+
|
|
207
|
+
# Add annotation
|
|
208
|
+
ax.text(0.98, 0.02,
|
|
209
|
+
'Utility-based strategies (Kelly, Merton, Optimal F) analyze risk\n' +
|
|
210
|
+
'Rule-based strategies (Fixed, Dynamic) apply mechanical rules\n' +
|
|
211
|
+
'Naive pays expected value ($1,000 for 1k flips), capped at wealth',
|
|
212
|
+
transform=ax.transAxes, fontsize=9,
|
|
213
|
+
verticalalignment='bottom', horizontalalignment='right',
|
|
214
|
+
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.3))
|
|
215
|
+
|
|
216
|
+
plt.tight_layout()
|
|
217
|
+
|
|
218
|
+
# Create output directory
|
|
219
|
+
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
|
|
220
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
221
|
+
|
|
222
|
+
# Save figure
|
|
223
|
+
fig_path = os.path.join(output_dir, "st_petersburg_paradox.png")
|
|
224
|
+
plt.savefig(fig_path, dpi=300, bbox_inches='tight')
|
|
225
|
+
print(f"\n✓ Chart saved to: {fig_path}")
|
|
226
|
+
|
|
227
|
+
# Save CSV with better formatting
|
|
228
|
+
csv_data = []
|
|
229
|
+
for result in all_results:
|
|
230
|
+
row = {"Bankroll": result["Bankroll"]}
|
|
231
|
+
for strategy_name in strategies.keys():
|
|
232
|
+
row[f"{strategy_name} ($)"] = f"${result[strategy_name]:.2f}"
|
|
233
|
+
row[f"{strategy_name} (%)"] = f"{result[f'{strategy_name} (%BR)']:.3f}%"
|
|
234
|
+
csv_data.append(row)
|
|
235
|
+
|
|
236
|
+
df_csv = pd.DataFrame(csv_data)
|
|
237
|
+
csv_path = os.path.join(output_dir, "st_petersburg_paradox.csv")
|
|
238
|
+
df_csv.to_csv(csv_path, index=False)
|
|
239
|
+
print(f"✓ Data saved to: {csv_path}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
main()
|