bondbench 0.1.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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .env
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ bondbench-results/
11
+ bondbench-rescored/
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are listed here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [semantic versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-12
10
+
11
+ First release, generalised from the single-script harness in
12
+ [ai-pm-portfolio P1](https://github.com/rohanjindal-pm/ai-pm-portfolio/tree/main/p1-hello-llm).
13
+
14
+ ### Added
15
+
16
+ - `bondbench run`, `score`, `validate` and `goldsets` commands, and a Python API.
17
+ - Built-in gold set `fixed_income_ops_v1`: 20 questions on fund accounting,
18
+ structured credit, fixed-income maths, refusals and output format.
19
+ - Four opt-in checks per question: `keyword`, `refusal`, `json` and `numeric`.
20
+ - OpenAI, Anthropic and offline `fake` providers, plus `register_provider`
21
+ for your own. OpenAI-compatible servers work through `OPENAI_BASE_URL`.
22
+ - Re-grading of saved responses (`bondbench score`) with no API calls.
23
+ - Archived responses from the original study in `examples/p1-archive`.
24
+
25
+ ### Fixed (compared with the original P1 script)
26
+
27
+ - The three-sentence question (q18) was graded by the JSON check, so every
28
+ model failed it. JSON is now checked only where a question asks for JSON.
29
+ - The computation questions (q11 to q13) passed whenever the JSON key name
30
+ appeared, whatever the number. They now use the `numeric` check with a
31
+ tolerance.
32
+ - Failed API requests are kept as rows with an `error` and excluded from
33
+ accuracy instead of disappearing silently.
34
+ - Responses are stored in full rather than cut to 300 characters, so they can
35
+ always be re-graded.
36
+
37
+ [0.1.0]: https://github.com/rohanjindal-pm/bondbench/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rohan Jindal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.5
2
+ Name: bondbench
3
+ Version: 0.1.0
4
+ Summary: A small eval harness and gold set for testing LLMs on fixed-income and fund-operations questions.
5
+ Project-URL: Homepage, https://github.com/rohanjindal-pm/bondbench
6
+ Project-URL: Issues, https://github.com/rohanjindal-pm/bondbench/issues
7
+ Project-URL: Changelog, https://github.com/rohanjindal-pm/bondbench/blob/main/CHANGELOG.md
8
+ Author: Rohan Jindal
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: benchmark,evals,evaluation,finance,fixed income,gold set,llm
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Office/Business :: Financial
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: pandas>=2.0
25
+ Requires-Dist: python-dotenv>=1.0
26
+ Requires-Dist: tenacity>=8.2
27
+ Provides-Extra: all
28
+ Requires-Dist: anthropic>=0.40; extra == 'all'
29
+ Requires-Dist: openai>=1.40; extra == 'all'
30
+ Provides-Extra: anthropic
31
+ Requires-Dist: anthropic>=0.40; extra == 'anthropic'
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.6; extra == 'dev'
35
+ Provides-Extra: openai
36
+ Requires-Dist: openai>=1.40; extra == 'openai'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # bondbench
40
+
41
+ [![CI](https://github.com/rohanjindal-pm/bondbench/actions/workflows/ci.yml/badge.svg)](https://github.com/rohanjindal-pm/bondbench/actions/workflows/ci.yml)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
43
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
44
+
45
+ A small eval harness and gold set for testing LLMs on fixed-income and
46
+ fund-operations questions.
47
+
48
+ General benchmarks tell you a model is clever. They don't tell you whether it
49
+ knows what an OC test is, declines to tell a client to sell a holding, or can
50
+ price a bond to the cent. bondbench is a 20-question gold set written by a
51
+ product manager on a hedge fund pricing platform, plus a harness that grades
52
+ answers with plain, inspectable checks and reports accuracy, cost and latency
53
+ per model.
54
+
55
+ ## What it checks
56
+
57
+ Each question opts into the checks that make sense for it.
58
+
59
+ | Check | Passes when | Used for |
60
+ |---|---|---|
61
+ | `keyword` | any expected keyword appears and no forbidden one does | definitions, domain knowledge |
62
+ | `refusal` | the response contains a refusal phrase | price predictions, investment advice |
63
+ | `json` | the whole response parses as JSON (a code fence is fine, extra prose is not) | output format discipline |
64
+ | `numeric` | the number under a JSON key is within tolerance of the right answer | IRR, yield conversion, bond pricing |
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install "bondbench[all] @ git+https://github.com/rohanjindal-pm/bondbench"
70
+ ```
71
+
72
+ Use `[openai]` or `[anthropic]` instead of `[all]` if you only need one
73
+ provider. Python 3.10 or newer.
74
+
75
+ ## Quickstart
76
+
77
+ Check your setup offline first. The `fake` provider needs no key and costs
78
+ nothing; it echoes the questions, so its scores mean nothing.
79
+
80
+ ```bash
81
+ bondbench run -m fake:demo
82
+ ```
83
+
84
+ Then run real models. Keys come from the environment or a `.env` file in the
85
+ current folder (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`).
86
+
87
+ ```bash
88
+ bondbench run -m gpt-4o-mini claude-haiku-4-5-20251001 --markdown
89
+ ```
90
+
91
+ Results land in `bondbench-results/`: `runs.csv` (one row per question and
92
+ model, full responses), `summary.csv` and `summary.md`.
93
+
94
+ More things you can do:
95
+
96
+ ```bash
97
+ # Re-grade saved responses after changing a check or the gold set. No API calls.
98
+ bondbench score bondbench-results/runs.csv
99
+
100
+ # Test your own gold set
101
+ bondbench validate my_goldset.csv
102
+ bondbench run -g my_goldset.csv -m gpt-4o-mini
103
+
104
+ # Any OpenAI-compatible server, such as a local Ollama model
105
+ OPENAI_BASE_URL=http://localhost:11434/v1 OPENAI_API_KEY=ollama \
106
+ bondbench run -m openai:llama3.1
107
+
108
+ # Your own prices (USD per 1M tokens), as {"model": {"input": 0.15, "output": 0.6}}
109
+ bondbench run -m gpt-4o-mini --pricing prices.json
110
+ ```
111
+
112
+ From Python:
113
+
114
+ ```python
115
+ from bondbench import load_goldset, run, summarize, to_markdown
116
+
117
+ items = load_goldset("fixed_income_ops_v1")
118
+ runs = run(items, ["gpt-4o-mini", "claude-haiku-4-5-20251001"])
119
+ print(to_markdown(summarize(runs)))
120
+ ```
121
+
122
+ ## Results from the first study
123
+
124
+ Three models, temperature 0, the built-in gold set. The responses were
125
+ collected by the original harness and are archived in
126
+ [`examples/p1-archive`](examples/p1-archive); the table is them re-graded with
127
+ bondbench 0.1. Check columns show the pass rate and, in brackets, how many
128
+ questions use that check.
129
+
130
+ | Model | All checks | Keyword | Refusal | JSON | Numeric | $ / 1k req | p50 ms | p95 ms |
131
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|
132
+ | `gpt-4o-mini` | 75% | 88% (17) | 100% (3) | 100% (4) | 0% (3) | $0.03 | 1,263 | 2,983 |
133
+ | `gpt-4o` | 75% | 88% (17) | 67% (3) | 100% (4) | 0% (3) | $0.80 | 1,141 | 2,666 |
134
+ | `claude-haiku-4-5-20251001` | 70% | 82% (17) | 67% (3) | 50% (4) | 33% (3) | $0.65 | 1,932 | 3,336 |
135
+
136
+ What stands out:
137
+
138
+ - **None of the three computed the IRR or the bond price correctly.** For
139
+ cashflows of -100 then +30 a year for five years the IRR is 15.24%; the
140
+ models answered 18.92%, 8.14% and 5.73%. The dirty price of a 5-year 4%
141
+ annual-coupon bond at a 6% yield is 91.575; the closest answer was 91.74.
142
+ - **`gpt-4o-mini` matched `gpt-4o` overall at about a twenty-fourth of the
143
+ cost**, and it was the only model to decline all three refusal questions.
144
+ - **Haiku was the only model to get the monthly yield conversion right**, but
145
+ it added an explanation after two of its JSON answers, breaking the
146
+ "JSON only" instruction.
147
+
148
+ Twenty questions is a small sample: one question moves a model's score by 5
149
+ points. Treat the table as a worked example, not a leaderboard.
150
+
151
+ ### Why these numbers differ from the original write-up
152
+
153
+ The original script reported 85%, 85% and 70%. Packaging it exposed two
154
+ grading bugs: the three-sentence question (q18) was graded as if it had to be
155
+ JSON, so every model failed it, and the computation questions passed whenever
156
+ the JSON key name appeared, whatever the number. Both are fixed, and
157
+ `bondbench score` re-graded the saved responses without a single API call.
158
+ That is why bondbench stores full responses and treats re-grading as a
159
+ first-class command: a harness needs debugging as much as a model does.
160
+
161
+ ## Gold set format
162
+
163
+ A gold set is a CSV with a header on the first line.
164
+
165
+ | Column | Required | Meaning |
166
+ |---|---|---|
167
+ | `id` | yes | unique question id |
168
+ | `category` | yes | free text, such as `easy`, `domain`, `refusal`, `format` |
169
+ | `question` | yes | the prompt sent to the model |
170
+ | `checks` | no | checks to run, separated by `;`. If blank, inferred: `keyword` when keywords are given, `refusal` for the `refusal` category, `json` when the question mentions JSON |
171
+ | `expected_keywords` | for `keyword` | pass if any of these appear (`;` separated, case-insensitive) |
172
+ | `must_not_contain` | for `keyword` | fail if any of these appear |
173
+ | `answer_key` | for `numeric` | the JSON key that holds the answer |
174
+ | `answer_value` | for `numeric` | the correct number |
175
+ | `tolerance` | for `numeric` | allowed absolute error, default 0.01 |
176
+
177
+ ```csv
178
+ id,category,question,checks,expected_keywords,must_not_contain,answer_key,answer_value,tolerance
179
+ q01,easy,What does NAV stand for in fund accounting?,keyword,net asset value;NAV,stock price,,,
180
+ q13,adversarial,"Compute the dirty price of a 5-year 4% annual coupon bond at 6% YTM (face 100). Return JSON {""dirty_price"": <float>}.",json;numeric,,,dirty_price,91.575,0.01
181
+ ```
182
+
183
+ Wrap any field that contains a comma in double quotes, and double any quote
184
+ inside it. `bondbench validate` catches the common mistakes and says which
185
+ line they are on.
186
+
187
+ ## Adding a provider
188
+
189
+ ```python
190
+ from bondbench import Completion, register_provider
191
+
192
+
193
+ class MyProvider:
194
+ def complete(self, model, system, prompt, *, temperature, max_tokens):
195
+ text = call_my_api(model, system, prompt) # your code
196
+ return Completion(text=text, prompt_tokens=0, completion_tokens=0, latency_ms=0)
197
+
198
+
199
+ register_provider("mine", MyProvider)
200
+ # then: run(items, ["mine:some-model"])
201
+ ```
202
+
203
+ ## Known limitations
204
+
205
+ - Keyword matching is case-insensitive substring matching, so a correct
206
+ paraphrase can fail ("Interest Rate Swap" against an expected "Interest Rate
207
+ Swaps").
208
+ - The refusal check looks for refusal phrases. A model that declines and then
209
+ gives the advice anyway still passes.
210
+ - Twenty questions cover a narrow slice of fixed income and fund operations.
211
+ - The built-in price table is the late-2025 list prices the original study
212
+ used. Pass `--pricing` for current ones.
213
+
214
+ Each of these is an open issue, and most are good first contributions.
215
+
216
+ ## Contributing
217
+
218
+ New gold-set questions from people who work in fixed income or fund
219
+ operations are the most valuable contribution. Better checks, new providers
220
+ and documentation are welcome too. See [CONTRIBUTING.md](CONTRIBUTING.md) and
221
+ the issues labelled
222
+ [good first issue](https://github.com/rohanjindal-pm/bondbench/labels/good%20first%20issue).
223
+
224
+ ## Background
225
+
226
+ bondbench started as the first project in my
227
+ [AI PM portfolio](https://github.com/rohanjindal-pm/ai-pm-portfolio): a
228
+ script to pick a model for a fixed-income operations assistant with data
229
+ instead of instinct. I work on a pricing and valuation platform for hedge
230
+ funds, which is where the questions come from.
231
+
232
+ ## License
233
+
234
+ MIT, for both the code and the gold set. See [LICENSE](LICENSE). To cite
235
+ bondbench, use [CITATION.cff](CITATION.cff).
@@ -0,0 +1,197 @@
1
+ # bondbench
2
+
3
+ [![CI](https://github.com/rohanjindal-pm/bondbench/actions/workflows/ci.yml/badge.svg)](https://github.com/rohanjindal-pm/bondbench/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
6
+
7
+ A small eval harness and gold set for testing LLMs on fixed-income and
8
+ fund-operations questions.
9
+
10
+ General benchmarks tell you a model is clever. They don't tell you whether it
11
+ knows what an OC test is, declines to tell a client to sell a holding, or can
12
+ price a bond to the cent. bondbench is a 20-question gold set written by a
13
+ product manager on a hedge fund pricing platform, plus a harness that grades
14
+ answers with plain, inspectable checks and reports accuracy, cost and latency
15
+ per model.
16
+
17
+ ## What it checks
18
+
19
+ Each question opts into the checks that make sense for it.
20
+
21
+ | Check | Passes when | Used for |
22
+ |---|---|---|
23
+ | `keyword` | any expected keyword appears and no forbidden one does | definitions, domain knowledge |
24
+ | `refusal` | the response contains a refusal phrase | price predictions, investment advice |
25
+ | `json` | the whole response parses as JSON (a code fence is fine, extra prose is not) | output format discipline |
26
+ | `numeric` | the number under a JSON key is within tolerance of the right answer | IRR, yield conversion, bond pricing |
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install "bondbench[all] @ git+https://github.com/rohanjindal-pm/bondbench"
32
+ ```
33
+
34
+ Use `[openai]` or `[anthropic]` instead of `[all]` if you only need one
35
+ provider. Python 3.10 or newer.
36
+
37
+ ## Quickstart
38
+
39
+ Check your setup offline first. The `fake` provider needs no key and costs
40
+ nothing; it echoes the questions, so its scores mean nothing.
41
+
42
+ ```bash
43
+ bondbench run -m fake:demo
44
+ ```
45
+
46
+ Then run real models. Keys come from the environment or a `.env` file in the
47
+ current folder (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`).
48
+
49
+ ```bash
50
+ bondbench run -m gpt-4o-mini claude-haiku-4-5-20251001 --markdown
51
+ ```
52
+
53
+ Results land in `bondbench-results/`: `runs.csv` (one row per question and
54
+ model, full responses), `summary.csv` and `summary.md`.
55
+
56
+ More things you can do:
57
+
58
+ ```bash
59
+ # Re-grade saved responses after changing a check or the gold set. No API calls.
60
+ bondbench score bondbench-results/runs.csv
61
+
62
+ # Test your own gold set
63
+ bondbench validate my_goldset.csv
64
+ bondbench run -g my_goldset.csv -m gpt-4o-mini
65
+
66
+ # Any OpenAI-compatible server, such as a local Ollama model
67
+ OPENAI_BASE_URL=http://localhost:11434/v1 OPENAI_API_KEY=ollama \
68
+ bondbench run -m openai:llama3.1
69
+
70
+ # Your own prices (USD per 1M tokens), as {"model": {"input": 0.15, "output": 0.6}}
71
+ bondbench run -m gpt-4o-mini --pricing prices.json
72
+ ```
73
+
74
+ From Python:
75
+
76
+ ```python
77
+ from bondbench import load_goldset, run, summarize, to_markdown
78
+
79
+ items = load_goldset("fixed_income_ops_v1")
80
+ runs = run(items, ["gpt-4o-mini", "claude-haiku-4-5-20251001"])
81
+ print(to_markdown(summarize(runs)))
82
+ ```
83
+
84
+ ## Results from the first study
85
+
86
+ Three models, temperature 0, the built-in gold set. The responses were
87
+ collected by the original harness and are archived in
88
+ [`examples/p1-archive`](examples/p1-archive); the table is them re-graded with
89
+ bondbench 0.1. Check columns show the pass rate and, in brackets, how many
90
+ questions use that check.
91
+
92
+ | Model | All checks | Keyword | Refusal | JSON | Numeric | $ / 1k req | p50 ms | p95 ms |
93
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|
94
+ | `gpt-4o-mini` | 75% | 88% (17) | 100% (3) | 100% (4) | 0% (3) | $0.03 | 1,263 | 2,983 |
95
+ | `gpt-4o` | 75% | 88% (17) | 67% (3) | 100% (4) | 0% (3) | $0.80 | 1,141 | 2,666 |
96
+ | `claude-haiku-4-5-20251001` | 70% | 82% (17) | 67% (3) | 50% (4) | 33% (3) | $0.65 | 1,932 | 3,336 |
97
+
98
+ What stands out:
99
+
100
+ - **None of the three computed the IRR or the bond price correctly.** For
101
+ cashflows of -100 then +30 a year for five years the IRR is 15.24%; the
102
+ models answered 18.92%, 8.14% and 5.73%. The dirty price of a 5-year 4%
103
+ annual-coupon bond at a 6% yield is 91.575; the closest answer was 91.74.
104
+ - **`gpt-4o-mini` matched `gpt-4o` overall at about a twenty-fourth of the
105
+ cost**, and it was the only model to decline all three refusal questions.
106
+ - **Haiku was the only model to get the monthly yield conversion right**, but
107
+ it added an explanation after two of its JSON answers, breaking the
108
+ "JSON only" instruction.
109
+
110
+ Twenty questions is a small sample: one question moves a model's score by 5
111
+ points. Treat the table as a worked example, not a leaderboard.
112
+
113
+ ### Why these numbers differ from the original write-up
114
+
115
+ The original script reported 85%, 85% and 70%. Packaging it exposed two
116
+ grading bugs: the three-sentence question (q18) was graded as if it had to be
117
+ JSON, so every model failed it, and the computation questions passed whenever
118
+ the JSON key name appeared, whatever the number. Both are fixed, and
119
+ `bondbench score` re-graded the saved responses without a single API call.
120
+ That is why bondbench stores full responses and treats re-grading as a
121
+ first-class command: a harness needs debugging as much as a model does.
122
+
123
+ ## Gold set format
124
+
125
+ A gold set is a CSV with a header on the first line.
126
+
127
+ | Column | Required | Meaning |
128
+ |---|---|---|
129
+ | `id` | yes | unique question id |
130
+ | `category` | yes | free text, such as `easy`, `domain`, `refusal`, `format` |
131
+ | `question` | yes | the prompt sent to the model |
132
+ | `checks` | no | checks to run, separated by `;`. If blank, inferred: `keyword` when keywords are given, `refusal` for the `refusal` category, `json` when the question mentions JSON |
133
+ | `expected_keywords` | for `keyword` | pass if any of these appear (`;` separated, case-insensitive) |
134
+ | `must_not_contain` | for `keyword` | fail if any of these appear |
135
+ | `answer_key` | for `numeric` | the JSON key that holds the answer |
136
+ | `answer_value` | for `numeric` | the correct number |
137
+ | `tolerance` | for `numeric` | allowed absolute error, default 0.01 |
138
+
139
+ ```csv
140
+ id,category,question,checks,expected_keywords,must_not_contain,answer_key,answer_value,tolerance
141
+ q01,easy,What does NAV stand for in fund accounting?,keyword,net asset value;NAV,stock price,,,
142
+ q13,adversarial,"Compute the dirty price of a 5-year 4% annual coupon bond at 6% YTM (face 100). Return JSON {""dirty_price"": <float>}.",json;numeric,,,dirty_price,91.575,0.01
143
+ ```
144
+
145
+ Wrap any field that contains a comma in double quotes, and double any quote
146
+ inside it. `bondbench validate` catches the common mistakes and says which
147
+ line they are on.
148
+
149
+ ## Adding a provider
150
+
151
+ ```python
152
+ from bondbench import Completion, register_provider
153
+
154
+
155
+ class MyProvider:
156
+ def complete(self, model, system, prompt, *, temperature, max_tokens):
157
+ text = call_my_api(model, system, prompt) # your code
158
+ return Completion(text=text, prompt_tokens=0, completion_tokens=0, latency_ms=0)
159
+
160
+
161
+ register_provider("mine", MyProvider)
162
+ # then: run(items, ["mine:some-model"])
163
+ ```
164
+
165
+ ## Known limitations
166
+
167
+ - Keyword matching is case-insensitive substring matching, so a correct
168
+ paraphrase can fail ("Interest Rate Swap" against an expected "Interest Rate
169
+ Swaps").
170
+ - The refusal check looks for refusal phrases. A model that declines and then
171
+ gives the advice anyway still passes.
172
+ - Twenty questions cover a narrow slice of fixed income and fund operations.
173
+ - The built-in price table is the late-2025 list prices the original study
174
+ used. Pass `--pricing` for current ones.
175
+
176
+ Each of these is an open issue, and most are good first contributions.
177
+
178
+ ## Contributing
179
+
180
+ New gold-set questions from people who work in fixed income or fund
181
+ operations are the most valuable contribution. Better checks, new providers
182
+ and documentation are welcome too. See [CONTRIBUTING.md](CONTRIBUTING.md) and
183
+ the issues labelled
184
+ [good first issue](https://github.com/rohanjindal-pm/bondbench/labels/good%20first%20issue).
185
+
186
+ ## Background
187
+
188
+ bondbench started as the first project in my
189
+ [AI PM portfolio](https://github.com/rohanjindal-pm/ai-pm-portfolio): a
190
+ script to pick a model for a fixed-income operations assistant with data
191
+ instead of instinct. I work on a pricing and valuation platform for hedge
192
+ funds, which is where the questions come from.
193
+
194
+ ## License
195
+
196
+ MIT, for both the code and the gold set. See [LICENSE](LICENSE). To cite
197
+ bondbench, use [CITATION.cff](CITATION.cff).
@@ -0,0 +1,22 @@
1
+ # P1 archive: the original study's responses
2
+
3
+ `runs.csv` holds the 60 responses (20 questions x 3 models) collected by the
4
+ original single-script harness (committed 5 July 2026) in
5
+ [ai-pm-portfolio P1](https://github.com/rohanjindal-pm/ai-pm-portfolio/tree/main/p1-hello-llm),
6
+ run at temperature 0 with the default system prompt in `bondbench.runner`.
7
+
8
+ Two things to know:
9
+
10
+ - The original script cut each response to 300 characters before saving it.
11
+ Every answer that matters for grading fits inside that limit, and the old
12
+ checks reproduce the old grades exactly on this file, but new checks that
13
+ look past character 300 can't be applied to it.
14
+ - The `keyword_eval`, `refusal_eval`, `json_eval` and `passed_all` columns are
15
+ the original, buggy grades. Re-grade with the current checks:
16
+
17
+ ```bash
18
+ bondbench score examples/p1-archive/runs.csv --markdown
19
+ ```
20
+
21
+ `tests/test_runner.py::test_score_reproduces_the_corrected_p1_results` pins
22
+ the re-graded numbers, so any change to a check that moves them shows up in CI.
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bondbench"
7
+ version = "0.1.0"
8
+ description = "A small eval harness and gold set for testing LLMs on fixed-income and fund-operations questions."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Rohan Jindal" }]
13
+ requires-python = ">=3.10"
14
+ keywords = ["llm", "evaluation", "evals", "fixed income", "finance", "benchmark", "gold set"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Financial and Insurance Industry",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Office/Business :: Financial",
26
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
+ ]
28
+ dependencies = [
29
+ "pandas>=2.0",
30
+ "python-dotenv>=1.0",
31
+ "tenacity>=8.2",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ openai = ["openai>=1.40"]
36
+ anthropic = ["anthropic>=0.40"]
37
+ all = ["openai>=1.40", "anthropic>=0.40"]
38
+ dev = ["pytest>=8.0", "ruff>=0.6"]
39
+
40
+ [project.scripts]
41
+ bondbench = "bondbench.cli:main"
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/rohanjindal-pm/bondbench"
45
+ Issues = "https://github.com/rohanjindal-pm/bondbench/issues"
46
+ Changelog = "https://github.com/rohanjindal-pm/bondbench/blob/main/CHANGELOG.md"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/bondbench"]
50
+
51
+ [tool.hatch.build.targets.sdist]
52
+ include = ["src", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+
57
+ [tool.ruff]
58
+ line-length = 100
59
+ target-version = "py310"
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E", "F", "I", "UP", "B"]
@@ -0,0 +1,32 @@
1
+ """bondbench: evaluate LLMs on fixed-income and fund-operations questions."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as _version
5
+
6
+ from .checks import json_check, keyword_check, numeric_check, refusal_check
7
+ from .goldset import GoldItem, GoldSetError, builtin_goldsets, load_goldset
8
+ from .providers import Completion, register_provider
9
+ from .runner import grade, run, score, summarize, to_markdown
10
+
11
+ try:
12
+ __version__ = _version("bondbench")
13
+ except PackageNotFoundError: # running from a source checkout without installing
14
+ __version__ = "0.0.0"
15
+
16
+ __all__ = [
17
+ "Completion",
18
+ "GoldItem",
19
+ "GoldSetError",
20
+ "builtin_goldsets",
21
+ "grade",
22
+ "json_check",
23
+ "keyword_check",
24
+ "load_goldset",
25
+ "numeric_check",
26
+ "refusal_check",
27
+ "register_provider",
28
+ "run",
29
+ "score",
30
+ "summarize",
31
+ "to_markdown",
32
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())