Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save thedavidmeister/d1c21bb3a6d6ebedcd44cdc70ce42597 to your computer and use it in GitHub Desktop.
Save thedavidmeister/d1c21bb3a6d6ebedcd44cdc70ce42597 to your computer and use it in GitHub Desktop.
# 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 BLUE was available in a tranche to buy RED at
# 1 BLUE per RED and this fully cleared, the next tranche might be to buy up to
# 1000 BLUE at 0.9 BLUE per RED, 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 BLUE remaining in a 1000 BLUE tranche at
# 0.9 BLUE per RED then after 24 hours the strategy will be offering 500 BLUE
# at 1 BLUE per RED. 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 BLUE 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:
arbitrum-one:
rpc: https://arbitrum.llamarpc.com
chain-id: 42161
network-id: 42161
currency: ETH
subgraphs:
arbitrum-one: https://api.thegraph.com/subgraphs/name/h20liquidity/arbitrum-0x90caf23e
orderbooks:
arbitrum-one:
address: 0x90CAF23eA7E507BB722647B0674e50D8d6468234
network: arbitrum-one
subgraph: arbitrum-one
deployers:
arbitrum-one:
address: 0x2AeE87D75CD000583DAEC7A28db103B1c0c18b76
network: arbitrum-one
tokens:
arbitrum-one-red:
network: arbitrum-one
address: 0x6d3abb80c3cbae0f60ba274f36137298d8571fbe
arbitrum-one-blue:
network: arbitrum-one
address: 0x667f41fF7D9c06D2dd18511b32041fC6570Dc468
orders:
# vault-id generated with `openssl rand -hex 32`
arbitrum-one-buy:
inputs:
- token: arbitrum-one-red
vault-id: 0x562bd75e19e548420f9f3da43a7d7d67c6344580256b952c9214192c445d6053
outputs:
- token: arbitrum-one-blue
vault-id: 0x562bd75e19e548420f9f3da43a7d7d67c6344580256b952c9214192c445d6053
arbitrum-one-sell:
inputs:
- token: arbitrum-one-blue
vault-id: 0x562bd75e19e548420f9f3da43a7d7d67c6344580256b952c9214192c445d6053
outputs:
- token: arbitrum-one-red
vault-id: 0x562bd75e19e548420f9f3da43a7d7d67c6344580256b952c9214192c445d6053
scenarios:
arb-red-blue-tranches:
network: arbitrum-one
deployer: arbitrum-one
orderbook: arbitrum-one
bindings:
# 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: 0x5Cf7d0a8c61c8dcC6b0ECB281dF1C17264C2A517
orderbook-subparser: 0x23F77e7Bc935503e437166498D7D72f2Ea290E1f
# How far we move through tranche space in a second.
# 1e18 is a whole tranche, so we divide 1e18 by the number of seconds
# per recharge to calculate the per-second rate.
# Examples:
# 172800 seconds in 2 days, 48 hours = 1e18 / 172800 = 5787037037037
# 86400 seconds in 1 day, 24 hours = 1e18 / 86400 = 11574074074074
# 43200 seconds in 12 hours, 12 hours = 1e18 / 43200 = 23148148148148
# 3600 seconds in 1 hour, 1 hour = 1e18 / 3600 = 277777777777777
tranche-space-per-second: 11574074074074
# 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. 9e17) 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: 1e17
# Snap to the nearest tranche to avoid dust issues at the edges, either
# due to rounding in the evm or potentially malicious trades.
# 1e16 is 1%
tranche-space-snap-threshold: 1e16
# 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'
scenarios:
buy:
bindings:
# If we want to denominate the amount in BLUE when we're
# buying RED with it, then the amount is the OUTPUT.
amount-is-output: 1
io-ratio-expr: '''linear-growth'
io-ratio-base: 1e18
io-ratio-growth: 5e16
tranche-size-expr: '''exponential-growth'
tranche-size-base: 1e18
tranche-size-growth: 1e16
scenarios:
initialized:
bindings:
# 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
scenarios:
prod:
bindings:
get-last-tranche: '''get-last-tranche-prod'
set-last-tranche: '''set-last-tranche-prod'
plottables: '''plottables-prod'
tranche-space-shyness: 0
test:
runs: 100
bindings:
get-last-tranche: '''get-last-tranche-test-init'
set-last-tranche: '''set-last-tranche-test'
plottables: '''plottables-test'
test-last-update-time: 0
test-now: 0
test-shy-tranche:
bindings:
get-last-tranche: '''get-last-tranche-prod'
set-last-tranche: '''set-last-tranche-prod'
plottables: '''plottables-prod'
tranche-space-shyness: 9e17
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: 20e18
test-last-update-time: 0
test-now: 0
sell:
bindings:
# If we want to denominate the amount in BLUE when we're
# selling RED for it, then the amount is the INPUT.
amount-is-output: 0
io-ratio-expr: '''linear-growth'
io-ratio-base: 10e18
io-ratio-growth: 5e17
tranche-size-expr: '''exponential-growth'
tranche-size-base: 1e18
tranche-size-growth: 1e16
scenarios:
initialized:
bindings:
# 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
scenarios:
prod:
bindings:
get-last-tranche: '''get-last-tranche-prod'
set-last-tranche: '''set-last-tranche-prod'
plottables: '''plottables-prod'
tranche-space-shyness: 0
test:
runs: 100
bindings:
get-last-tranche: '''get-last-tranche-test-init'
set-last-tranche: '''set-last-tranche-test'
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: 20e18
test-last-update-time: 0
test-now: 0
charts:
buy-initial-deployment:
scenario: arb-red-blue-tranches.buy.initialized.test
metrics:
- label: Amount of BLUE required to buy RED in the first tranche
value: 0.6
unit-prefix: $
- label: Amount of RED purchased in the first tranche
value: 0.5.2
- label: 'Initial io-ratio (i.e. # RED purchased per BLUE spent)'
value: 0.7
- label: Initial effective buy price (e.g. how much you pay for 1 RED in BLUE, visible on dextools)
value: 0.5.3
unit-prefix: $
- label: Starting tranche
value: 0.2.0
plots:
buy-simulation:
scenario: arb-red-blue-tranches.buy.test
plots:
BLUE spent and recharge rate per tranche:
marks:
- type: line
options:
x: 0.0
y: 0.6
Number of RED purchased per tranche:
marks:
- type: line
options:
x: 0.0
y: 0.5.2
'io-ratio (i.e. # of RED purchased per BLUE spent) vs tranche':
marks:
- type: line
options:
x: 0.0
y: 0.7
Effective buy price by tranche (e.g. how much you pay for 1 RED token in BLUE e.g. visible on dextools):
marks:
- type: line
options:
x: 0.0
y: 0.5.3
Starting tranche:
marks:
- type: dot
options:
x: 0.2.0
y: 0.0
sell-initial-deployment:
scenario: arb-red-blue-tranches.sell.initialized.test
metrics:
- label: Amount of RED sold to meet the first tranche of BLUE
value: 0.6
unit-prefix: $
- label: Amount of BLUE received selling the first tranche of RED tokens
value: 0.5.2
- label: 'Initial io-ratio (i.e. # of BLUE purchased per RED spent)'
value: 0.7
- label: Initial effective sell price (Amount of RED to sell 1 BLUE e.g. visible on dextools)
value: 0.7
unit-prefix: $
- label: Starting tranche
value: 0.2.0
plots:
sell-simulation:
scenario: arb-red-blue-tranches.sell.test
plots:
Number of RED sold per tranche:
marks:
- type: line
options:
x: 0.0
y: 0.6
BLUE per tranche:
marks:
- type: line
options:
x: 0.0
y: 0.5.2
'io-ratio (i.e. # of TKN purchased per $ spent) vs tranche':
marks:
- type: line
options:
x: 0.0
y: 0.7
'Effective price (e.g. visible on dextools) vs tranche':
marks:
- type: line
options:
x: 0.0
y: 0.7
Starting tranche:
marks:
- type: dot
options:
x: 0.2.0
y: 0.0
deployments:
arbitrum-one-buy:
scenario: arb-red-blue-tranches.buy.initialized.prod
order: arbitrum-one-buy
arbitrum-one-sell:
scenario: arb-red-blue-tranches.sell.initialized.prod
order: arbitrum-one-sell
---
#tranche-space-per-second !The amount of tranche space that is recharged per second as a normalized 18 decimal fixed point value.
#tranche-space-recharge-delay !The duration in seconds that no recharging occurs after a trade occurs.
#tranche-size-expr !The binding to get the tranche size for the current tranche space.
#tranche-size-base !Base tranche size is the size of the smallest tranche, denominated in output token.
#tranche-size-growth !The exponential growth factor of the size of each tranche, as a decimal 18 fixed point number. E.g. 1e16 is 1% output amount growth per tranche.
#io-ratio-expr !The binding to get the IO ratio for the current tranche space.
#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, as a decimal 18 fixed point number. E.g. 1e16 is 1% io-ratio growth per tranche.
#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.
#min-tranche-space-diff !The minimum tranche space difference that is allowed per trade, as a decimal 18 fixed point number. Prevents dusting the strat to stop it recharging.
#tranche-space-snap-threshold !The threshold in tranche space to snap to the nearest tranche to avoid dust issues at the edges.
#initial-tranche-space !The initial tranche space when the order is first deployed.
#get-last-tranche !The binding to get the last tranche space and update time.
#set-last-tranche !The binding to set the last tranche space and update time.
#test-tranche-space-before !Returned by get-test-last-tranche to allow the tranche space before to be bound for testing.
#test-last-update-time !Returned by get-test-last-tranche to allow the last update time to be bound for testing.
#test-now !Returned by get-test-last-tranche to allow the current time to be bound for testing.
#io-ratio-multiplier !The binding to get the IO ratio multiplier.
#amount-is-output !Whether the amount is an output or input amount. Non-zero means output (i.e. normal orderbook behaviour), zero means input.
#init-key "init"
#tranche-space-key "tranche-space"
#update-time-key "update-time"
#plottables !The binding for additional things we want to plot during testing.
#uniswap-words !The subparser for the Uniswap words
#orderbook-subparser !The subparser for the Orderbook
#plottables-test
amount
io-ratio:,
input-amount: decimal18-mul(amount io-ratio),
effective-price: decimal18-inv(io-ratio);
#plottables-prod
amount
io-ratio:;
#get-last-tranche-prod
is-initialized: get(hash(order-hash() init-key)),
tranche-space-before: if(
is-initialized
get(hash(order-hash() tranche-space-key))
initial-tranche-space
),
last-update-time: if(
is-initialized
get(hash(order-hash() update-time-key))
block-timestamp()
),
now: block-timestamp();
#tranche-space-shyness !The shyness of the liquidity in tranches, as a decimal 18 fixed point number. E.g. 9e17 is 90% shy.
#set-last-tranche-prod
tranche-space now:,
shy-tranche-space: if(
is-zero(decimal18-frac(tranche-space))
decimal18-add(tranche-space tranche-space-shyness)
tranche-space),
:set(hash(order-hash() init-key) 1),
:set(hash(order-hash() tranche-space-key) shy-tranche-space),
:set(hash(order-hash() update-time-key) now);
/* Forward the bindings through as is to the caller. */
#max-test-tranche-space !The maximum tranche space that will appear on the test chart.
#get-last-tranche-test
tranche-space-before: int-mod(test-tranche-space-before max-test-tranche-space),
last-update-time: test-last-update-time,
now: test-now;
#get-last-tranche-test-init
tranche-space-before: initial-tranche-space,
last-update-time: test-last-update-time,
now: test-now;
/* There's nothing to set if we're just rebinding in tests. */
#set-last-tranche-test
tranche-space now:;
#exponential-growth
base rate t:,
_: decimal18-exponential-growth(base rate t);
#linear-growth
base rate t:,
_: decimal18-linear-growth(base rate t);
#constant-growth
base _ _:,
_: base;
#calculate-tranche
tranche-space-before
last-update-time
now: call<'get-last-tranche>(),
recharge-duration: int-saturating-sub(now int-add(last-update-time tranche-space-recharge-delay)),
recharged-tranche-space: decimal18-mul(int-to-decimal18(recharge-duration) tranche-space-per-second),
/* repeat now for easy access by callers */
_: now,
tranche-space-now: decimal18-saturating-sub(tranche-space-before recharged-tranche-space),
tranche-space-available: decimal18-headroom(tranche-space-now),
tranche-total-size: call<'tranche-size-expr>(tranche-size-base tranche-size-growth decimal18-floor(tranche-space-now));
#io-ratio-multiplier-sell
multiplier: uniswap-v3-twap-output-ratio(reference-stable reference-stable-decimals reference-reserve reference-reserve-decimals twap-duration 0 twap-fee);
#io-ratio-multiplier-buy
multiplier: uniswap-v3-twap-output-ratio(reference-reserve reference-reserve-decimals reference-stable reference-stable-decimals twap-duration 0 twap-fee);
#io-ratio-multiplier-identity
multiplier: 1e18;
#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 decimal18-floor(tranche-space-now)),
final-io-ratio: decimal18-mul(tranche-io-ratio call<'io-ratio-multiplier>()),
amount-available: decimal18-mul(tranche-total-size tranche-space-available),
amount: if(amount-is-output amount-available decimal18-div(amount-available final-io-ratio)),
io-ratio: final-io-ratio,
:call<'plottables>(amount io-ratio);
#handle-io
now
tranche-space-before
_
tranche-total-size: call<'calculate-tranche>(),
tranche-amount-diff: if(
amount-is-output
decimal18-scale-18-dynamic(output-token-decimals() output-vault-balance-decrease())
decimal18-scale-18-dynamic(input-token-decimals() input-vault-balance-increase())),
tranche-space-diff: decimal18-div(tranche-amount-diff tranche-total-size),
tranche-space-after: decimal18-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: decimal18-snap-to-unit(tranche-space-snap-threshold tranche-space-after),
:ensure(
greater-than-or-equal-to(tranche-space-after-snapped decimal18-add(tranche-space-before min-tranche-space-diff))
"Minimum trade size not met."
),
:call<'set-last-tranche>(tranche-space-after-snapped now);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment