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.
- bondbench-0.1.0/.gitignore +11 -0
- bondbench-0.1.0/CHANGELOG.md +37 -0
- bondbench-0.1.0/LICENSE +21 -0
- bondbench-0.1.0/PKG-INFO +235 -0
- bondbench-0.1.0/README.md +197 -0
- bondbench-0.1.0/examples/p1-archive/README.md +22 -0
- bondbench-0.1.0/pyproject.toml +62 -0
- bondbench-0.1.0/src/bondbench/__init__.py +32 -0
- bondbench-0.1.0/src/bondbench/__main__.py +3 -0
- bondbench-0.1.0/src/bondbench/checks.py +118 -0
- bondbench-0.1.0/src/bondbench/cli.py +164 -0
- bondbench-0.1.0/src/bondbench/data/__init__.py +1 -0
- bondbench-0.1.0/src/bondbench/data/fixed_income_ops_v1.csv +21 -0
- bondbench-0.1.0/src/bondbench/goldset.py +167 -0
- bondbench-0.1.0/src/bondbench/pricing.py +46 -0
- bondbench-0.1.0/src/bondbench/providers.py +173 -0
- bondbench-0.1.0/src/bondbench/runner.py +227 -0
- bondbench-0.1.0/tests/test_checks.py +92 -0
- bondbench-0.1.0/tests/test_cli.py +46 -0
- bondbench-0.1.0/tests/test_goldset.py +84 -0
- bondbench-0.1.0/tests/test_runner.py +106 -0
|
@@ -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
|
bondbench-0.1.0/LICENSE
ADDED
|
@@ -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.
|
bondbench-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/rohanjindal-pm/bondbench/actions/workflows/ci.yml)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+

|
|
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
|
+
[](https://github.com/rohanjindal-pm/bondbench/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+

|
|
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
|
+
]
|