diff --git a/strategies/wlth-grid-trading.rain b/strategies/wlth-tranche-init.rain similarity index 67% rename from strategies/wlth-grid-trading.rain rename to strategies/wlth-tranche-init.rain index bee4f65..d6b7443 100644 --- a/strategies/wlth-grid-trading.rain +++ b/strategies/wlth-tranche-init.rain @@ -1,20 +1,30 @@ -# Strat: Grid trading -# Grid trading is a strategy that involves placing buy and sell -# orders at pWLTHetermined intervals above and below a set price -# level to profit from market volatility. It creates a grid of orders, -# aiming to capture profits from the regular price fluctuations without -# pWLTHicting market direction. This method is particularly effective in -# sideways or ranging markets, where prices oscillate within a defined -# range, allowing traders to systematically buy low and sell high. -# -# Target Network: Base -# Quote (Input / Incoming): USDC or WLTH -# Base (Output / Outgoing): WLTH or USDC -# Token contract: https://basescan.org/address/0x99b2B1A2aDB02B38222ADcD057783D7e5D1FCC7D -# Token github: NA -# Liquidity protocol: Uniswap V3 -# Liquidity pool address: https://www.dextools.io/app/en/base/pair-explorer/0x1536ee1506e24e5a36be99c73136cd82907a902e?t=1717921711270 -# Liquidity pool fee: 0.3% +# Strat: Recharging tranches +# +# High level idea is that the strategy offers a firm price for batches of tokens. +# Each batch is called a "tranche". +# +# Every time a batch of tokens fully clears a new price further from the previous +# trades is offered for the next tranche. +# +# For example, if 1000 USDC was available in a tranche to buy WLTH at +# 1 USDC per WLTH and this fully cleared, the next tranche might be to buy up to +# 1000 USDC at 0.9 USDC per WLTH, then 0.8, etc. etc. +# +# Tranches slowly recharge over time passively when no trades are happening against +# the current price offer. For example, the strategy might recharge 1 tranche per +# day, so if the last trade left 500 USDC remaining in a 1000 USDC tranche at +# 0.9 USDC per WLTH then after 24 hours the strategy will be offering 500 USDC +# at 1 USDC per WLTH. I.e. 0.5 tranches were recharged at 0.9 ratio and then +# another 0.5 tranches were recharged at 1 ratio. After another 12 hours +# there will be 1000 USDC on offer at 1 ratio, etc. +# +# Almost everything about the strat is bindable and chartable, e.g. +# - The algorithms that determine the price and amount of each tranche +# - The recharge rate and delay before the passive recharge starts to kick in +# - Whether the amounts per tranche are denominated in the input or output token +# - Whether the strategy is buying or selling TKN +# - An optional conversion between the input/output token and some external +# price, e.g. converting WETH in a vault to USD equivalent. networks: base-community: @@ -65,58 +75,98 @@ orders: vault-id: 0x85bdfb90f2cabd6661c1bce26962cb35b8c1f43687641f4a4aa58066be447b52 scenarios: - grid-trading: + wlth-tranches: network: base-community deployer: base-community orderbook: base-community bindings: - # The uniswap words are only requiWLTH if there is a conversion between + # The uniswap words are only required if there is a conversion between # the input/output token and some external price. Typically this is # not the case as the io-ratio is defined in terms of the input/output # token and the io-ratio-multiplier is set to the identity function. uniswap-words: 0xD6B34F97d4A8Cb38D0544dB241CB3f335866f490 - # Orderbook subparser for orderbook words. - orderbook-subparser: 0x8D96ea3EF24D7123882c51CE4325b89bc0d63f9e - # Uniswap v3 factory contract address and corresponding init code hash. # If the factory address and the init code aren't available as a literals # then non-literal address and hash value can be given as bindings. - uniswap-v3-factory: 0x33128a8fC17869897dcE68Ed026d694621f6FDfD + uniswap-v3-factory: '[uniswap-v3-factory]' uniswap-v3-init-code: '[uniswap-v3-init-code]' - # Uniswap v3 fee tier and twap duration - twap-fee: '[uniswap-v3-init-medium]' - twap-duration: 0 + orderbook-subparser: 0x8D96ea3EF24D7123882c51CE4325b89bc0d63f9e + + # How far we move through tranche space in a second. + # 1e18 is a whole tranche, so we divide 1 by the number of seconds + # per recharge to calculate the per-second rate. + # Examples: + # 172800 seconds in 2 days, 48 hours = 1 / 172800 = 0.000005787037037037 + # 86400 seconds in 1 day, 24 hours = 1 / 86400 = 0.000011574074074074 + # 43200 seconds in 12 hours, 12 hours = 1 / 43200 = 0.000023148148148148 + # 3600 seconds in 1 hour, 1 hour = 1 / 3600 = 0.000277777777777777 + tranche-space-per-second: 0.000011574074074074 + + # After any trade happens we pause before recharging. + # Delay is to observe market feedback to the previous trade, e.g. to + # potentially offer the next tranche at a different price for some time + # before recharging back to the previous price. + # Too long and people could grief to stop recharging. + # Too quick and it will be difficult to move between tranches. + # The default here is 5 minutes (units are seconds) and probably never + # needs to be changed. + tranche-space-recharge-delay: 300 + + # When a tranche is completely cleared, the next tranche MAY be jumped + # into partially. For example, if tranches are 90% shy (i.e. 0.9) then + # if a tranche is cleared completely then the next tranche will be + # started at 10% of its maximum size. This means that the capital + # requirements for the strategy to reprice itself as the market moves + # are reduced. + # This MUST be set to a value less than 1e18, else it will entirely + # skip tranches. + # Shyness MAY be set to 0, in which case every tranche will be fully + # available as it is entered. + # tranche-space-shyness: 0 + + # Minimum trade size, if you put in a trade for less than x% of a + # tranche it wont clear. + # Mitigates people pinging strat for dust orders to stop recharging. + min-tranche-space-diff: 0.1 + + # Snap to the nearest tranche to avoid dust issues at the edges, either + # due to rounding in the evm or potentially malicious trades. + # 0.01 is 1% + tranche-space-snap-threshold: 0.01 + + # This is only relevant if the tranche size/ratio is denominated in + # some token other than the input/output tokens. For example, if the + # TKN was being traded for WETH but the tranche size was denominated in + # USD then the reference-stable would be USD and the reference-reserve + # would be WETH, and the identity multiplier needs to be swapped out + # for e.g. a TWAP USDT based multiplier. + # Typically this is NOT needed as the tranche size and ratio ARE + # denominated in the input/output tokens. + io-ratio-multiplier: '''io-ratio-multiplier-identity' - # Reference token addresses - reference-token: 0x99b2B1A2aDB02B38222ADcD057783D7e5D1FCC7D - reference-stable: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 - scenarios: buy: bindings: - # WLTH/USDC base ratio and increment. - io-ratio-base: 40 - io-ratio-growth: 2.5 - - # Base amount of USDC tokens to offer and increment. - tranche-size-base: 5 - tranche-size-growth: 1 - + # If we want to denominate the amount in USDC when we're + # buying WLTH with it, then the amount is the OUTPUT. + amount-is-output: 1 + io-ratio-expr: '''linear-growth' + io-ratio-base: 15 + io-ratio-growth: 0.5 + tranche-size-expr: '''linear-growth' + tranche-size-base: 5 + tranche-size-growth: 0.1 scenarios: - grid: + initialized: bindings: - tranche-space-per-second: 0 - tranche-space-recharge-delay: 300 - min-tranche-space-diff: 0.1 - tranche-space-snap-threshold: 0.01 - io-ratio-multiplier: '''io-ratio-multiplier-identity' - amount-is-output: 1 - tranche-space-shyness: 0 + # This needs to be set upon going live. + # Generate a chart and compare to current market prices, if + # the market is within the chart then set this to the closest + # tranche that won't immediately dump into the market. + # If the market is outside the chart then set this to 0. initial-tranche-space: 0 - io-ratio-expr: '''linear-growth' - tranche-size-expr: '''linear-growth' post-action: '''post-action-buy' scenarios: prod: @@ -125,7 +175,7 @@ scenarios: set-last-tranche: '''set-last-tranche-prod' plottables: '''plottables-prod' tranche-space-shyness: 0 - init: + test: runs: 100 bindings: get-last-tranche: '''get-last-tranche-test-init' @@ -133,39 +183,41 @@ scenarios: plottables: '''plottables-test' test-last-update-time: 0 test-now: 0 - test: - runs: 10000 + test-shy-tranche: bindings: - get-last-tranche: '''get-last-tranche-test' - set-last-tranche: '''set-last-tranche-test' - plottables: '''plottables-test' - max-test-tranche-space: 20 - test-last-update-time: 0 - test-now: 0 + get-last-tranche: '''get-last-tranche-prod' + set-last-tranche: '''set-last-tranche-prod' + plottables: '''plottables-prod' + tranche-space-shyness: 0.9 + test: + runs: 10000 + bindings: + get-last-tranche: '''get-last-tranche-test' + set-last-tranche: '''set-last-tranche-test' + plottables: '''plottables-test' + max-test-tranche-space: 20 + test-last-update-time: 0 + test-now: 0 sell: bindings: - # USDC/WLTH base ratio and increment. - io-ratio-base: 0.020 - io-ratio-growth: 0.002 - - # Base amount of USDC tokens to received and increment. - tranche-size-base: 5 - tranche-size-growth: 1 - + # If we want to denominate the amount in USDC when we're + # selling WLTH for it, then the amount is the INPUT. + amount-is-output: 0 + io-ratio-expr: '''linear-growth' + io-ratio-base: 0.015 + io-ratio-growth: 0.001 + tranche-size-expr: '''linear-growth' + tranche-size-base: 5 + tranche-size-growth: 0.1 scenarios: - grid: + initialized: bindings: - tranche-space-per-second: 0 - tranche-space-recharge-delay: 300 - min-tranche-space-diff: 0.1 - tranche-space-snap-threshold: 0.01 - io-ratio-multiplier: '''io-ratio-multiplier-identity' - amount-is-output: 0 - tranche-space-shyness: 0 + # This needs to be set upon going live. + # Generate a chart and compare to current market prices, if + # the market is within the chart then set this to the closest + # tranche that won't immediately dump into the market. + # If the market is outside the chart then set this to 0. initial-tranche-space: 0 - io-ratio-expr: '''linear-growth' - tranche-size-expr: '''linear-growth' - post-action: '''post-action-sell' scenarios: prod: bindings: @@ -173,7 +225,7 @@ scenarios: set-last-tranche: '''set-last-tranche-prod' plottables: '''plottables-prod' tranche-space-shyness: 0 - init: + test: runs: 100 bindings: get-last-tranche: '''get-last-tranche-test-init' @@ -181,19 +233,20 @@ scenarios: plottables: '''plottables-test' test-last-update-time: 0 test-now: 0 - test: - runs: 10000 - bindings: - get-last-tranche: '''get-last-tranche-test' - set-last-tranche: '''set-last-tranche-test' - plottables: '''plottables-test' - max-test-tranche-space: 20 - test-last-update-time: 0 - test-now: 0 - + test: + runs: 10000 + bindings: + get-last-tranche: '''get-last-tranche-test' + set-last-tranche: '''set-last-tranche-test' + plottables: '''plottables-test' + max-test-tranche-space: 20 + test-last-update-time: 0 + test-now: 0 + + charts: - base-wlth-buy-deployment: - scenario: grid-trading.buy.grid.init + buy-initial-deployment: + scenario: wlth-tranches.buy.initialized.test metrics: - label: Initial USDC sold value: 0.6 @@ -215,7 +268,7 @@ charts: plots: buy-simulation: - scenario: grid-trading.buy.grid.test + scenario: wlth-tranches.buy.test plots: USDT sold per tranche: x: @@ -260,8 +313,8 @@ charts: x: 0.0 y: 0.5.3 - base-wlth-sell-deployment: - scenario: grid-trading.sell.grid.init + sell-initial-deployment: + scenario: wlth-tranches.sell.initialized.test metrics: - label: Initial WLTH sold value: 0.6 @@ -283,7 +336,7 @@ charts: plots: sell-simulation: - scenario: grid-trading.sell.grid.test + scenario: wlth-tranches.sell.test plots: WLTH sold per tranche: x: @@ -330,10 +383,10 @@ charts: deployments: base-wlth-buy: - scenario: grid-trading.buy.grid.prod + scenario: wlth-tranches.buy.initialized.prod order: base-wlth-buy base-wlth-sell: - scenario: grid-trading.sell.grid.prod + scenario: wlth-tranches.sell.initialized.prod order: base-wlth-sell --- #tranche-space-per-second !The amount of tranche space that is recharged per second. @@ -347,9 +400,10 @@ deployments: #io-ratio-base !The base IO ratio, as a decimal 18 fixed point number. This is the IO ratio at tranche space 0 and grows according to the growth factor per tranche. #io-ratio-growth !The exponential growth factor of the IO ratio. E.g. 0.01 is 1% io-ratio growth per tranche. -#reference-token !Strategy token for which the budget is written. #reference-stable !The stable token that is used as a reference for the TWAP to offer dollar equivalent conversions. +#reference-stable-decimals !The number of decimals of the reference stable token. #reference-reserve !The token that will be used to compare against the reference stable token to calculate the TWAP for dollar equivalent conversions. +#reference-reserve-decimals !The number of decimals of the reserve token. #twap-duration !The duration in seconds of the TWAP window for dollar equivalence conversions. #twap-fee !The uniswap fee tier to use for the TWAP. @@ -464,6 +518,37 @@ deployments: #io-ratio-multiplier-identity multiplier: 1; +#calculate-io + using-words-from uniswap-words orderbook-subparser + tranche-space-now + tranche-space-available + tranche-total-size: call<'calculate-tranche>(), + tranche-io-ratio: call<'io-ratio-expr>(io-ratio-base io-ratio-growth floor(tranche-space-now)), + final-io-ratio: mul(tranche-io-ratio call<'io-ratio-multiplier>()), + amount-available: mul(tranche-total-size tranche-space-available), + amount: if(amount-is-output amount-available div(amount-available final-io-ratio)), + io-ratio: final-io-ratio, + :call<'plottables>(amount io-ratio); + +#handle-io + current-time + tranche-space-before + _ + tranche-total-size: call<'calculate-tranche>(), + tranche-amount-diff: if( + amount-is-output + scale-18-dynamic(output-token-decimals() uint256-output-vault-decrease()) + scale-18-dynamic(input-token-decimals() uint256-input-vault-increase())), + tranche-space-diff: div(tranche-amount-diff tranche-total-size), + tranche-space-after: add(tranche-space-before tranche-space-diff), + /* Snap tranche space to the nearest tranche to avoid dust issues at the edges */ + tranche-space-after-snapped: snap-to-unit(tranche-space-snap-threshold tranche-space-after), + :ensure( + greater-than-or-equal-to(tranche-space-after-snapped add(tranche-space-before min-tranche-space-diff)) + "Minimum trade size not met." + ), + :call<'set-last-tranche>(tranche-space-after-snapped current-time); + #post-action-buy _: uniswap-v3-twap-output-ratio( reference-stable reference-token @@ -481,7 +566,7 @@ deployments: #post-action-constant _: io-ratio-current; -#post-action-source +#post-add-order current-ratio: call<'post-action>(), tranche-0-ratio: call<'io-ratio-expr>(io-ratio-base io-ratio-growth 0), tranche-1-ratio: call<'io-ratio-expr>(io-ratio-base io-ratio-growth 1), @@ -500,35 +585,4 @@ deployments: "default" initial-tranche-space ), :set(hash(order-hash() init-key) 1), - :set(hash(order-hash() tranche-space-key) init-tranche); - -#calculate-io - using-words-from uniswap-words orderbook-subparser - tranche-space-now - tranche-space-available - tranche-total-size: call<'calculate-tranche>(), - tranche-io-ratio: call<'io-ratio-expr>(io-ratio-base io-ratio-growth floor(tranche-space-now)), - final-io-ratio: mul(tranche-io-ratio call<'io-ratio-multiplier>()), - amount-available: mul(tranche-total-size tranche-space-available), - amount: if(amount-is-output amount-available div(amount-available final-io-ratio)), - io-ratio: final-io-ratio, - :call<'plottables>(amount io-ratio); - -#handle-io - current-time - tranche-space-before - _ - tranche-total-size: call<'calculate-tranche>(), - tranche-amount-diff: if( - amount-is-output - scale-18-dynamic(output-token-decimals() uint256-output-vault-decrease()) - scale-18-dynamic(input-token-decimals() uint256-input-vault-increase())), - tranche-space-diff: div(tranche-amount-diff tranche-total-size), - tranche-space-after: add(tranche-space-before tranche-space-diff), - /* Snap tranche space to the nearest tranche to avoid dust issues at the edges */ - tranche-space-after-snapped: snap-to-unit(tranche-space-snap-threshold tranche-space-after), - :ensure( - greater-than-or-equal-to(tranche-space-after-snapped add(tranche-space-before min-tranche-space-diff)) - "Minimum trade size not met." - ), - :call<'set-last-tranche>(tranche-space-after-snapped current-time); + :set(hash(order-hash() tranche-space-key) init-tranche); \ No newline at end of file diff --git a/test/WlthTrancheInitTest.t.sol b/test/WlthTrancheInitTest.t.sol new file mode 100644 index 0000000..bdd8d52 --- /dev/null +++ b/test/WlthTrancheInitTest.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; +import {console2, Test} from "forge-std/Test.sol"; + +import { + IOrderBookV3, + IO +} from "rain.orderbook.interface/interface/IOrderBookV3.sol"; +import { + IOrderBookV4, + OrderV3, + OrderConfigV3, + TakeOrderConfigV3, + TakeOrdersConfigV3, + ActionV1 +} from "rain.orderbook.interface/interface/unstable/IOrderBookV4.sol"; + +import {IParserV2} from "rain.interpreter.interface/interface/unstable/IParserV2.sol"; +import {IOrderBookV4ArbOrderTaker} from "rain.orderbook.interface/interface/unstable/IOrderBookV4ArbOrderTaker.sol"; + +// import {IOrderBookV3ArbOrderTaker} from "rain.orderbook.interface/interface/IOrderBookV3ArbOrderTaker.sol"; +// import {IParserV1} from "rain.interpreter.interface/interface/IParserV1.sol"; +import {IExpressionDeployerV3} from "rain.interpreter.interface/interface/IExpressionDeployerV3.sol"; +// import {IInterpreterV2} from "rain.interpreter.interface/interface/IInterpreterV2.sol"; +import {IInterpreterV3} from "rain.interpreter.interface/interface/unstable/IInterpreterV3.sol"; +import {IInterpreterStoreV2} from "rain.interpreter.interface/interface/IInterpreterStoreV2.sol"; +import {StrategyTests, IRouteProcessor, LibStrategyDeployment, LibComposeOrders,IInterpreterV3} from "h20.test-std/StrategyTests.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import {SafeERC20, IERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "h20.test-std/lib/LibProcessStream.sol"; + +uint256 constant VAULT_ID = uint256(keccak256("vault")); + + +/// @dev https://basescan.org/address/0x99b2B1A2aDB02B38222ADcD057783D7e5D1FCC7D +IERC20 constant BASE_WLTH= IERC20(0x99b2B1A2aDB02B38222ADcD057783D7e5D1FCC7D); + +/// @dev https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 +IERC20 constant BASE_USDC = IERC20(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913); + +function baseWlthIo() pure returns (IO memory) { + return IO(address(BASE_WLTH), 18, VAULT_ID); +} + +function baseUsdcIo() pure returns (IO memory) { + return IO(address(BASE_USDC), 6, VAULT_ID); +} + +contract StopLimitTest is StrategyTests { + + using SafeERC20 for IERC20; + using Strings for address; + + uint256 constant FORK_BLOCK_NUMBER = 17914079; + + function selectFork() internal { + uint256 fork = vm.createFork(vm.envString("RPC_URL_BASE")); + vm.selectFork(fork); + vm.rollFork(FORK_BLOCK_NUMBER); + } + + function setUp() public { + selectFork(); + + iParser = IParserV2(0x56394785a22b3BE25470a0e03eD9E0a939C47b9b); + iStore = IInterpreterStoreV2(0x6E4b01603edBDa617002A077420E98C86595748E); + iInterpreter = IInterpreterV3(0x379b966DC6B117dD47b5Fc5308534256a4Ab1BCC); + iExpressionDeployer = IExpressionDeployerV3(0x56394785a22b3BE25470a0e03eD9E0a939C47b9b); + iOrderBook = IOrderBookV4(0xA2f56F8F74B7d04d61f281BE6576b6155581dcBA); + iArbInstance = IOrderBookV4ArbOrderTaker(0xF97A86C2Cb3e42f89AC5f5AA020E5c3505015a88); + iRouteProcessor = IRouteProcessor(address(0x0389879e0156033202C44BF784ac18fC02edeE4f)); + EXTERNAL_EOA = address(0x654FEf5Fb8A1C91ad47Ba192F7AA81dd3C821427); + APPROVED_EOA = address(0x669845c29D9B1A64FFF66a55aA13EB4adB889a88); + ORDER_OWNER = address(0x5e01e44aE1969e16B9160d903B6F2aa991a37B21); + } + + function testWlthTrancheInitBuy() public { + + IO[] memory inputVaults = new IO[](1); + inputVaults[0] = baseWlthIo(); + + IO[] memory outputVaults = new IO[](1); + outputVaults[0] = baseUsdcIo(); + + LibStrategyDeployment.StrategyDeployment memory strategy = LibStrategyDeployment.StrategyDeployment( + getEncodedSellWlthRoute(), + getEncodedBuyWlthRoute(), + 0, + 0, + 1000000e18, + 10000e6, + 0, + 0, + "strategies/wlth-tranche-init.rain", + "wlth-tranches.buy.initialized.prod", + "./lib/h20.test-std/lib/rain.orderbook", + "./lib/h20.test-std/lib/rain.orderbook/Cargo.toml", + inputVaults, + outputVaults + ); + + OrderV3 memory order = addOrderDepositOutputTokens(strategy); + + } + + function getEncodedBuyWlthRoute() internal pure returns (bytes memory) { + bytes memory BUY_WLTH_ROUTE = + hex"02833589fCD6eDb6E08f4c7C32D4f71b54bdA0291301ffff011536EE1506e24e5A36Be99C73136cD82907A902E01F97A86C2Cb3e42f89AC5f5AA020E5c3505015a88"; + + return abi.encode(BUY_WLTH_ROUTE); + } + + function getEncodedSellWlthRoute() internal pure returns (bytes memory) { + bytes memory SELL_WLTH_ROUTE = + hex"0299b2B1A2aDB02B38222ADcD057783D7e5D1FCC7D01ffff011536EE1506e24e5A36Be99C73136cD82907A902E00F97A86C2Cb3e42f89AC5f5AA020E5c3505015a88"; + + return abi.encode(SELL_WLTH_ROUTE); + } + +} \ No newline at end of file