A computational economics research tool that models the "ostrich effect" in investing -- the tendency for investors to avoid checking their portfolio when markets are doing poorly.
Given an investor's historical pattern of "looking" or "not looking" at their portfolio, this program uses dynamic programming to find the optimal decision rule for a hypothetical rational investor with loss-averse preferences. It then compares the model's predicted look/don't-look decisions against the actual observed behavior, producing an R-squared goodness-of-fit measure.
The model treats the investor as an infinite-horizon utility maximizer whose period utility is defined over portfolio gains and losses relative to a benchmark (rather than consumption). The state space is four-dimensional:
- Approximate wealth (what the investor believes their portfolio is worth)
- Benchmark (reference point for gains/losses)
- Time since last looked
- Wealth at time of last look
| Parameter | Symbol | Description |
|---|---|---|
alpha |
A | Utility multiplier when looking (attention premium) |
beta |
B | Discount factor (0 = closed-form solver available) |
gamma |
G | Loss aversion multiplier |
delta |
D | Benchmark update weight when looking |
theta |
T | Benchmark update weight when not looking |
- General (Riemann sum): Full backward induction with numerical integration (beta != 0)
- Beta-zero (closed form): Analytical expected utility when beta = 0
- Standard utility: Linear loss aversion
- Exponential utility: Exponential loss aversion
- C++17 compiler (g++ or clang++)
- CMake >= 3.15
- Python >= 3.9 (for the Python interface)
- pybind11 (for the Python extension)
git clone https://github.com/jcharit1/Ostrich-Project.git
cd Ostrich-Project
mkdir build && cd build
cmake ..
makeThis produces:
build/ostrich-- standalone CLI executablebuild/libostrich_lib.a-- static C++ librarybuild/ostrich_core.*.so-- Python extension module (if pybind11 is found)
pip install pybind11
pip install -e ".[all]" # Installs with SQLAlchemy + pandas supportOr install with only the dependencies you need:
pip install -e . # Core only (no database/pandas)
pip install -e ".[db]" # With SQLAlchemy for database storage
pip install -e ".[analysis]" # With pandas for DataFramescd build
cmake .. -DBUILD_LEGACY=ON
makeInvestor data files are whitespace-delimited with 4 columns and no header:
ID LOOK PERSONAL_RETURN MARKET_RETURN
74 1 1.415634 1.283377
74 1 0.856195 0.575592
74 0 0.654541 2.312507
74 0 0.540954 0.254741
- ID: Investor identifier
- LOOK: 1 = looked, 0 = didn't look, -999 = missing
- PERSONAL_RETURN: Gross personal portfolio return (not log)
- MARKET_RETURN: Gross market return (not log)
Test data is included at test/test_investor_data/88808sample74.txt (504 rows).
./build/ostrich \
test/test_investor_data/88808sample74.txt \
88808 504 Test \
0.0 0.8 3.0 1.0 0.5 \
10.0 10 1 RS 1000 0.001 0.001 TestArguments in order: data_file, sample, nrows, param_name, alpha, beta, gamma, delta, theta, t_max, npart, z, EU_type, mc_num, alpha_ci, error_pct, sys.
Output (to stdout):
74, 0, 0.8, 3, 1, 0.5, 10, 0.529762
from ostrich import OstrichModel
model = OstrichModel(
data_file="test/test_investor_data/88808sample74.txt",
max_row=504,
num_partitions=10,
t_max=10,
)
result = model.run(alpha=0.0, beta=0.8, gamma=3.0, delta=1.0, theta=0.5)
print(result)
# {'investor_id': 74.0, 'alpha': 0.0, 'beta': 0.8, 'gamma': 3.0,
# 'delta': 1.0, 'theta': 0.5, 'num_partitions': 10, 'fit': 0.5297...}from ostrich import OstrichModel
model = OstrichModel(
data_file="test/test_investor_data/88808sample74.txt",
max_row=504,
num_partitions=10,
)
results = model.run_grid(
alpha_values=[0.0, 1.0, 2.0],
beta_values=[0.0, 0.4, 0.8],
gamma_values=[2.0, 3.0, 5.0],
delta_values=[0.5, 1.0],
theta_values=[0.3, 0.5, 0.7],
)
# Results are sorted by fit (best first)
print(f"Tested {len(results)} combinations")
print(f"Best fit: {results[0]['fit']:.4f}")
print(f"Best params: alpha={results[0]['alpha']}, beta={results[0]['beta']}, "
f"gamma={results[0]['gamma']}, delta={results[0]['delta']}, "
f"theta={results[0]['theta']}")from ostrich import OstrichModel, ResultStore
store = ResultStore(mode="file", output_dir="results/", file_format="csv")
model = OstrichModel("test/test_investor_data/88808sample74.txt", max_row=504)
results = model.run_grid(
alpha_values=[0.0, 2.0],
beta_values=[0.0, 0.8],
gamma_values=[3.0],
delta_values=[1.0],
theta_values=[0.5],
store=store,
)
# Results are written to results/results_inv74.csv as they complete
print(f"Saved {len(store)} results to results/")from ostrich import ResultStore
store = ResultStore(mode="file", output_dir="results/", file_format="json")
# ... run model with store=store ...
# Each result is appended as a JSON line to results/result_inv74_n10.jsonfrom ostrich import OstrichModel, ResultStore
store = ResultStore(mode="database", db_url="sqlite:///ostrich_results.db")
model = OstrichModel("test/test_investor_data/88808sample74.txt", max_row=504)
result = model.run(alpha=0.0, beta=0.8, gamma=3.0, delta=1.0, theta=0.5, store=store)
# Results are inserted into the 'ostrich_results' table
print(store.get_best(n=3)) # Top 3 by fitfrom ostrich import ResultStore
store = ResultStore(
mode="database",
db_url="postgresql://user:password@host:5432/mydb",
table_name="ostrich_fits",
)
# ... run model with store=store ...
# Results go to the 'ostrich_fits' table in PostgreSQLfrom ostrich import OstrichModel, ResultStore
store = ResultStore(mode="memory")
model = OstrichModel("test/test_investor_data/88808sample74.txt", max_row=504)
results = model.run_grid(
alpha_values=[0.0, 1.0, 2.0],
beta_values=[0.0, 0.8],
gamma_values=[3.0],
delta_values=[1.0],
theta_values=[0.5],
store=store,
)
df = store.to_dataframe()
print(df.sort_values("fit", ascending=False))
# investor_id alpha beta gamma delta theta num_partitions fit
# 3 74.0 0.0 0.8 3.0 1.0 0.5 10 0.529762
# 0 74.0 0.0 0.0 3.0 1.0 0.5 10 0.486111
# ...model = OstrichModel(
data_file="test/test_investor_data/88808sample74.txt",
max_row=504,
utility_type="exponential", # Instead of "standard"
)
result = model.run(alpha=0.0, beta=0.8, gamma=3.0, delta=1.0, theta=0.5)# Single run
python -m ostrich run test/test_investor_data/88808sample74.txt 504 \
--alpha 0.0 --beta 0.8 --gamma 3.0 --delta 1.0 --theta 0.5
# Grid search with CSV output
python -m ostrich grid test/test_investor_data/88808sample74.txt 504 \
--params params.json --output results/
# Grid search with database output
python -m ostrich grid test/test_investor_data/88808sample74.txt 504 \
--params params.json --db-url sqlite:///results.dbWhere params.json looks like:
{
"alpha": [0.0, 1.0, 2.0],
"beta": [0.0, 0.4, 0.8],
"gamma": [2.0, 3.0],
"delta": [0.5, 1.0],
"theta": [0.3, 0.5]
}#include "ostrich/ostrich.h"
#include <iostream>
int main() {
ostrich::ModelParams params;
params.alpha = 0.0;
params.beta = 0.8;
params.gamma = 3.0;
params.delta = 1.0;
params.theta = 0.5;
ostrich::NumericalConfig config;
config.num_partitions = 10;
config.t_max = 10;
config.mc_num = 1000;
config.alpha_ci = 0.001;
config.error_pct = 0.001;
config.method = ostrich::IntegrationMethod::RIEMANN_SUM;
config.utility_type = ostrich::UtilityType::STANDARD;
config.solver_type = ostrich::SolverType::GENERAL;
auto result = ostrich::run_model("data.txt", 504, params, config);
std::cout << "Fit: " << result.fit << std::endl;
return 0;
}Compile with:
g++ -std=c++17 -O3 -I include my_program.cpp -L build -lostrich_lib -o my_programOstrich-Project/
include/ostrich/ # Refactored C++ headers (struct-based API)
src/ostrich/ # Refactored C++ implementation
python/ostrich/ # Python frontend package
include/ # Legacy C headers (preserved)
src/ # Legacy C++ source (preserved)
test/ # Test data and parameters
data/ # Parameter tables
CMakeLists.txt # Build system (new code)
makefile # Build system (legacy code)
pyproject.toml # Python package config
setup.py # Python build with C++ extension
- Fork it
- Create your feature branch:
git checkout -b my-new-feature - Commit your changes:
git commit -am 'Add some feature' - Push to the branch:
git push origin my-new-feature - Submit a pull request
MIT License
Copyright (c) 2016 Jimmy Charite
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.