owlplanner 2025.12.20__py3-none-any.whl → 2026.2.2__py3-none-any.whl
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.
- owlplanner/__init__.py +20 -1
- owlplanner/abcapi.py +18 -17
- owlplanner/cli/README.md +50 -0
- owlplanner/cli/_main.py +52 -0
- owlplanner/cli/cli_logging.py +56 -0
- owlplanner/cli/cmd_list.py +83 -0
- owlplanner/cli/cmd_run.py +86 -0
- owlplanner/config.py +315 -118
- owlplanner/data/__init__.py +21 -0
- owlplanner/data/rates.csv +99 -98
- owlplanner/debts.py +36 -8
- owlplanner/fixedassets.py +95 -21
- owlplanner/mylogging.py +157 -25
- owlplanner/plan.py +938 -390
- owlplanner/plotting/__init__.py +16 -3
- owlplanner/plotting/base.py +17 -3
- owlplanner/plotting/factory.py +16 -3
- owlplanner/plotting/matplotlib_backend.py +30 -7
- owlplanner/plotting/plotly_backend.py +32 -9
- owlplanner/progress.py +16 -3
- owlplanner/rates.py +50 -34
- owlplanner/socialsecurity.py +28 -19
- owlplanner/tax2026.py +119 -38
- owlplanner/timelists.py +194 -18
- owlplanner/utils.py +179 -4
- owlplanner/version.py +20 -1
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/METADATA +11 -3
- owlplanner-2026.2.2.dist-info/RECORD +35 -0
- owlplanner-2026.2.2.dist-info/entry_points.txt +2 -0
- owlplanner-2026.2.2.dist-info/licenses/AUTHORS +15 -0
- owlplanner/tax2025.py +0 -359
- owlplanner-2025.12.20.dist-info/RECORD +0 -29
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/WHEEL +0 -0
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/licenses/LICENSE +0 -0
owlplanner/__init__.py
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Owl planner package initialization and public API exports.
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
"""
|
|
19
|
+
|
|
1
20
|
from owlplanner.plan import Plan # noqa: F401
|
|
2
21
|
from owlplanner.plan import clone # noqa: F401
|
|
3
|
-
from owlplanner.config import readConfig
|
|
22
|
+
from owlplanner.config import readConfig, saveConfig # noqa: F401
|
|
4
23
|
from owlplanner.rates import getRatesDistributions # noqa: F401
|
|
5
24
|
from owlplanner.version import __version__ # noqa: F401
|
|
6
25
|
|
owlplanner/abcapi.py
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
|
+
Abstract API for building linear programming constraint matrices.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
This module provides a solver-neutral API to build constraint matrices and
|
|
5
|
+
objective functions line by line, abstracting the building process to enable
|
|
6
|
+
use of various solvers (MOSEK, HiGHS, etc.) for comparison. The name ABCAPI
|
|
7
|
+
refers to A (matrix), B (bounds), C (constraints).
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
of
|
|
11
|
+
This program is free software: you can redistribute it and/or modify
|
|
12
|
+
it under the terms of the GNU General Public License as published by
|
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
14
|
+
(at your option) any later version.
|
|
10
15
|
|
|
11
|
-
This
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
This approach has been successful with the MOSEK and the HiGHS solvers.
|
|
17
|
-
A for matrix, B for bounds, C for constraints. Thus the name ABCAPI.
|
|
18
|
-
|
|
19
|
-
Copyright © 2024 - Martin-D. Lacasse
|
|
20
|
-
|
|
21
|
-
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
16
|
+
This program is distributed in the hope that it will be useful,
|
|
17
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
+
GNU General Public License for more details.
|
|
22
20
|
|
|
21
|
+
You should have received a copy of the GNU General Public License
|
|
22
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
@@ -176,7 +176,7 @@ class Bounds:
|
|
|
176
176
|
self.ind.append(ii)
|
|
177
177
|
self.lb.append(lb)
|
|
178
178
|
self.ub.append(ub)
|
|
179
|
-
if lb
|
|
179
|
+
if np.isclose(lb, ub):
|
|
180
180
|
self.key.append("fx")
|
|
181
181
|
elif ub == np.inf and lb == -np.inf:
|
|
182
182
|
self.key.append("fr")
|
|
@@ -204,6 +204,7 @@ class Bounds:
|
|
|
204
204
|
return lb, ub
|
|
205
205
|
|
|
206
206
|
def integralityArray(self):
|
|
207
|
+
# All variables continuous by default.
|
|
207
208
|
integrality = np.zeros(self.nvars, dtype=int)
|
|
208
209
|
for ii in range(len(self.integrality)):
|
|
209
210
|
integrality[self.integrality[ii]] = 1
|
owlplanner/cli/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# OWLCLI - a Command Line Interface for OWL
|
|
2
|
+
|
|
3
|
+
OWLCLI is a command line interface tool to streamline the listing, running and experimenting with OWL plan files outside the streamlit interface.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
OWLCLI is installed with the owlplanner module. Once OWLPLANNER is installed, owlcli can be run from the command line.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
At present, OWLCLI provides two commands: `list` and `run`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
❯ owlcli list
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command will list all the available OWL .toml plan files in the current directory.
|
|
18
|
+
|
|
19
|
+
To list all the available OWL plan files in the `examples` directory, relative to the current directory, use:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
❯ owlcli list examples
|
|
23
|
+
FILE PLAN NAME TIME LISTS FILE
|
|
24
|
+
--------------------------------------------------------------------------------
|
|
25
|
+
Case_jack+jill jack+jill ✓HFP_jack+jill.xlsx
|
|
26
|
+
Case_joe joe ✓HFP_joe.xlsx
|
|
27
|
+
Case_john+sally john+sally ✓HFP_john+sally.xlsx
|
|
28
|
+
Case_jon+jane Jon+Jane ✗HFP_jon+jane.xslx
|
|
29
|
+
Case_kim+sam-bequest kim+sam-bequest ✓HFP_kim+sam.xlsx
|
|
30
|
+
Case_kim+sam-spending kim+sam-spending ✓HFP_kim+sam.xlsx
|
|
31
|
+
case_drawdowncalc-comparison-1 drawdowncalc-com... ✗edited values
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The listing shows the file name, plan name and Household Financial Plan file (timeListsFile) associated with each plan.
|
|
35
|
+
|
|
36
|
+
✓ indicates that the Household Financial Plan file listed in the OWL Plan file exists.
|
|
37
|
+
✗ indicates that the Household Financial Plan file was not found.
|
|
38
|
+
*edited values* indicates that the plan file may have been changed since the Household Financial Plan file was created.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
To run an OWL plan file, use the `run` command followed by the plan file name:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
❯ owlcli run examples/Case_kim+sam-spending
|
|
45
|
+
Case status: solved
|
|
46
|
+
Results saved to: examples/Case_kim+sam-spending_results.xlsx
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This example runs the `Case_kim+sam-spending` plan file located in the `examples` directory. The results of the run are saved to a new Excel file with `_results.xlsx` appended to the original plan file name. A copy of the input OWL plan file is saved as the new first tab in the Excel file.
|
|
50
|
+
|
owlplanner/cli/_main.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface main entry point for Owl retirement planner.
|
|
3
|
+
|
|
4
|
+
This module provides the main CLI group and command registration for the
|
|
5
|
+
Owl command-line interface.
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
8
|
+
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
from .cli_logging import configure_logging, LOG_LEVELS
|
|
27
|
+
from .cmd_list import cmd_list
|
|
28
|
+
from .cmd_run import cmd_run
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
@click.option(
|
|
33
|
+
"--log-level",
|
|
34
|
+
type=click.Choice(LOG_LEVELS, case_sensitive=False),
|
|
35
|
+
default="INFO",
|
|
36
|
+
show_default=True,
|
|
37
|
+
help="Set logging verbosity.",
|
|
38
|
+
)
|
|
39
|
+
@click.pass_context
|
|
40
|
+
def cli(ctx, log_level: str):
|
|
41
|
+
"""SSG command-line interface."""
|
|
42
|
+
ctx.ensure_object(dict)
|
|
43
|
+
ctx.obj["log_level"] = log_level.upper()
|
|
44
|
+
|
|
45
|
+
configure_logging(log_level)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
cli.add_command(cmd_list)
|
|
49
|
+
cli.add_command(cmd_run)
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
cli()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI logging configuration utilities.
|
|
3
|
+
|
|
4
|
+
This module provides logging configuration functions for the command-line
|
|
5
|
+
interface, including log level management and formatting.
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
8
|
+
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
from loguru import logger
|
|
25
|
+
|
|
26
|
+
LOG_LEVELS = {
|
|
27
|
+
"TRACE",
|
|
28
|
+
"DEBUG",
|
|
29
|
+
"INFO",
|
|
30
|
+
"SUCCESS",
|
|
31
|
+
"WARNING",
|
|
32
|
+
"ERROR",
|
|
33
|
+
"CRITICAL",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def configure_logging(log_level: str = "INFO"):
|
|
38
|
+
log_level = log_level.upper()
|
|
39
|
+
|
|
40
|
+
if log_level not in LOG_LEVELS:
|
|
41
|
+
raise ValueError(f"Invalid log level: {log_level}")
|
|
42
|
+
|
|
43
|
+
logger.remove() # remove default handler
|
|
44
|
+
|
|
45
|
+
logger.add(
|
|
46
|
+
sys.stderr,
|
|
47
|
+
level=log_level,
|
|
48
|
+
format=(
|
|
49
|
+
# "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
50
|
+
"<level>{level:8}</level> | "
|
|
51
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
52
|
+
"<level>{message}</level>"
|
|
53
|
+
),
|
|
54
|
+
backtrace=(log_level == "TRACE"),
|
|
55
|
+
diagnose=(log_level == "TRACE"),
|
|
56
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI command for listing retirement planning case files.
|
|
3
|
+
|
|
4
|
+
This module provides the 'list' command for discovering and displaying
|
|
5
|
+
information about retirement planning case files in a directory.
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
8
|
+
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from loguru import logger
|
|
26
|
+
|
|
27
|
+
import owlplanner as owl
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.command(name="list")
|
|
31
|
+
@click.argument(
|
|
32
|
+
"directory",
|
|
33
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
34
|
+
default=".",
|
|
35
|
+
)
|
|
36
|
+
def cmd_list(directory):
|
|
37
|
+
"""
|
|
38
|
+
List OWL plan files in a directory.
|
|
39
|
+
|
|
40
|
+
DIRECTORY defaults to the current directory.
|
|
41
|
+
"""
|
|
42
|
+
logger.debug(f"Listing plans in directory: {directory}")
|
|
43
|
+
|
|
44
|
+
toml_files = sorted(directory.glob("*.toml"))
|
|
45
|
+
|
|
46
|
+
if not toml_files:
|
|
47
|
+
click.echo("No .toml files found.")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
plans = []
|
|
51
|
+
|
|
52
|
+
for filename in toml_files:
|
|
53
|
+
try:
|
|
54
|
+
logger.debug(f"Loading plan from {filename}")
|
|
55
|
+
plan = owl.readConfig(str(filename), logstreams="loguru", readContributions=False)
|
|
56
|
+
plans.append((filename.stem, plan))
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.warning(f"Failed to load {filename}: {e}")
|
|
59
|
+
|
|
60
|
+
if not plans:
|
|
61
|
+
click.echo("No valid OWL plans found.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
click.echo(f"{'FILE':<30} {'PLAN NAME':<20} {' TIME LISTS FILE':<30}")
|
|
65
|
+
click.echo("-" * 80)
|
|
66
|
+
|
|
67
|
+
CHECK = "✓"
|
|
68
|
+
CROSS = "✗"
|
|
69
|
+
|
|
70
|
+
plan_dir = directory
|
|
71
|
+
|
|
72
|
+
for stem, plan in plans:
|
|
73
|
+
# Truncate plan name if needed
|
|
74
|
+
plan_name = plan._name
|
|
75
|
+
if len(plan_name) > 20:
|
|
76
|
+
plan_name = plan_name[:16] + "..."
|
|
77
|
+
|
|
78
|
+
# Check if timeListsFileName exists in current directory
|
|
79
|
+
tl_name = plan.timeListsFileName
|
|
80
|
+
exists = (plan_dir / tl_name).exists() if tl_name else False
|
|
81
|
+
mark = CHECK if exists else CROSS
|
|
82
|
+
|
|
83
|
+
click.echo(f"{stem:<30} {plan_name:<20} {mark}{tl_name:<30}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI command for running retirement planning cases.
|
|
3
|
+
|
|
4
|
+
This module provides the 'run' command for executing retirement planning
|
|
5
|
+
optimization from the command line.
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
8
|
+
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
from loguru import logger
|
|
25
|
+
import owlplanner as owl
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_toml(ctx, param, value: Path):
|
|
30
|
+
if value is None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
# If no suffix, append .toml
|
|
34
|
+
if value.suffix == "":
|
|
35
|
+
value = value.with_suffix(".toml")
|
|
36
|
+
|
|
37
|
+
# Enforce .toml extension
|
|
38
|
+
if value.suffix.lower() != ".toml":
|
|
39
|
+
raise click.BadParameter("File must have a .toml extension")
|
|
40
|
+
|
|
41
|
+
# Check existence AFTER normalization
|
|
42
|
+
if not value.exists():
|
|
43
|
+
raise click.BadParameter(f"File '{value}' does not exist")
|
|
44
|
+
|
|
45
|
+
if not value.is_file():
|
|
46
|
+
raise click.BadParameter(f"'{value}' is not a file")
|
|
47
|
+
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.command(name="run")
|
|
52
|
+
@click.argument(
|
|
53
|
+
"filename",
|
|
54
|
+
type=click.Path(exists=False, dir_okay=False, path_type=Path),
|
|
55
|
+
callback=validate_toml,
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--with-config",
|
|
59
|
+
"with_config",
|
|
60
|
+
type=click.Choice(["no", "first", "last"], case_sensitive=False),
|
|
61
|
+
default="first",
|
|
62
|
+
show_default=True,
|
|
63
|
+
help="Include config TOML sheet at the first or last position.",
|
|
64
|
+
)
|
|
65
|
+
def cmd_run(filename: Path, with_config: str):
|
|
66
|
+
"""Run the solver for an input OWL plan file.
|
|
67
|
+
|
|
68
|
+
FILENAME is the OWL plan file to run. If no extension is provided,
|
|
69
|
+
.toml will be appended. The file must exist.
|
|
70
|
+
|
|
71
|
+
An output Excel file with results will be created in the current directory.
|
|
72
|
+
The output filename is derived from the input filename by appending
|
|
73
|
+
'_results.xlsx' to the stem of the input filename.
|
|
74
|
+
|
|
75
|
+
Optionally include the case configuration as a TOML worksheet.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
logger.debug(f"Executing the run command with file: {filename}")
|
|
79
|
+
|
|
80
|
+
plan = owl.readConfig(str(filename), logstreams="loguru", readContributions=True)
|
|
81
|
+
plan.solve(plan.objective, plan.solverOptions)
|
|
82
|
+
click.echo(f"Case status: {plan.caseStatus}")
|
|
83
|
+
if plan.caseStatus == "solved":
|
|
84
|
+
output_filename = filename.with_name(filename.stem + "_results.xlsx")
|
|
85
|
+
plan.saveWorkbook(basename=output_filename, overwrite=True, with_config=with_config)
|
|
86
|
+
click.echo(f"Results saved to: {output_filename}")
|