Skip to content

Conversation

@alastairong1
Copy link

@alastairong1 alastairong1 commented Dec 8, 2025

Motivation

Tokens like Albion are finite life, and their value changes over time, naturally decaying as the underlying asset is depleted, but also changing with oil price. This makes DeFi utility complicated. For example, if using Albion tokens as lending collateral, the lender has to account for the changing (and tending to decline) collateral value.

Solution

We provide two POC strategies for finance 101 valuation of the underlying asset, specifically using the PV approach. These valuations should be aligned with corporate finance methodology used in the O&G industry, but likely lacks the nuance and detail of dedicated trading strategies.

Both strategies rely on hardcoding the production forecasts for the asset, as these are unlikely to change more than every 3-6 months. The precomputed one precomputes various terms to reduce gas fees.

Checks

By submitting this for review, I'm confirming I've done the following:

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Summary by CodeRabbit

  • New Features

    • Folio: Portfolio rebalancer maintaining equal-weight asset distribution
    • Fixed-spread: Pricing strategy using Pyth oracle baseline
    • NPV-DCA: Net present value-based DCA strategies (standard and gas-optimized versions)
    • Base2 network support across all strategies
  • Updates

    • Version upgrades for existing strategies (canary, dynamic-spread, grid)
    • Network metadata and oracle endpoints updated
  • Documentation

    • New strategy documentation added

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 8, 2025

Walkthrough

Updates registry URLs for five existing strategies to a new commit hash and adds folio. Introduces base2 network support across settings and multiple strategy files. Adds four new strategy implementations (fixed-spread, folio, npv-dca, npv-dca-approx) with supporting documentation.

Changes

Cohort / File(s) Summary
Registry Updates
registry.yaml
Updated URLs for fixed-limit, auction-dca, grid, dynamic-spread, and canary from their previous commit-specific locations to commit b051cdb. Added new folio entry pointing to the same commit.
Network Configuration
settings.yaml
Added base2 network with RPC, chain-id 8453, and ETH currency. Added base2 subgraph endpoint and updated arbitrum/arbitrum2 subgraph URLs. Added base2 orderbook with new address and deployer entry.
Version Bumps
src/grid.rain, src/canary.rain, src/dynamic-spread.rain
Incremented version from 1 to 2 across three strategy files.
Subgraph URL Updates
src/canary.rain, src/dynamic-spread.rain
Updated arbitrum subgraph URLs in metaboards from mb-arbitrum/0.1 references to metadata-arbitrum-one with 2025-07-06 timestamps.
Base2 Network Integration
src/auction-dca.rain, src/fixed-limit.rain
Added base2 network definitions, subgraph entries, orderbooks, deployers, and corresponding scenario/deployment configurations. Extended GUI deployments to include base2 variants. Introduced pyth-based baseline price feeds and related bindings.
New Strategy: Fixed-Spread
src/fixed-spread.rain, src/fixed-spread.md
Introduced new fixed-spread strategy using Pyth price feed baseline with configurable multiplier for premium-priced trades. Includes network, order, scenario, and GUI definitions for base2 deployment with presets and IO controls.
New Strategy: Folio
src/folio.rain, src/folio.md
Introduced comprehensive Folio rebalancer configuration for maintaining equal-weight portfolio distribution. Defines network, orderbook, deployer, order, scenario, and GUI metadata for arbitrum and base networks with raindex-subparser IO logic.
New Strategy: NPV-DCA
src/npv-dca.rain, src/npv-dca.md
Introduced NPV-based dollar-cost averaging strategy combining production schedule data, discount factor calculations, and auction mechanics. Includes state management, time-bound epoch handling, and per-token NPV normalization on base2.
New Strategy: NPV-DCA-Approx
src/npv-dca-approx.rain, src/npv-dca-approx.md
Introduced gas-optimized NPV-DCA variant using pre-computed discounted cumulative future production, reducing discount-factor operations from 32 to 1 while maintaining mathematical exactness.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Multiple affected files: 15+ files spanning registry, configuration, and strategy implementations
  • Heterogeneous changes: Mix of trivial URL updates, version bumps, network configuration, and complex new strategy logic
  • Complex new implementations: folio.rain, npv-dca.rain, and npv-dca-approx.rain contain substantial YAML/DSL with financial logic (NPV calculations, discount factors, production schedules, auction mechanics)
  • Consistency verification: Base2 network wiring must be verified across settings.yaml and multiple src files; pyth baseline definitions require validation

Areas requiring extra attention:

  • NPV calculation logic and discount factor computations in npv-dca.rain and npv-dca-approx.rain
  • Folio rebalancer IO and threshold/fee logic in folio.rain
  • Base2 network configuration consistency across all modified strategy files
  • Pyth price feed baseline and multiplier logic in auction-dca.rain and fixed-spread.rain

Possibly related PRs

Suggested labels

webapp

Suggested reviewers

  • hardyjosh
  • findolor
  • 0xgleb

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Albion npv strat' is vague and generic. It uses abbreviations (npv, strat) without clarity and doesn't convey meaningful information about the actual changeset scope. Replace with a more descriptive title that explains the main changes, such as 'Add NPV-based DCA strategy and update strategy registry' or 'Introduce NPV approximation and Folio rebalancer strategies'.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alastairong1 alastairong1 changed the base branch from main to auction-dca-port December 8, 2025 10:04
@alastairong1 alastairong1 changed the base branch from auction-dca-port to feat/orderbookv5 December 8, 2025 10:05
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/auction-dca.rain (1)

321-1582: Deployments and GUI wiring are complete; consider aligning doc URL SHA

All new deployments (arbitrum2, base2, Pyth variants, and folio-style updates) have matching entries in deployments and gui.deployments, and the Pyth GUI flows correctly introduce the pyth-pair binding plus the familiar budget/auction controls and token selectors. Functionally this looks sound.

The GUI description URL is pinned to e25bc4876b5ffb8bb28097b0ca66de291c75ff56, while the registry points strategies at b051cdb7b9ef5e65ab58a16a5b7a1c4eb522039d. If the markdown has changed alongside the strategy code, consider updating the GUI description URL to the same SHA to keep docs and implementation in sync.

Based on learnings about SHA pinning in the registry, keeping docs pinned to the same commit as the .rain file would preserve a consistent snapshot.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1378c75 and 5e7b395.

📒 Files selected for processing (15)
  • registry (1 hunks)
  • settings.yaml (5 hunks)
  • src/auction-dca.rain (16 hunks)
  • src/canary.rain (2 hunks)
  • src/dynamic-spread.rain (2 hunks)
  • src/fixed-limit.rain (10 hunks)
  • src/fixed-spread.md (1 hunks)
  • src/fixed-spread.rain (1 hunks)
  • src/folio.md (1 hunks)
  • src/folio.rain (1 hunks)
  • src/grid.rain (1 hunks)
  • src/npv-dca-approx.md (1 hunks)
  • src/npv-dca-approx.rain (1 hunks)
  • src/npv-dca.md (1 hunks)
  • src/npv-dca.rain (1 hunks)
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: 0xgleb
Repo: rainlanguage/rain.strategies PR: 45
File: registry:0-0
Timestamp: 2025-07-26T15:31:37.916Z
Learning: In the rain.strategies repository, the registry file uses full commit SHAs to pin strategy URLs as the current established pattern, rather than using tags or branches, as confirmed by project maintainer 0xgleb.
Learnt from: findolor
Repo: rainlanguage/rain.strategies PR: 59
File: src/fixed-limit.rain:68-68
Timestamp: 2025-09-01T12:19:42.343Z
Learning: In the rain.strategies repository, the team plans to replace contract addresses in the configuration files, and deployment-block values will be updated as part of that broader effort, so placeholder values like 1 are intentionally temporary.
📚 Learning: 2025-09-01T12:19:42.343Z
Learnt from: findolor
Repo: rainlanguage/rain.strategies PR: 59
File: src/fixed-limit.rain:68-68
Timestamp: 2025-09-01T12:19:42.343Z
Learning: In the rain.strategies repository, the team plans to replace contract addresses in the configuration files, and deployment-block values will be updated as part of that broader effort, so placeholder values like 1 are intentionally temporary.

Applied to files:

  • src/npv-dca-approx.rain
  • src/npv-dca.rain
  • src/fixed-limit.rain
  • src/fixed-spread.rain
  • registry
  • src/auction-dca.rain
  • src/folio.rain
📚 Learning: 2025-09-04T12:59:34.219Z
Learnt from: thedavidmeister
Repo: rainlanguage/rain.strategies PR: 60
File: src/dynamic-spread.rain:924-929
Timestamp: 2025-09-04T12:59:34.219Z
Learning: In the Rain Strategies dynamic spread system, the io values cannot be zero due to business logic constraints where baseline uses any(cost-basis-io last-io) and cost-basis-multiplier is positive, making division-by-zero in inv(stored-last-io) mathematically impossible under normal operation.

Applied to files:

  • src/npv-dca-approx.rain
  • src/npv-dca.rain
  • src/fixed-spread.rain
  • registry
  • src/folio.rain
📚 Learning: 2025-06-19T03:59:46.063Z
Learnt from: Siddharth2207
Repo: rainlanguage/rain.strategies PR: 42
File: src/fixed-limit.rain:19-23
Timestamp: 2025-06-19T03:59:46.063Z
Learning: In rain.strategies configuration files, `arbitrum2` is an intentional alias for the Arbitrum network (same chain-id 42161) but with updated contract addresses. It serves as a deployment target for newer contract versions while maintaining the existing `arbitrum` configuration during transition periods.

Applied to files:

  • src/dynamic-spread.rain
  • src/fixed-limit.rain
  • src/canary.rain
  • registry
  • src/auction-dca.rain
  • settings.yaml
  • src/folio.rain
📚 Learning: 2025-04-30T06:32:43.860Z
Learnt from: findolor
Repo: rainlanguage/rain.strategies PR: 29
File: settings.yaml:2-6
Timestamp: 2025-04-30T06:32:43.860Z
Learning: The `network-id` field is intentionally omitted for the flare network in the `settings.yaml` configuration file, unlike other networks which include both `chain-id` and `network-id` fields.

Applied to files:

  • src/fixed-limit.rain
📚 Learning: 2025-07-03T09:47:48.019Z
Learnt from: thedavidmeister
Repo: rainlanguage/rain.strategies PR: 28
File: src/folio.rain:28-42
Timestamp: 2025-07-03T09:47:48.019Z
Learning: In Rain language strategy files, thedavidmeister prefers intentionally oversimplified setups over complex validation logic, especially in initial implementations like the folio strategy configuration.

Applied to files:

  • src/fixed-spread.rain
  • registry
  • src/folio.rain
📚 Learning: 2025-07-03T09:47:14.894Z
Learnt from: thedavidmeister
Repo: rainlanguage/rain.strategies PR: 28
File: src/folio.rain:115-122
Timestamp: 2025-07-03T09:47:14.894Z
Learning: In Rain language folio strategy, the zero balance check using `every(input-vault-before() output-vault-before())` is specifically implemented to prevent trades from executing while users are depositing or withdrawing from vaults, serving as a safety mechanism against race conditions during vault operations.

Applied to files:

  • src/fixed-spread.rain
  • registry
  • src/folio.rain
📚 Learning: 2025-07-26T15:31:37.916Z
Learnt from: 0xgleb
Repo: rainlanguage/rain.strategies PR: 45
File: registry:0-0
Timestamp: 2025-07-26T15:31:37.916Z
Learning: In the rain.strategies repository, the registry file uses full commit SHAs to pin strategy URLs as the current established pattern, rather than using tags or branches, as confirmed by project maintainer 0xgleb.

Applied to files:

  • registry
📚 Learning: 2025-04-30T06:32:49.466Z
Learnt from: findolor
Repo: rainlanguage/rain.strategies PR: 29
File: settings.yaml:1-35
Timestamp: 2025-04-30T06:32:49.466Z
Learning: In the Rain Strategies project, the `matchain` network referenced in `orderbooks` and `deployers` sections of the configuration YAML doesn't require an explicit definition in the `networks` section.

Applied to files:

  • src/auction-dca.rain
  • src/folio.rain
🪛 markdownlint-cli2 (0.18.1)
src/npv-dca-approx.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

src/npv-dca.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


4-4: Files should end with a single newline character

(MD047, single-trailing-newline)

src/fixed-spread.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


1-1: Files should end with a single newline character

(MD047, single-trailing-newline)

🔇 Additional comments (15)
settings.yaml (3)

14-19: LGTM: base2 network alias follows established pattern.

The addition of base2 as an alias for the Base network (chain-id 8453) with updated contract addresses follows the established pattern used for arbitrum2, which serves as a deployment target for newer contract versions during transition periods.

Based on learnings, this is an intentional configuration pattern in the rain.strategies repository.


59-72: Updated subgraph and metaboard endpoints for base2 and arbitrum2.

The additions introduce newer subgraph endpoints (dated 2025-08-18) for base2 and arbitrum2, while their respective base/arbitrum counterparts retain older endpoints. The metaboard endpoints are consistently shared between each pair.


120-122: Remove base2 deployer address inconsistency concern.

Address differences between configuration files may reflect intentional temporary values during the deployment phase. Per your team's documented practices, placeholder values in configuration are expected to be updated as part of the broader deployment effort. Verify that both addresses are intentionally temporary or clarify the correct canonical address before flagging this as an issue.

src/dynamic-spread.rain (1)

1-1: LGTM: Version bump and metaboard URL update.

The version increment to 2 and the arbitrum metaboard URL update to the newer metadata-arbitrum-one endpoint are consistent with the broader infrastructure updates in this PR. No functional logic changes.

Also applies to: 122-122

src/canary.rain (1)

1-1: LGTM: Version bump and metaboard URL update.

The version increment to 2 and the arbitrum metaboard URL update align with the infrastructure modernization across all strategy files in this PR. No functional changes to the canary strategy logic.

Also applies to: 44-44

src/grid.rain (1)

1-1: LGTM: Version bump.

The version increment to 2 maintains consistency with other strategy files in this PR. No functional changes.

src/fixed-spread.rain (2)

11-12: Verify the placeholder subgraph URL.

The subgraph URL https://example.com/subgraph at lines 11-12 requires verification to confirm it is a placeholder rather than intentional configuration, and to identify the correct production URL.


204-208: Verify Pyth oracle confidence parameter format and value.

The confidence parameter 0.0000000000000036 uses a decimal format, but Pyth documentation specifies that confidence should be an integer mantissa (actual_confidence = conf × 10^exponent) applied with the same exponent as the price. Confirm this value aligns with Pyth's specification and that the Rain language implementation correctly interprets this format.

src/folio.rain (1)

22-23: Placeholder subgraph URLs detected.

Both subgraph entries use https://example.com/subgraph. Verify these are intentionally placeholder values or update to actual subgraph endpoints before production deployment.

src/fixed-limit.rain (2)

16-21: LGTM!

The base2 network definition follows the established pattern for network aliases with updated contract addresses. The configuration is consistent with existing networks.


78-79: Verify address consistency between base and base2.

base2 uses the same deployer address as base (line 95 vs 97: 0xC1A14cE2fd58A3A2f99deCb8eDd866204eE07f8D) but a different orderbook address (0x881cf4c0764e733d9C387f3858eE87CcA04AFFe0 vs 0xd2938e7c9fe3597f78832ce780feb61945c377d7). Confirm this is intentional for the updated orderbook deployment.

Also applies to: 96-97

registry (1)

1-6: Registry pinning and folio addition look consistent

All strategy entries (including the new folio) are pinned to the same full commit SHA and follow the established raw.githubusercontent.com/.../src/*.rain pattern, matching the registry convention of SHA pinning.

Based on learnings, this aligns with the existing registry style of using full commit SHAs.

src/auction-dca.rain (3)

1-108: Alias network wiring for arbitrum2/base2 is coherent

arbitrum2 and base2 reuse the same RPCs, chain/network IDs, and currency as arbitrum/base, and have corresponding subgraphs, metaboards, orderbooks, and deployers entries wired up consistently. This matches the documented pattern of alias networks with updated contract addresses.

Based on learnings, the alias-network setup for arbitrum2 is intentional and this extension to base2 follows the same approach.


119-319: Orders and scenarios (including Pyth variants) are wired consistently

orders and scenarios for arbitrum2 and base2 mirror the existing networks, and the new Pyth scenarios correctly reference the alias orderbooks/deployers and the pyth-price-baseline / pyth-price-baseline-inv functions. No mismatched names or missing backing definitions stand out.


1983-2103: Pyth helper words and binding comment integrate cleanly

The new #pyth-pair comment plus pyth-price-baseline / pyth-price-baseline-inv helpers are consistent with how other baseline helpers are defined and are correctly referenced by the new Pyth scenarios and GUI bindings. No structural or naming issues spotted.

@@ -0,0 +1 @@
This strategy has the ratio track a benchmark price (e.g. pyth) with a fixed multiplier. As long as that multiplier is >1, the strategy will always offer to trade at a premium to the benchmark, thereby increasing the likelihood that the deployer can "buy low sell high". No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Fix documentation formatting issues.

The documentation file has minor formatting issues flagged by static analysis:

  • Missing top-level heading (MD041)
  • Missing trailing newline (MD047)

Apply this diff to improve formatting:

+# Fixed Spread Strategy
+
 This strategy has the ratio track a benchmark price (e.g. pyth) with a fixed multiplier. As long as that multiplier is >1, the strategy will always offer to trade at a premium to the benchmark, thereby increasing the likelihood that the deployer can "buy low sell high".
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
This strategy has the ratio track a benchmark price (e.g. pyth) with a fixed multiplier. As long as that multiplier is >1, the strategy will always offer to trade at a premium to the benchmark, thereby increasing the likelihood that the deployer can "buy low sell high".
# Fixed Spread Strategy
This strategy has the ratio track a benchmark price (e.g. pyth) with a fixed multiplier. As long as that multiplier is >1, the strategy will always offer to trade at a premium to the benchmark, thereby increasing the likelihood that the deployer can "buy low sell high".
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


1-1: Files should end with a single newline character

(MD047, single-trailing-newline)

🤖 Prompt for AI Agents
In src/fixed-spread.md around lines 1 to 1, the markdown is missing a top-level
heading and a trailing newline; add a descriptive H1 heading at the top (e.g.,
"# Fixed Spread Strategy") and ensure the file ends with a single trailing
newline character so the file conforms to MD041 and MD047.

Comment on lines +25 to +33
tokens:
flare-wflr:
network: flare
address: 0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d
decimals: 18
flare-sflr:
network: flare
address: 0x12e605bc104e93B45e1aD99F9e555f659051c2BB
decimals: 18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused Flare token definitions.

The flare-wflr and flare-sflr token definitions appear to be unused in this configuration, which focuses exclusively on the base2 network. These may be copy-paste residue from another strategy file.

Apply this diff to remove the unused tokens:

-tokens:
-  flare-wflr:
-    network: flare
-    address: 0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d
-    decimals: 18
-  flare-sflr:
-    network: flare
-    address: 0x12e605bc104e93B45e1aD99F9e555f659051c2BB
-    decimals: 18
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokens:
flare-wflr:
network: flare
address: 0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d
decimals: 18
flare-sflr:
network: flare
address: 0x12e605bc104e93B45e1aD99F9e555f659051c2BB
decimals: 18
🤖 Prompt for AI Agents
In src/fixed-spread.rain around lines 25 to 33, remove the unused flare-wflr and
flare-sflr token entries (their network/address/decimals mappings) which appear
to be copy-paste residue; after deleting those entries, ensure the tokens: map
remains valid YAML (no dangling commas or incorrect indentation) and run a quick
grep/lookup in the repo to confirm no other references to these token keys exist
before committing.

description: Select the token you want to purchase
base-pyth-inv:
name: Base pyth buy shares
description: Deploy an fixed-spread trategy on Base using Pyth price feeds to buy shares.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in description.

Line 135 contains "trategy" which should be "strategy".

Apply this diff:

-      description: Deploy an fixed-spread trategy on Base using Pyth price feeds to buy shares.
+      description: Deploy an fixed-spread strategy on Base using Pyth price feeds to buy shares.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: Deploy an fixed-spread trategy on Base using Pyth price feeds to buy shares.
description: Deploy an fixed-spread strategy on Base using Pyth price feeds to buy shares.
🤖 Prompt for AI Agents
In src/fixed-spread.rain at line 135, the description contains a typo: "trategy"
should be "strategy"; update the description text to read "Deploy a fixed-spread
strategy on Base using Pyth price feeds to buy shares." ensuring grammar (change
"an" to "a") and correct spelling.

Comment on lines +179 to +183
- binding: multiplier
name: Benchmark price multiplier
description: |
The multiplier to be applied to the benchmark when calculating the price the strategy will offer at
default: 1.001
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent binding name between deployments.

The base-pyth deployment uses baseline-multiplier (line 121) while base-pyth-inv uses multiplier (line 179). This inconsistency could cause configuration errors or unexpected behavior.

Apply this diff to maintain consistency:

-        - binding: multiplier
+        - binding: baseline-multiplier
           name: Benchmark price multiplier
           description: |
             The multiplier to be applied to the benchmark when calculating the price the strategy will offer at
           default: 1.001
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- binding: multiplier
name: Benchmark price multiplier
description: |
The multiplier to be applied to the benchmark when calculating the price the strategy will offer at
default: 1.001
- binding: baseline-multiplier
name: Benchmark price multiplier
description: |
The multiplier to be applied to the benchmark when calculating the price the strategy will offer at
default: 1.001
🤖 Prompt for AI Agents
In src/fixed-spread.rain around lines 179 to 183, the binding key is named
"multiplier" while the other deployment uses "baseline-multiplier" (line 121);
rename the binding from "multiplier" to "baseline-multiplier" and keep the same
name, description and default (1.001) so both deployments use the identical
binding name, and update any local references in this file that read the old
"multiplier" key to the new "baseline-multiplier".

Comment on lines 1 to 3
# Folio Rebalancer

Folio rebalances a portfolio of tokens to maintain an equal weight distribution across all assets.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minimal but clear documentation.

The Folio Rebalancer documentation is brief but provides a clear high-level description. Consider expanding this in future iterations to include usage examples, configuration parameters, or rebalancing logic details.

🤖 Prompt for AI Agents
In src/folio.md around lines 1 to 3, the documentation is minimal; expand the
README to include a short usage section, configuration parameters, and a brief
summary of the rebalancing algorithm: add example CLI/SDK usage (commands or
code snippets), list configurable options (rebalance interval, tolerance
thresholds, allowed tokens, fee settings), and outline the rebalancing
steps/logic (how weights are computed, trade execution order, and edge-case
handling). Keep each addition concise with headings and a one-paragraph
explanation for each item to improve clarity for future readers.

Comment on lines +510 to +518
#io-for-epoch
epoch:,
last-io: call<'get-last-trade>(),
max-next-trade: any(mul(last-io next-trade-multiplier) call<'initial-io-fn>()),
baseline-next-trade: mul(last-io next-trade-baseline-multiplier),
real-baseline: max(baseline-next-trade call<'baseline-fn>()),
variable-component: saturating-sub(max-next-trade real-baseline),
above-baseline: mul(variable-component call<'halflife>(epoch)),
_: add(real-baseline above-baseline);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

io-for-epoch only captures one value from get-last-trade which returns two.

get-last-trade returns (last-time, last-io) but line 512 only captures last-io. The last-time value is silently dropped. If this is intentional, consider using _ to explicitly discard the first return value for clarity.

Apply this diff:

 #io-for-epoch
 epoch:,
-last-io: call<'get-last-trade>(),
+_ last-io: call<'get-last-trade>(),

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/npv-dca-approx.rain around lines 510 to 518, the call to get-last-trade
only binds last-io but get-last-trade returns (last-time, last-io), so last-time
is being silently dropped; update the binding to capture both return values
(e.g., last-time last-io) or explicitly discard the first value with _ last-io
to make intent clear, then use last-io as before (no other logic changes).

Comment on lines +1 to +4
This is a variant of the auction-dca strategy. In this strategy, the floor price for the auction is calculated as the "fair value" of the token based on present value of future cash flows. The production profiles and discount rates are hardcoded. The fair value will vary based on:
- Benchmark oil prices, which are fetched from pyth oracle
- The current time (as time progresses more of the production gets discarded as already historical)
- Token supply, if new mints occur No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Fix documentation formatting issues.

The documentation file has minor formatting issues flagged by static analysis:

  • Missing top-level heading (MD041)
  • Missing trailing newline (MD047)

Apply this diff to improve formatting:

+# NPV-based DCA Strategy
+
 This is a variant of the auction-dca strategy. In this strategy, the floor price for the auction is calculated as the "fair value" of the token based on present value of future cash flows. The production profiles and discount rates are hardcoded. The fair value will vary based on:
 - Benchmark oil prices, which are fetched from pyth oracle
 - The current time (as time progresses more of the production gets discarded as already historical)
 - Token supply, if new mints occur
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
This is a variant of the auction-dca strategy. In this strategy, the floor price for the auction is calculated as the "fair value" of the token based on present value of future cash flows. The production profiles and discount rates are hardcoded. The fair value will vary based on:
- Benchmark oil prices, which are fetched from pyth oracle
- The current time (as time progresses more of the production gets discarded as already historical)
- Token supply, if new mints occur
# NPV-based DCA Strategy
This is a variant of the auction-dca strategy. In this strategy, the floor price for the auction is calculated as the "fair value" of the token based on present value of future cash flows. The production profiles and discount rates are hardcoded. The fair value will vary based on:
- Benchmark oil prices, which are fetched from pyth oracle
- The current time (as time progresses more of the production gets discarded as already historical)
- Token supply, if new mints occur
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


4-4: Files should end with a single newline character

(MD047, single-trailing-newline)

🤖 Prompt for AI Agents
In src/npv-dca.md around lines 1 to 4, the Markdown file is missing a top-level
heading and a trailing newline; add a clear H1 title (e.g., "# NPV DCA Strategy"
or an appropriate descriptive top-level heading) at the top of the file and
ensure the file ends with a newline character so the document passes MD041 and
MD047 checks.

Comment on lines +1 to +518
version: 2

networks:
base2:
rpcs:
- https://base-rpc.publicnode.com
chain-id: 8453
network-id: 8453
currency: ETH

subgraphs:
base2: https://example.com/subgraph

metaboards:
base2: https://api.goldsky.com/api/public/project_clv14x04y9kzi01saerx7bxpg/subgraphs/metadata-base/2025-07-06-594f/gn

orderbooks:
base2:
address: 0x881cf4c0764e733d9C387f3858eE87CcA04AFFe0

deployers:
base2:
address: 0xC1A14cE2fd58A3A2f99deCb8eDd866204eE07f8D

tokens:
base-usdc:
network: base2
address: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
decimals: 6

orders:
base2:
orderbook: base2
inputs:
- token: input
outputs:
- token: output

scenarios:
base2:
orderbook: base2
deployer: base2
runs: 1
bindings:
raindex-subparser: 0xb05BC61963B6bEBAe9C2FEd827de600E99FB83fD
pyth-subparser: 0x2fbBe5715c3b40aD455992dD75129e7e0733ec7a
baseline-fn: '''npv-baseline'
initial-io-fn: '''npv-baseline'
next-trade-baseline-multiplier: 0
shy-epoch: 0.05

deployments:
base2:
order: base2
scenario: base2

gui:
name: NPV-based DCA Strategy
description: A strategy that sells tokens at or above their calculated NPV floor price.
short-description: Sells tokens using NPV of future cashflows as the floor price.
deployments:
base2:
name: Base NPV DCA
description: Deploy an NPV-based DCA strategy on Base.
deposits:
- token: output
fields:
- binding: pyth-feed
name: Pyth price feed ID
description: The Pyth feed ID for the commodity price (e.g., natural gas).
- binding: price-adjustment
name: Price adjustment
description: Amount to subtract from Pyth price (e.g., 1.3 for transport/basis differential).
default: 1.3
- binding: disc-month
name: Monthly discount rate
description: Monthly discount rate as decimal (e.g., 0.008 for 0.8%).
default: 0.008
- binding: time-per-amount-epoch
name: Budget period (seconds)
description: The budget period for amount streaming.
show-custom-field: true
default: 86400
presets:
- name: Per day (86400)
value: 86400
- name: Per week (604800)
value: 604800
- binding: amount-per-epoch
name: Budget per period
description: Amount of tokens to sell per budget period.
- binding: max-trade-amount
name: Maximum trade size
description: Maximum tokens to sell in a single trade.
- binding: min-trade-amount
name: Minimum trade size
description: Minimum tokens to sell in a single trade.
- binding: time-per-trade-epoch
name: Auction period (seconds)
description: Time between auction price halvenings.
show-custom-field: true
default: 3600
presets:
- name: Every hour (3600)
value: 3600
- name: Every 2 hours (7200)
value: 7200
- binding: next-trade-multiplier
name: Auction start multiplier
description: Multiplier above last trade to start next auction.
show-custom-field: true
default: 1.01
presets:
- name: 1.01x
value: 1.01
- name: 1.02x
value: 1.02
- name: 1.05x
value: 1.05
select-tokens:
- key: output
name: Token to Sell
description: Select the token you want to sell (represents cashflow ownership)
- key: input
name: Token to Buy
description: Select the token you want to receive (e.g., USDC)

---
#raindex-subparser !Raindex subparser.
#pyth-subparser !Pyth oracle subparser.

#time-per-amount-epoch !Duration of one unit of streaming amount halflife.
#amount-per-epoch !Amount of output token to approve for selling per epoch.
#min-trade-amount !Each trade must be at least this many output tokens.
#max-trade-amount !Each trade will be capped at this many tokens.

#time-per-trade-epoch !Duration of one unit of io ratio halflife.
#shy-epoch !Epoch below which only the minimum amount is offered.

#baseline-fn !Function to calculate the baseline (floor) price.
#initial-io-fn !Function to calculate the initial io ratio.

#next-trade-multiplier !Start next auction at this x the last trade.
#next-trade-baseline-multiplier !Lifts baseline relative to previous trade.

#pyth-feed !The Pyth feed ID for the commodity price.
#price-adjustment !Amount to subtract from Pyth price for basis/transport.
#disc-month !Monthly discount rate (e.g., 0.008 = 0.8%).
#avg-month-seconds 2629746

/* ============ PRODUCTION SCHEDULE (EOM timestamps and production amounts) ============ */
/* Each row: prev_eom timestamp, this_eom timestamp, production amount */

#row0-prev 1746057599
#row0-eom 1748735999
#row0-prod 347.76

#row1-prev 1748735999
#row1-eom 1751327999
#row1-prod 330.885

#row2-prev 1751327999
#row2-eom 1754006399
#row2-prod 336.24

#row3-prev 1754006399
#row3-eom 1756684799
#row3-prod 330.615

#row4-prev 1756684799
#row4-eom 1759276799
#row4-prod 314.64

#row5-prev 1759276799
#row5-eom 1761955199
#row5-prod 319.725

#row6-prev 1761955199
#row6-eom 1764547199
#row6-prod 304.245

#row7-prev 1764547199
#row7-eom 1767225599
#row7-prod 302.85

#row8-prev 1767225599
#row8-eom 1769903999
#row8-prod 297.72

#row9-prev 1769903999
#row9-eom 1772323199
#row9-prod 252.675

#row10-prev 1772323199
#row10-eom 1775001599
#row10-prod 280.08

#row11-prev 1775001599
#row11-eom 1777593599
#row11-prod 136.26

#row12-prev 1777593599
#row12-eom 1780271999
#row12-prod 397.125

#row13-prev 1780271999
#row13-eom 1782863999
#row13-prod 339.75

#row14-prev 1782863999
#row14-eom 1785542399
#row14-prod 328.095

#row15-prev 1785542399
#row15-eom 1788220799
#row15-prod 305.1

#row16-prev 1788220799
#row16-eom 1790812799
#row16-prod 301.32

#row17-prev 1790812799
#row17-eom 1793491199
#row17-prod 317.025

#row18-prev 1793491199
#row18-eom 1796083199
#row18-prod 307.665

#row19-prev 1796083199
#row19-eom 1798761599
#row19-prod 311.355

#row20-prev 1798761599
#row20-eom 1801439999
#row20-prod 273.96

#row21-prev 1801439999
#row21-eom 1803859199
#row21-prod 221.58

#row22-prev 1803859199
#row22-eom 1806537599
#row22-prod 229.14

#row23-prev 1806537599
#row23-eom 1809129599
#row23-prod 219.375

#row24-prev 1809129599
#row24-eom 1811807999
#row24-prod 227.655

#row25-prev 1811807999
#row25-eom 1814399999
#row25-prod 217.08

#row26-prev 1814399999
#row26-eom 1817078399
#row26-prod 217.755

#row27-prev 1817078399
#row27-eom 1819756799
#row27-prod 203.85

#row28-prev 1819756799
#row28-eom 1822348799
#row28-prod 184.455

#row29-prev 1822348799
#row29-eom 1825027199
#row29-prod 177.885

#row30-prev 1825027199
#row30-eom 1827619199
#row30-prod 159.84

#row31-prev 1827619199
#row31-eom 1830297599
#row31-prod 151.02

/* ============ STATE KEYS ============ */
#last-trade-time-key "last-trade-time"
#last-trade-io-key "last-trade-io"
#initial-time-key "initial-time"
#amount-used-key "amount-used"

/* ============ DISCOUNT FACTOR HELPER ============ */
/* Returns discount factor from now to payment time */
/* df = (1 + r)^(-t) where t is time in months */
#discount-factor
paytime:,
time-to-payment: saturating-sub(paytime now()),
months-to-payment: div(time-to-payment avg-month-seconds),
discount-base: inv(add(1 disc-month)),
_: power(discount-base months-to-payment);

/* ============ STEP FUNCTION HELPER ============ */
/* Returns 1 if prev < t <= eom, else 0 */
#step-eom
t prev-eom this-eom:,
after-prev: greater-than(t prev-eom),
at-or-before-eom: greater-than-or-equal-to(this-eom t),
_: mul(after-prev at-or-before-eom);

/* ============ GET ADJUSTED PRICE ============ */
#get-adjusted-price
raw-price: pyth-price(pyth-feed 0.0000000000000036),
_: saturating-sub(raw-price price-adjustment);

/* ============ NPV CALCULATION ============ */
#npv-baseline
t: now(),
adj-price: call<'get-adjusted-price>(),

/* Determine which month we're in using step indicators */
i0: call<'step-eom>(t row0-prev row0-eom),
i1: call<'step-eom>(t row1-prev row1-eom),
i2: call<'step-eom>(t row2-prev row2-eom),
i3: call<'step-eom>(t row3-prev row3-eom),
i4: call<'step-eom>(t row4-prev row4-eom),
i5: call<'step-eom>(t row5-prev row5-eom),
i6: call<'step-eom>(t row6-prev row6-eom),
i7: call<'step-eom>(t row7-prev row7-eom),
i8: call<'step-eom>(t row8-prev row8-eom),
i9: call<'step-eom>(t row9-prev row9-eom),
i10: call<'step-eom>(t row10-prev row10-eom),
i11: call<'step-eom>(t row11-prev row11-eom),
i12: call<'step-eom>(t row12-prev row12-eom),
i13: call<'step-eom>(t row13-prev row13-eom),
i14: call<'step-eom>(t row14-prev row14-eom),
i15: call<'step-eom>(t row15-prev row15-eom),
i16: call<'step-eom>(t row16-prev row16-eom),
i17: call<'step-eom>(t row17-prev row17-eom),
i18: call<'step-eom>(t row18-prev row18-eom),
i19: call<'step-eom>(t row19-prev row19-eom),
i20: call<'step-eom>(t row20-prev row20-eom),
i21: call<'step-eom>(t row21-prev row21-eom),
i22: call<'step-eom>(t row22-prev row22-eom),
i23: call<'step-eom>(t row23-prev row23-eom),
i24: call<'step-eom>(t row24-prev row24-eom),
i25: call<'step-eom>(t row25-prev row25-eom),
i26: call<'step-eom>(t row26-prev row26-eom),
i27: call<'step-eom>(t row27-prev row27-eom),
i28: call<'step-eom>(t row28-prev row28-eom),
i29: call<'step-eom>(t row29-prev row29-eom),
i30: call<'step-eom>(t row30-prev row30-eom),
i31: call<'step-eom>(t row31-prev row31-eom),

/* Recover the current month's boundaries and production */
next-eom: add(
mul(i0 row0-eom) mul(i1 row1-eom) mul(i2 row2-eom) mul(i3 row3-eom)
mul(i4 row4-eom) mul(i5 row5-eom) mul(i6 row6-eom) mul(i7 row7-eom)
mul(i8 row8-eom) mul(i9 row9-eom) mul(i10 row10-eom) mul(i11 row11-eom)
mul(i12 row12-eom) mul(i13 row13-eom) mul(i14 row14-eom) mul(i15 row15-eom)
mul(i16 row16-eom) mul(i17 row17-eom) mul(i18 row18-eom) mul(i19 row19-eom)
mul(i20 row20-eom) mul(i21 row21-eom) mul(i22 row22-eom) mul(i23 row23-eom)
mul(i24 row24-eom) mul(i25 row25-eom) mul(i26 row26-eom) mul(i27 row27-eom)
mul(i28 row28-eom) mul(i29 row29-eom) mul(i30 row30-eom) mul(i31 row31-eom)
),
prev-eom: add(
mul(i0 row0-prev) mul(i1 row1-prev) mul(i2 row2-prev) mul(i3 row3-prev)
mul(i4 row4-prev) mul(i5 row5-prev) mul(i6 row6-prev) mul(i7 row7-prev)
mul(i8 row8-prev) mul(i9 row9-prev) mul(i10 row10-prev) mul(i11 row11-prev)
mul(i12 row12-prev) mul(i13 row13-prev) mul(i14 row14-prev) mul(i15 row15-prev)
mul(i16 row16-prev) mul(i17 row17-prev) mul(i18 row18-prev) mul(i19 row19-prev)
mul(i20 row20-prev) mul(i21 row21-prev) mul(i22 row22-prev) mul(i23 row23-prev)
mul(i24 row24-prev) mul(i25 row25-prev) mul(i26 row26-prev) mul(i27 row27-prev)
mul(i28 row28-prev) mul(i29 row29-prev) mul(i30 row30-prev) mul(i31 row31-prev)
),
next-prod: add(
mul(i0 row0-prod) mul(i1 row1-prod) mul(i2 row2-prod) mul(i3 row3-prod)
mul(i4 row4-prod) mul(i5 row5-prod) mul(i6 row6-prod) mul(i7 row7-prod)
mul(i8 row8-prod) mul(i9 row9-prod) mul(i10 row10-prod) mul(i11 row11-prod)
mul(i12 row12-prod) mul(i13 row13-prod) mul(i14 row14-prod) mul(i15 row15-prod)
mul(i16 row16-prod) mul(i17 row17-prod) mul(i18 row18-prod) mul(i19 row19-prod)
mul(i20 row20-prod) mul(i21 row21-prod) mul(i22 row22-prod) mul(i23 row23-prod)
mul(i24 row24-prod) mul(i25 row25-prod) mul(i26 row26-prod) mul(i27 row27-prod)
mul(i28 row28-prod) mul(i29 row29-prod) mul(i30 row30-prod) mul(i31 row31-prod)
),

/* Prorate first partial month: (next_eom - now) / (next_eom - prev_eom) * production */
month-length: saturating-sub(next-eom prev-eom),
remaining-in-month: saturating-sub(next-eom t),
proration: div(remaining-in-month any(month-length 1)),
first-prod: mul(next-prod proration),

/* PV of first partial cashflow at next_eom */
pv-first: mul(mul(first-prod adj-price) call<'discount-factor>(next-eom)),

/* PV of all FULL future months (strictly after next_eom) */
/* Each term: production * price * (eom > next_eom ? discount_factor : 0) */
pv-rest: add(
mul(mul(row0-prod adj-price) mul(greater-than(row0-eom next-eom) call<'discount-factor>(row0-eom)))
mul(mul(row1-prod adj-price) mul(greater-than(row1-eom next-eom) call<'discount-factor>(row1-eom)))
mul(mul(row2-prod adj-price) mul(greater-than(row2-eom next-eom) call<'discount-factor>(row2-eom)))
mul(mul(row3-prod adj-price) mul(greater-than(row3-eom next-eom) call<'discount-factor>(row3-eom)))
mul(mul(row4-prod adj-price) mul(greater-than(row4-eom next-eom) call<'discount-factor>(row4-eom)))
mul(mul(row5-prod adj-price) mul(greater-than(row5-eom next-eom) call<'discount-factor>(row5-eom)))
mul(mul(row6-prod adj-price) mul(greater-than(row6-eom next-eom) call<'discount-factor>(row6-eom)))
mul(mul(row7-prod adj-price) mul(greater-than(row7-eom next-eom) call<'discount-factor>(row7-eom)))
mul(mul(row8-prod adj-price) mul(greater-than(row8-eom next-eom) call<'discount-factor>(row8-eom)))
mul(mul(row9-prod adj-price) mul(greater-than(row9-eom next-eom) call<'discount-factor>(row9-eom)))
mul(mul(row10-prod adj-price) mul(greater-than(row10-eom next-eom) call<'discount-factor>(row10-eom)))
mul(mul(row11-prod adj-price) mul(greater-than(row11-eom next-eom) call<'discount-factor>(row11-eom)))
mul(mul(row12-prod adj-price) mul(greater-than(row12-eom next-eom) call<'discount-factor>(row12-eom)))
mul(mul(row13-prod adj-price) mul(greater-than(row13-eom next-eom) call<'discount-factor>(row13-eom)))
mul(mul(row14-prod adj-price) mul(greater-than(row14-eom next-eom) call<'discount-factor>(row14-eom)))
mul(mul(row15-prod adj-price) mul(greater-than(row15-eom next-eom) call<'discount-factor>(row15-eom)))
mul(mul(row16-prod adj-price) mul(greater-than(row16-eom next-eom) call<'discount-factor>(row16-eom)))
mul(mul(row17-prod adj-price) mul(greater-than(row17-eom next-eom) call<'discount-factor>(row17-eom)))
mul(mul(row18-prod adj-price) mul(greater-than(row18-eom next-eom) call<'discount-factor>(row18-eom)))
mul(mul(row19-prod adj-price) mul(greater-than(row19-eom next-eom) call<'discount-factor>(row19-eom)))
mul(mul(row20-prod adj-price) mul(greater-than(row20-eom next-eom) call<'discount-factor>(row20-eom)))
mul(mul(row21-prod adj-price) mul(greater-than(row21-eom next-eom) call<'discount-factor>(row21-eom)))
mul(mul(row22-prod adj-price) mul(greater-than(row22-eom next-eom) call<'discount-factor>(row22-eom)))
mul(mul(row23-prod adj-price) mul(greater-than(row23-eom next-eom) call<'discount-factor>(row23-eom)))
mul(mul(row24-prod adj-price) mul(greater-than(row24-eom next-eom) call<'discount-factor>(row24-eom)))
mul(mul(row25-prod adj-price) mul(greater-than(row25-eom next-eom) call<'discount-factor>(row25-eom)))
mul(mul(row26-prod adj-price) mul(greater-than(row26-eom next-eom) call<'discount-factor>(row26-eom)))
mul(mul(row27-prod adj-price) mul(greater-than(row27-eom next-eom) call<'discount-factor>(row27-eom)))
mul(mul(row28-prod adj-price) mul(greater-than(row28-eom next-eom) call<'discount-factor>(row28-eom)))
mul(mul(row29-prod adj-price) mul(greater-than(row29-eom next-eom) call<'discount-factor>(row29-eom)))
mul(mul(row30-prod adj-price) mul(greater-than(row30-eom next-eom) call<'discount-factor>(row30-eom)))
mul(mul(row31-prod adj-price) mul(greater-than(row31-eom next-eom) call<'discount-factor>(row31-eom)))
),

/* Total NPV = PV(first partial) + PV(all future full months) */
total-npv: add(pv-first pv-rest),

/* Divide by total supply to get per-token NPV */
total-supply: erc20-total-supply(output-token()),
_: div(total-npv total-supply);

/* ============ AUCTION DCA MECHANICS (from auction-dca.rain) ============ */

#set-last-trade
last-io:,
:set(hash(order-hash() last-trade-time-key) now()),
:set(hash(order-hash() last-trade-io-key) last-io);

#set-initial-time
:set(hash(order-hash() initial-time-key) now());

#get-initial-time
_: get(hash(order-hash() initial-time-key));

#get-last-trade
last-time: get(hash(order-hash() last-trade-time-key)),
last-io: get(hash(order-hash() last-trade-io-key));

#get-epoch
initial-time: call<'get-initial-time>(),
last-time _: call<'get-last-trade>(),
duration: sub(now() any(last-time initial-time)),
total-duration: sub(now() initial-time),
ratio-freeze-amount-epochs: div(min-trade-amount amount-per-epoch),
ratio-freeze-trade-epochs: mul(ratio-freeze-amount-epochs div(time-per-amount-epoch time-per-trade-epoch)),
amount-epochs: div(total-duration time-per-amount-epoch),
trade-epochs: saturating-sub(div(duration time-per-trade-epoch) ratio-freeze-trade-epochs);

#amount-for-epoch
amount-epochs
trade-epochs:,
total-available: linear-growth(0 amount-per-epoch amount-epochs),
used: get(hash(order-hash() amount-used-key)),
unused: sub(total-available used),
decay: call<'halflife>(trade-epochs),
shy-decay: every(greater-than(trade-epochs shy-epoch) decay),
variable-component: sub(max-trade-amount min-trade-amount),
target-amount: add(min-trade-amount mul(variable-component shy-decay)),
capped-unused: min(unused target-amount);

#halflife
epoch:,
multiplier: power(0.5 div(epoch 10)),
val: mul(
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
);

#io-for-epoch
epoch:,
last-io: call<'get-last-trade>(),
max-next-trade: any(mul(last-io next-trade-multiplier) call<'initial-io-fn>()),
baseline-next-trade: mul(last-io next-trade-baseline-multiplier),
real-baseline: max(baseline-next-trade call<'baseline-fn>()),
variable-component: saturating-sub(max-next-trade real-baseline),
above-baseline: mul(variable-component call<'halflife>(epoch)),
_: add(real-baseline above-baseline);

/* ============ ENTRY POINTS ============ */

#handle-add-order
using-words-from raindex-subparser pyth-subparser
:call<'set-initial-time>();

#calculate-io
using-words-from raindex-subparser pyth-subparser
amount-epochs
trade-epochs: call<'get-epoch>(),
max-output: call<'amount-for-epoch>(amount-epochs trade-epochs),
io: call<'io-for-epoch>(trade-epochs),
:call<'set-last-trade>(io);

#handle-io
min-amount: mul(min-trade-amount 0.9),
:ensure(greater-than-or-equal-to(output-vault-decrease() min-amount) "Min trade amount."),
used: get(hash(order-hash() amount-used-key)),
:set(hash(order-hash() amount-used-key) add(used output-vault-decrease()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting shared code between npv-dca.rain and npv-dca-approx.rain.

Both files share significant code duplication:

  • Auction DCA mechanics (set-last-trade, get-epoch, amount-for-epoch, halflife, io-for-epoch)
  • Production schedule data (32 rows of identical row*-prev, row*-eom, row*-prod)
  • State keys, entry points, and handle-io logic

Consider extracting shared components into a common file to improve maintainability and reduce the risk of divergent bug fixes.

🤖 Prompt for AI Agents
In src/npv-dca.rain lines 1-518, large blocks are duplicated with
npv-dca-approx.rain (auction DCA mechanics, 32-row production schedule, state
keys, and entry points). Extract the shared pieces into a new common file (e.g.,
src/npv-shared.rain) containing: the production rows and row constants,
discount/step/adjusted-price/npv helpers, auction helper functions
(set-last-trade, get-initial-time/get-last-trade/get-epoch, amount-for-epoch,
halflife, io-for-epoch), state key constants, and shared entry point wiring;
then update both npv-dca.rain and npv-dca-approx.rain to remove the duplicated
blocks and import/compose the shared file (use the Rain module/include pattern
your project uses or expose the helper words and call them via using-words-from
or equivalent), keeping public function names and keys unchanged so existing
bindings/calls continue to work.

Comment on lines +474 to +488
#halflife
epoch:,
multiplier: power(0.5 div(epoch 10)),
val: mul(
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

halflife function declares but does not return val.

Same issue as in npv-dca-approx.rain: the function computes val but doesn't return it.

Apply this diff:

 #halflife
 epoch:,
 multiplier: power(0.5 div(epoch 10)),
-val: mul(
+_: mul(
   multiplier
   multiplier
   multiplier
   multiplier
   multiplier
   multiplier
   multiplier
   multiplier
   multiplier
   multiplier
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#halflife
epoch:,
multiplier: power(0.5 div(epoch 10)),
val: mul(
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
);
#halflife
epoch:,
multiplier: power(0.5 div(epoch 10)),
_: mul(
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
multiplier
);
🤖 Prompt for AI Agents
In src/npv-dca.rain around lines 474 to 488 the halflife function computes a
local variable named val but never returns it; modify the function to return val
(e.g., add a final expression or explicit return of val as the function result)
so the computed value is exposed to callers, ensuring the function body ends
with val as the returned value.

Comment on lines +490 to +498
#io-for-epoch
epoch:,
last-io: call<'get-last-trade>(),
max-next-trade: any(mul(last-io next-trade-multiplier) call<'initial-io-fn>()),
baseline-next-trade: mul(last-io next-trade-baseline-multiplier),
real-baseline: max(baseline-next-trade call<'baseline-fn>()),
variable-component: saturating-sub(max-next-trade real-baseline),
above-baseline: mul(variable-component call<'halflife>(epoch)),
_: add(real-baseline above-baseline);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

io-for-epoch only captures one value from get-last-trade which returns two.

Same issue as in npv-dca-approx.rain: get-last-trade returns (last-time, last-io) but only last-io is captured.

Apply this diff:

 #io-for-epoch
 epoch:,
-last-io: call<'get-last-trade>(),
+_ last-io: call<'get-last-trade>(),
🤖 Prompt for AI Agents
In src/npv-dca.rain around lines 490 to 498, the io-for-epoch binding calls
get-last-trade which returns two values (last-time, last-io) but the code only
captures last-io; update the binding to destructure both returned values (e.g.,
bind last-time and last-io) and then keep all subsequent uses of last-io
unchanged (or reference last-time where appropriate if time is needed). Ensure
the call<'get-last-trade>() line is changed to capture both results and that no
other bindings shadow these names.

@alastairong1 alastairong1 changed the base branch from feat/orderbookv5 to 2025-04-22-folio December 8, 2025 10:12


#handle-io
:ensure(greater-than-or-equal-to(output-vault-decrease() min-trade-amount) "Min trade amount."),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we actually need these min/max guards?


gui:
name: Pyth fixed spread limit
name: Auction based cost averaging
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still misnamed here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants