finrb 1.1.0 → 1.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +89 -0
- data/README.md +83 -10
- data/SECURITY.md +41 -0
- data/lib/finrb/accounting.rb +93 -74
- data/lib/finrb/amortization.rb +167 -35
- data/lib/finrb/calendars/base.rb +161 -0
- data/lib/finrb/calendars/hebrew_calendar.rb +77 -0
- data/lib/finrb/calendars/israel_tase.rb +96 -0
- data/lib/finrb/calendars/us_federal_reserve.rb +82 -0
- data/lib/finrb/calendars.rb +6 -0
- data/lib/finrb/cashflows.rb +4 -7
- data/lib/finrb/config.rb +5 -5
- data/lib/finrb/day_count.rb +62 -0
- data/lib/finrb/fixed_rate_bond.rb +140 -0
- data/lib/finrb/rates.rb +3 -7
- data/lib/finrb/ratios.rb +41 -40
- data/lib/finrb/returns.rb +49 -52
- data/lib/finrb/schedule.rb +126 -0
- data/lib/finrb/tvm.rb +154 -52
- data/lib/finrb/validation.rb +42 -0
- data/lib/finrb/version.rb +1 -1
- data/lib/finrb/yields.rb +42 -31
- data/lib/finrb.rb +4 -0
- data/sig/finrb.rbs +114 -5
- metadata +26 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a5f20ce5e17a03b0043df557ea5ea2207eb15e286e8f80664ef436dadc6cbce
|
|
4
|
+
data.tar.gz: de4773bdabd2131fcfcf4be29cc9befecf47fd92353e308d7cdae5569d7c914e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0afa3b1d816b589d23ea2a6cd16c3a274d4ce68bb2a2552caa7b5c398d214b66c3f287fc824c7b607c6288df40b2b7fe7541c84f190a8e3f282cd8ed47238a7
|
|
7
|
+
data.tar.gz: 22c1d7ace18c693faa7f7d28551a064a1e9390e61d9bb023724dc98afbc24c13ae8d80044496260e1c846a37ba7d0a3b70da58e318d04a56aae979f5ead7892b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# finrb changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Dated lending and calendars
|
|
6
|
+
|
|
7
|
+
- Add opt-in dated amortization with monthly, quarterly, semiannual, and annual
|
|
8
|
+
payment frequencies, explicit short-final stubs, and Actual/365 Fixed or
|
|
9
|
+
Actual/360 interest accrual. Rate durations remain expressed in months.
|
|
10
|
+
- Add `Amortization#cashflow_yield` to report the dated borrower's effective
|
|
11
|
+
annual cashflow-equivalent cost from net proceeds and scheduled payments.
|
|
12
|
+
This is not a jurisdiction-specific legal APR disclosure.
|
|
13
|
+
- Add dependency-free US Federal Reserve payment-day and Israel TASE trading
|
|
14
|
+
calendars with business-day adjustment conventions. Supported ranges are
|
|
15
|
+
explicit: US 1950–2065 and TASE 2000–2050.
|
|
16
|
+
- Extract immutable `Finrb::Schedule` payment periods for reuse by dated loans
|
|
17
|
+
and fixed-rate bonds.
|
|
18
|
+
|
|
19
|
+
### Fixed income
|
|
20
|
+
|
|
21
|
+
- Add `Finrb::FixedRateBond` for regular fixed-coupon bullet bonds, with
|
|
22
|
+
Actual/Actual ICMA coupon accrual, accrued interest, clean and dirty prices,
|
|
23
|
+
and yield-to-maturity.
|
|
24
|
+
- Correct accrued-interest selection when settlement falls between an
|
|
25
|
+
unadjusted coupon boundary and its business-day-adjusted payment date.
|
|
26
|
+
|
|
27
|
+
### Verification and development
|
|
28
|
+
|
|
29
|
+
- Add pinned QuantLib reference fixtures and seeded randomized cross-validation
|
|
30
|
+
for dated amortization, calendar profiles, and fixed-rate bond valuations.
|
|
31
|
+
- Add calendar and randomized bond oracle checks to CI, and organize scripts by
|
|
32
|
+
verification, fixture generation, packaging, documentation, and benchmarks.
|
|
33
|
+
|
|
34
|
+
## 1.2.0
|
|
35
|
+
|
|
36
|
+
### Validation and financial correctness
|
|
37
|
+
|
|
38
|
+
- Apply the shared finite-decimal validation contract across yields, returns,
|
|
39
|
+
accounting, ratios, cashflows, rates, amortization, and TVM helpers.
|
|
40
|
+
- Replace abbreviated validation errors with descriptive financial terms such
|
|
41
|
+
as period count, future value, face value, and compounding periods.
|
|
42
|
+
- Add explicit denominator, rate, share-count, inventory, useful-life, and
|
|
43
|
+
residual-value domain checks while preserving valid decimal calculations and
|
|
44
|
+
return types.
|
|
45
|
+
- Fix LIFO ending-inventory retention when a sale is satisfied by the second
|
|
46
|
+
purchase layer, preserving inventory cost conservation.
|
|
47
|
+
- Reject invalid weighted-return vectors and replace the previous unsolicited
|
|
48
|
+
weighted-portfolio warning with an explicit validation error.
|
|
49
|
+
|
|
50
|
+
### Development and project maintenance
|
|
51
|
+
|
|
52
|
+
- Add deterministic `benchmark:run` scenarios using `benchmark-ips` for IRR,
|
|
53
|
+
XIRR, and amortization scaling, with known-root, equation-residual, solver
|
|
54
|
+
evaluation, and schedule-reconciliation preflight diagnostics.
|
|
55
|
+
- Add contributor and security guidance, and link the project entry documents
|
|
56
|
+
from the README.
|
|
57
|
+
- Include contributor and security documentation in packaged gem metadata.
|
|
58
|
+
|
|
3
59
|
## 1.1.0
|
|
4
60
|
|
|
5
61
|
### Investment returns and risk
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Contributing to finrb
|
|
2
|
+
|
|
3
|
+
finrb welcomes focused bug fixes, documentation improvements, tests, and
|
|
4
|
+
financial calculations that fit the project's scope. Before starting a large
|
|
5
|
+
feature or public API change, open an issue so its financial conventions and
|
|
6
|
+
design can be agreed upon first.
|
|
7
|
+
|
|
8
|
+
## Development setup
|
|
9
|
+
|
|
10
|
+
finrb requires Ruby 3.3 or newer. MRI 3.3, 3.4, and 4.0 are supported; JRuby
|
|
11
|
+
and TruffleRuby are tested as informational compatibility targets.
|
|
12
|
+
|
|
13
|
+
```shell
|
|
14
|
+
bundle install
|
|
15
|
+
bundle exec rake quality
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`rake quality` runs RuboCop and the RSpec suite with coverage, verifies
|
|
19
|
+
maintained API examples, and validates the packaged RBS declarations. Run
|
|
20
|
+
`bundle exec rake lint` alone for a focused full-repository lint check.
|
|
21
|
+
Additional checks are available for changes that affect their domains:
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
bundle exec rake security:audit
|
|
25
|
+
bundle exec rake package:verify
|
|
26
|
+
bundle exec rake benchmark:run
|
|
27
|
+
bundle exec rake solver:verify
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The solver verification task requires the optional Python environment
|
|
31
|
+
documented in [spec/fixtures/README.md](spec/fixtures/README.md). The Python
|
|
32
|
+
packages are development references and are not gem dependencies.
|
|
33
|
+
|
|
34
|
+
## Making changes
|
|
35
|
+
|
|
36
|
+
- Preserve unrelated code and behavior. Keep pull requests small enough to
|
|
37
|
+
review their financial and numerical consequences directly.
|
|
38
|
+
- Follow the local Ruby style and run RuboCop. Comments should explain a
|
|
39
|
+
non-obvious reason or convention, not restate the code.
|
|
40
|
+
- Route public numeric inputs through `Finrb::Validation`. Use `ArgumentError`
|
|
41
|
+
for malformed inputs and namespaced finrb errors for financial or numerical
|
|
42
|
+
domains.
|
|
43
|
+
- Preserve `Flt::DecNum` calculations and return types unless the change has a
|
|
44
|
+
documented compatibility reason.
|
|
45
|
+
- Update RBS declarations and user documentation when a public contract
|
|
46
|
+
changes.
|
|
47
|
+
- Do not add an unreleased changelog section for ordinary development work.
|
|
48
|
+
The maintainer prepares the changelog as part of an actual release.
|
|
49
|
+
|
|
50
|
+
## Numerical and financial changes
|
|
51
|
+
|
|
52
|
+
A formula compiling or producing a plausible number is not sufficient
|
|
53
|
+
verification. Describe the convention being implemented and test the relevant
|
|
54
|
+
invariants, boundaries, and failure modes.
|
|
55
|
+
|
|
56
|
+
For a solver or precision-sensitive change, include as appropriate:
|
|
57
|
+
|
|
58
|
+
- known-root or algebraically constructed cases;
|
|
59
|
+
- normalized equation residuals;
|
|
60
|
+
- zero, negative-rate, long-horizon, and scale-sensitive cases;
|
|
61
|
+
- attributable fixtures from a reputable independent implementation under
|
|
62
|
+
matching financial conventions;
|
|
63
|
+
- deterministic seeds for generated cases; and
|
|
64
|
+
- benchmarks that compare the same scenario across revisions, runtimes, or
|
|
65
|
+
architectures rather than ranking unrelated calculations.
|
|
66
|
+
|
|
67
|
+
Reference fixtures must record their source, version, conventions, generation
|
|
68
|
+
method, and tolerance. Do not replace committed fixtures merely to make a
|
|
69
|
+
changed implementation pass without explaining why the reference changed.
|
|
70
|
+
|
|
71
|
+
## Pull requests
|
|
72
|
+
|
|
73
|
+
Use the pull request template and include:
|
|
74
|
+
|
|
75
|
+
- the financial or technical problem;
|
|
76
|
+
- the intended contract and compatibility impact;
|
|
77
|
+
- the evidence used to verify correctness; and
|
|
78
|
+
- any checks skipped because an optional runtime or external reference was
|
|
79
|
+
unavailable.
|
|
80
|
+
|
|
81
|
+
Commit subjects in this repository commonly use the form
|
|
82
|
+
`domain [Category]: Imperative summary`, for example:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
cashflows [Fix]: Preserve negative-root selection
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
By contributing, you agree that your work is provided under the repository's
|
|
89
|
+
LGPL-3.0-or-later license.
|
data/README.md
CHANGED
|
@@ -58,14 +58,17 @@ API explicitly returns another financial object, such as `Finrb::Rate`.
|
|
|
58
58
|
| `Finrb::Cashflow` | NPV, XNPV, IRR, and XIRR |
|
|
59
59
|
| `Finrb::Rate` | Nominal APR, effective APY, and compounding conversions |
|
|
60
60
|
| `Finrb::Amortization` | Fixed and adjustable-rate loan amortization |
|
|
61
|
+
| `Finrb::Schedule` | Immutable dated payment periods with optional calendar adjustment |
|
|
62
|
+
| `Finrb::FixedRateBond` | Fixed-coupon bullet bond cashflows, accrued interest, price, and yield |
|
|
63
|
+
| `Finrb::Calendars` | US Federal Reserve and Israel TASE business calendars |
|
|
61
64
|
| `Finrb::TVM` | Present value, future value, payments, periods, and perpetuities |
|
|
62
65
|
| `Finrb::Returns` | Holding-period, time-weighted, portfolio, and risk-adjusted returns |
|
|
63
66
|
| `Finrb::Yields` | Money-market, bond-equivalent, effective, and continuous yield conversions |
|
|
64
67
|
| `Finrb::Ratios` | Liquidity, leverage, profitability, and per-share ratios |
|
|
65
68
|
| `Finrb::Accounting` | Inventory costing and depreciation |
|
|
66
69
|
|
|
67
|
-
The
|
|
68
|
-
its
|
|
70
|
+
The [API and examples guide](docs/api.md) documents each financial domain and
|
|
71
|
+
its public calculations. Packaged RBS declarations are available under `sig/`.
|
|
69
72
|
|
|
70
73
|
## Cashflows
|
|
71
74
|
|
|
@@ -131,6 +134,44 @@ first.interest_only?
|
|
|
131
134
|
first.closing_balance
|
|
132
135
|
```
|
|
133
136
|
|
|
137
|
+
Pass a Ruby `Date` as `start_date:` to opt into an actual/365 dated schedule.
|
|
138
|
+
The start date is the accrual boundary; the first payment date is one month
|
|
139
|
+
later. Month-end anchors stay at month end, while other day numbers are clamped
|
|
140
|
+
to shorter months and recovered from the original anchor in the following
|
|
141
|
+
month. Dated periods accrue simple nominal APR for their actual number of days
|
|
142
|
+
(`APR * days / 365`); this is a specific convention, not a universal loan
|
|
143
|
+
standard. Dates stay unadjusted by default. To opt in, supply a finrb market
|
|
144
|
+
calendar and explicit business-day convention; the adjusted payment dates then
|
|
145
|
+
drive the actual/365 accrual. finrb includes US Federal Reserve and Israel
|
|
146
|
+
TASE full-day calendars, with no runtime holiday-data dependency.
|
|
147
|
+
The profiles intentionally support US dates from 1950 through 2065 and TASE
|
|
148
|
+
dates from 2000 through 2050; querying or configuring dates outside those
|
|
149
|
+
windows raises `RangeError` rather than extrapolating silently.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
require 'date'
|
|
153
|
+
|
|
154
|
+
dated = Finrb::Amortization.new(
|
|
155
|
+
100_000,
|
|
156
|
+
Finrb::Rate.new(0.05, :apr, duration: 3),
|
|
157
|
+
start_date: Date.new(2024, 1, 31)
|
|
158
|
+
)
|
|
159
|
+
dated.schedule.first.date # => #<Date: 2024-02-29 ...>
|
|
160
|
+
|
|
161
|
+
bank_calendar = Finrb::Calendars::USFederalReserve.new
|
|
162
|
+
calendar_adjusted = Finrb::Amortization.new(
|
|
163
|
+
100_000,
|
|
164
|
+
Finrb::Rate.new(0.05, :apr, duration: 3),
|
|
165
|
+
start_date: Date.new(2026, 1, 31),
|
|
166
|
+
calendar: bank_calendar,
|
|
167
|
+
business_day_convention: :modified_following
|
|
168
|
+
)
|
|
169
|
+
calendar_adjusted.schedule.first.date # => #<Date: 2026-02-27 ...>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
See the [calendar API guide](docs/api.md#business-calendars) for market scope,
|
|
173
|
+
holiday overrides, and supported date-adjustment conventions.
|
|
174
|
+
|
|
134
175
|
Pass several duration-bearing rates for an adjustable-rate schedule. A block
|
|
135
176
|
can modify each scheduled payment, for example to add a $150 principal payment:
|
|
136
177
|
|
|
@@ -185,6 +226,25 @@ financed_fee.net_proceeds # => Flt::DecNum('250000')
|
|
|
185
226
|
financed_fee.amount_financed # => Flt::DecNum('252500')
|
|
186
227
|
```
|
|
187
228
|
|
|
229
|
+
For a dated schedule, `cashflow_yield` calculates the effective annual
|
|
230
|
+
cashflow-equivalent cost to the borrower using net proceeds on the start date
|
|
231
|
+
and each actual payment on its scheduled date. The final payment already
|
|
232
|
+
includes any balloon settlement, so it is counted only once. The result is a
|
|
233
|
+
`Finrb::Rate`; it is not a jurisdiction-specific legal APR and follows the
|
|
234
|
+
current `Finrb::Cashflow.xirr` configuration.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
require 'date'
|
|
238
|
+
|
|
239
|
+
dated_loan = Finrb::Amortization.new(
|
|
240
|
+
250_000,
|
|
241
|
+
rate,
|
|
242
|
+
start_date: Date.new(2025, 1, 15),
|
|
243
|
+
origination_fee: 2_500
|
|
244
|
+
)
|
|
245
|
+
dated_loan.cashflow_yield.apy # effective annual cost implied by proceeds and payments
|
|
246
|
+
```
|
|
247
|
+
|
|
188
248
|
## Configuration
|
|
189
249
|
|
|
190
250
|
Configure process-wide defaults during application startup:
|
|
@@ -261,14 +321,14 @@ Install the bundle and run the self-contained quality checks:
|
|
|
261
321
|
```shell
|
|
262
322
|
bundle install
|
|
263
323
|
bundle exec rake quality
|
|
264
|
-
bundle exec rubocop
|
|
265
324
|
bundle exec rake security:audit
|
|
266
325
|
bundle exec rake package:verify
|
|
267
326
|
```
|
|
268
327
|
|
|
269
|
-
The quality task runs the RSpec suite with line and branch coverage,
|
|
270
|
-
IRR/XIRR properties, committed SciPy/QuantLib reference fixtures,
|
|
271
|
-
validation.
|
|
328
|
+
The quality task runs RuboCop, the RSpec suite with line and branch coverage,
|
|
329
|
+
generated IRR/XIRR properties, committed SciPy/QuantLib reference fixtures,
|
|
330
|
+
verified API examples, and RBS validation. Run `bundle exec rake lint` alone for
|
|
331
|
+
a focused full-repository lint check.
|
|
272
332
|
|
|
273
333
|
`security:audit` updates ruby-advisory-db and checks the locked dependencies.
|
|
274
334
|
`package:verify` builds the gem, validates its contents and metadata, installs
|
|
@@ -321,23 +381,36 @@ only finrb's runtime dependencies and RSpec; MRI-only development tooling such
|
|
|
321
381
|
as RBS, RuboCop, and coverage is deliberately excluded from engine
|
|
322
382
|
compatibility runs.
|
|
323
383
|
|
|
324
|
-
Maintainers with the optional Python environment can run
|
|
325
|
-
|
|
384
|
+
Maintainers with the optional Python environment can run seeded randomized
|
|
385
|
+
cross-validation campaigns for solvers and fixed-rate bonds:
|
|
326
386
|
|
|
327
387
|
```shell
|
|
328
|
-
python3 -m pip install --requirement script/requirements-solver
|
|
388
|
+
python3 -m pip install --requirement script/verification/requirements-solver.txt
|
|
329
389
|
bundle exec rake solver:verify
|
|
330
390
|
```
|
|
331
391
|
|
|
392
|
+
The bond campaign needs only QuantLib-Python 1.43:
|
|
393
|
+
|
|
394
|
+
```shell
|
|
395
|
+
python3 -m pip install --requirement script/verification/requirements-bonds.txt
|
|
396
|
+
bundle exec rake bond:verify
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The same bond campaign can run in the isolated Docker target with
|
|
400
|
+
`bundle exec rake docker:verify_bond`.
|
|
401
|
+
|
|
332
402
|
The Python packages are verification references, not gem dependencies. See
|
|
333
403
|
[the fixture documentation](spec/fixtures/README.md) for reproducibility,
|
|
334
|
-
Docker, batching, and tolerance details.
|
|
404
|
+
Docker, batching, and tolerance details. Verification and maintainer utilities
|
|
405
|
+
are grouped by purpose under `script/`.
|
|
335
406
|
|
|
336
407
|
## Project links
|
|
337
408
|
|
|
338
409
|
- [RubyGems](https://rubygems.org/gems/finrb)
|
|
339
410
|
- [Source](https://github.com/ncs1/finrb)
|
|
340
411
|
- [Issue tracker](https://github.com/ncs1/finrb/issues)
|
|
412
|
+
- [Contributing](CONTRIBUTING.md)
|
|
413
|
+
- [Security policy](SECURITY.md)
|
|
341
414
|
|
|
342
415
|
## Acknowledgements
|
|
343
416
|
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are made against the current released finrb line. Older
|
|
6
|
+
versions may receive a fix when practical, but they are not guaranteed to be
|
|
7
|
+
maintained. Users should reproduce an issue on the newest release before
|
|
8
|
+
reporting it when possible.
|
|
9
|
+
|
|
10
|
+
JRuby and TruffleRuby are informational compatibility targets. A
|
|
11
|
+
runtime-specific vulnerability may also need to be reported to the relevant
|
|
12
|
+
Ruby implementation or dependency maintainers.
|
|
13
|
+
|
|
14
|
+
## Reporting a vulnerability
|
|
15
|
+
|
|
16
|
+
Do not disclose a suspected vulnerability in a public issue, discussion, or
|
|
17
|
+
pull request. Use GitHub's private vulnerability reporting for finrb:
|
|
18
|
+
|
|
19
|
+
<https://github.com/ncs1/finrb/security/advisories/new>
|
|
20
|
+
|
|
21
|
+
If private reporting is unavailable, contact the maintainer through the email
|
|
22
|
+
listed in the gem metadata. Include only enough information in the initial
|
|
23
|
+
message to establish a private channel.
|
|
24
|
+
|
|
25
|
+
A useful report contains:
|
|
26
|
+
|
|
27
|
+
- affected finrb and Ruby versions;
|
|
28
|
+
- the affected API or packaged artifact;
|
|
29
|
+
- minimal reproduction steps;
|
|
30
|
+
- the expected and observed impact; and
|
|
31
|
+
- any known mitigations or disclosure constraints.
|
|
32
|
+
|
|
33
|
+
Please avoid including production credentials, private financial data, access
|
|
34
|
+
tokens, or other secrets. There is no guaranteed response or remediation
|
|
35
|
+
timeline; this project is maintained by one person. Reports will be evaluated
|
|
36
|
+
according to reproducibility, impact, and available maintainer capacity.
|
|
37
|
+
|
|
38
|
+
Numerical disagreement alone is normally a correctness bug rather than a
|
|
39
|
+
security vulnerability. Treat it as security-sensitive when it can cross a
|
|
40
|
+
trust boundary, bypass validation, enable denial of service, corrupt packaged
|
|
41
|
+
artifacts, or predictably cause unsafe downstream financial decisions.
|
data/lib/finrb/accounting.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'decimal'
|
|
4
4
|
require_relative 'errors'
|
|
5
|
+
require_relative 'validation'
|
|
5
6
|
|
|
6
7
|
module Finrb
|
|
7
8
|
# Inventory costing and depreciation calculations.
|
|
@@ -34,74 +35,32 @@ module Finrb
|
|
|
34
35
|
# @example
|
|
35
36
|
# Finrb::Accounting.cogs(uinv=2,pinv=2,units=[3,5],price=[3,5],sinv=7,method="WAC")
|
|
36
37
|
def self.cogs(uinv:, pinv:, units:, price:, sinv:, method: 'FIFO')
|
|
37
|
-
uinv =
|
|
38
|
-
pinv = Flt::DecNum(pinv.to_s)
|
|
39
|
-
units = wrap_array(units).map { |value| Flt::DecNum(value.to_s) }
|
|
40
|
-
price = wrap_array(price).map { |value| Flt::DecNum(value.to_s) }
|
|
41
|
-
sinv = Flt::DecNum(sinv.to_s)
|
|
42
|
-
method = method.to_s
|
|
38
|
+
uinv, pinv, units, price, sinv, method = inventory_inputs(uinv:, pinv:, units:, price:, sinv:, method:)
|
|
43
39
|
|
|
44
40
|
n = units.size
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
cost_of_goods = sinv * pinv
|
|
53
|
-
ending_inventory = (uinv - sinv) * pinv
|
|
54
|
-
(0...n).each do |i|
|
|
55
|
-
ending_inventory += (units[i] * price[i])
|
|
56
|
-
end
|
|
57
|
-
else
|
|
58
|
-
cost_of_goods = uinv * pinv
|
|
59
|
-
sinv -= uinv
|
|
60
|
-
(0...n).each do |i|
|
|
61
|
-
if sinv <= units[i]
|
|
62
|
-
cost_of_goods += (sinv * price[i])
|
|
63
|
-
ending_inventory = (units[i] - sinv) * price[i]
|
|
64
|
-
if i < n
|
|
65
|
-
temp = i + 1
|
|
66
|
-
(temp...n).each do |j|
|
|
67
|
-
ending_inventory += (units[j] * price[j])
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
sinv = 0
|
|
71
|
-
break
|
|
72
|
-
else
|
|
73
|
-
cost_of_goods += (units[i] * price[i])
|
|
74
|
-
sinv -= units[i]
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
raise(Error, "Inventory is not enough to sell\n") if sinv.positive?
|
|
78
|
-
end
|
|
79
|
-
when 'WAC'
|
|
80
|
-
ending_inventory = uinv * pinv
|
|
81
|
-
tu = uinv
|
|
41
|
+
cost_of_goods = Flt::DecNum(0)
|
|
42
|
+
ending_inventory = Flt::DecNum(0)
|
|
43
|
+
case method
|
|
44
|
+
when 'FIFO'
|
|
45
|
+
if sinv <= uinv
|
|
46
|
+
cost_of_goods = sinv * pinv
|
|
47
|
+
ending_inventory = (uinv - sinv) * pinv
|
|
82
48
|
(0...n).each do |i|
|
|
83
49
|
ending_inventory += (units[i] * price[i])
|
|
84
|
-
tu += units[i]
|
|
85
|
-
end
|
|
86
|
-
if tu >= sinv
|
|
87
|
-
cost_of_goods = ending_inventory / tu * sinv
|
|
88
|
-
ending_inventory = ending_inventory / tu * (tu - sinv)
|
|
89
|
-
else
|
|
90
|
-
raise(Error, "Inventory is not enough to sell\n")
|
|
91
50
|
end
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
51
|
+
else
|
|
52
|
+
cost_of_goods = uinv * pinv
|
|
53
|
+
sinv -= uinv
|
|
54
|
+
(0...n).each do |i|
|
|
95
55
|
if sinv <= units[i]
|
|
96
56
|
cost_of_goods += (sinv * price[i])
|
|
97
57
|
ending_inventory = (units[i] - sinv) * price[i]
|
|
98
|
-
if i
|
|
99
|
-
temp = i
|
|
100
|
-
temp
|
|
58
|
+
if i < n
|
|
59
|
+
temp = i + 1
|
|
60
|
+
(temp...n).each do |j|
|
|
101
61
|
ending_inventory += (units[j] * price[j])
|
|
102
62
|
end
|
|
103
63
|
end
|
|
104
|
-
ending_inventory += (uinv * pinv)
|
|
105
64
|
sinv = 0
|
|
106
65
|
break
|
|
107
66
|
else
|
|
@@ -109,18 +68,52 @@ module Finrb
|
|
|
109
68
|
sinv -= units[i]
|
|
110
69
|
end
|
|
111
70
|
end
|
|
112
|
-
if sinv.positive?
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
71
|
+
raise(DomainError, 'Available inventory is insufficient for the requested sale.') if sinv.positive?
|
|
72
|
+
end
|
|
73
|
+
when 'WAC'
|
|
74
|
+
ending_inventory = uinv * pinv
|
|
75
|
+
tu = uinv
|
|
76
|
+
(0...n).each do |i|
|
|
77
|
+
ending_inventory += (units[i] * price[i])
|
|
78
|
+
tu += units[i]
|
|
79
|
+
end
|
|
80
|
+
if tu.zero? && sinv.zero?
|
|
81
|
+
cost_of_goods = Flt::DecNum(0)
|
|
82
|
+
ending_inventory = Flt::DecNum(0)
|
|
83
|
+
elsif tu >= sinv
|
|
84
|
+
cost_of_goods = ending_inventory / tu * sinv
|
|
85
|
+
ending_inventory = ending_inventory / tu * (tu - sinv)
|
|
86
|
+
else
|
|
87
|
+
raise(DomainError, 'Available inventory is insufficient for the requested sale.')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
when 'LIFO'
|
|
91
|
+
(n - 1).downto(0).each do |i|
|
|
92
|
+
if sinv <= units[i]
|
|
93
|
+
cost_of_goods += (sinv * price[i])
|
|
94
|
+
ending_inventory = (units[i] - sinv) * price[i]
|
|
95
|
+
if i.positive?
|
|
96
|
+
temp = i - 1
|
|
97
|
+
temp.downto(0).each do |j|
|
|
98
|
+
ending_inventory += (units[j] * price[j])
|
|
99
|
+
end
|
|
118
100
|
end
|
|
101
|
+
ending_inventory += (uinv * pinv)
|
|
102
|
+
sinv = 0
|
|
103
|
+
break
|
|
104
|
+
else
|
|
105
|
+
cost_of_goods += (units[i] * price[i])
|
|
106
|
+
sinv -= units[i]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
if sinv.positive?
|
|
110
|
+
if sinv <= uinv
|
|
111
|
+
cost_of_goods += (sinv * pinv)
|
|
112
|
+
ending_inventory += ((uinv - sinv) * pinv)
|
|
113
|
+
else
|
|
114
|
+
raise(DomainError, 'Available inventory is insufficient for the requested sale.')
|
|
119
115
|
end
|
|
120
116
|
end
|
|
121
|
-
|
|
122
|
-
else
|
|
123
|
-
raise(Error, "length of units and price are not the same\n")
|
|
124
117
|
end
|
|
125
118
|
|
|
126
119
|
{
|
|
@@ -129,6 +122,20 @@ module Finrb
|
|
|
129
122
|
}
|
|
130
123
|
end
|
|
131
124
|
|
|
125
|
+
def self.inventory_inputs(uinv:, pinv:, units:, price:, sinv:, method:)
|
|
126
|
+
uinv = Validation.non_negative_decimal(uinv, name: 'beginning inventory units')
|
|
127
|
+
pinv = Validation.non_negative_decimal(pinv, name: 'beginning inventory unit cost')
|
|
128
|
+
units = inventory_values(units, name: 'purchase units')
|
|
129
|
+
price = inventory_values(price, name: 'purchase unit cost')
|
|
130
|
+
sinv = Validation.non_negative_decimal(sinv, name: 'units sold')
|
|
131
|
+
method = method.to_s
|
|
132
|
+
raise(ArgumentError, 'Inventory costing method must be FIFO, LIFO, or WAC.') unless %w[FIFO LIFO WAC].include?(method)
|
|
133
|
+
raise(ArgumentError, 'Purchase units and unit costs must have equal lengths.') unless units.size == price.size
|
|
134
|
+
|
|
135
|
+
[uinv, pinv, units, price, sinv, method]
|
|
136
|
+
end
|
|
137
|
+
private_class_method :inventory_inputs
|
|
138
|
+
|
|
132
139
|
# Depreciation Expense Recognition -- double-declining balance (DDB), the most common declining balance method, which applies two times the straight-line rate to the declining balance.
|
|
133
140
|
#
|
|
134
141
|
# @param cost cost of long-lived assets
|
|
@@ -137,11 +144,10 @@ module Finrb
|
|
|
137
144
|
# @example
|
|
138
145
|
# Finrb::Accounting.ddb(cost=1200,rv=200,t=5)
|
|
139
146
|
def self.ddb(cost:, rv:, t:)
|
|
140
|
-
cost =
|
|
141
|
-
rv =
|
|
142
|
-
t =
|
|
143
|
-
|
|
144
|
-
raise(Error, 't should be larger than 1') if t < 2
|
|
147
|
+
cost = Validation.non_negative_decimal(cost, name: 'asset cost')
|
|
148
|
+
rv = residual_value(rv, cost:)
|
|
149
|
+
t = Validation.positive_integer(t, name: 'useful life')
|
|
150
|
+
raise(DomainError, 'Useful life must be at least 2 periods for double-declining depreciation.') if t < 2
|
|
145
151
|
|
|
146
152
|
ddb = [Flt::DecNum(0)] * t
|
|
147
153
|
ddb[0] = cost * 2 / t
|
|
@@ -170,11 +176,24 @@ module Finrb
|
|
|
170
176
|
# @example
|
|
171
177
|
# Finrb::Accounting.slde(cost=1200,rv=200,t=5)
|
|
172
178
|
def self.slde(cost:, rv:, t:)
|
|
173
|
-
cost =
|
|
174
|
-
rv =
|
|
175
|
-
t =
|
|
179
|
+
cost = Validation.non_negative_decimal(cost, name: 'asset cost')
|
|
180
|
+
rv = residual_value(rv, cost:)
|
|
181
|
+
t = Validation.positive_decimal(t, name: 'useful life', error: DomainError)
|
|
176
182
|
|
|
177
183
|
((cost - rv) / t)
|
|
178
184
|
end
|
|
185
|
+
|
|
186
|
+
def self.inventory_values(values, name:)
|
|
187
|
+
wrap_array(values).map { |value| Validation.non_negative_decimal(value, name:) }
|
|
188
|
+
end
|
|
189
|
+
private_class_method :inventory_values
|
|
190
|
+
|
|
191
|
+
def self.residual_value(value, cost:)
|
|
192
|
+
value = Validation.non_negative_decimal(value, name: 'residual value')
|
|
193
|
+
raise(DomainError, 'Residual value must not exceed asset cost.') if value > cost
|
|
194
|
+
|
|
195
|
+
value
|
|
196
|
+
end
|
|
197
|
+
private_class_method :residual_value
|
|
179
198
|
end
|
|
180
199
|
end
|