diff --git a/src/backtests/univ3/lpSim/grafana.json b/src/backtests/univ3/lpSim/grafana.json new file mode 100644 index 0000000..204317b --- /dev/null +++ b/src/backtests/univ3/lpSim/grafana.json @@ -0,0 +1,1274 @@ +{ + "__inputs": [ + { + "name": "DS_INFLUXDB2", + "label": "InfluxDB2", + "description": "", + "type": "datasource", + "pluginId": "influxdb", + "pluginName": "InfluxDB" + } + ], + "__elements": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "8.4.4" + }, + { + "type": "datasource", + "id": "influxdb", + "name": "InfluxDB", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 2, + "id": null, + "iteration": 1695357492465, + "links": [], + "liveNow": false, + "panels": [ + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 14, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 9, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "rebalanceCount" + } + ] + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "rangeSpread" + ], + "type": "tag" + }, + { + "params": [ + "debtRatioRange" + ], + "type": "tag" + }, + { + "params": [ + "collatRatio" + ], + "type": "tag" + } + ], + "measurement": "hedged_camelot3_summary", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "table", + "select": [ + [ + { + "params": [ + "rebalanceCount" + ], + "type": "field" + } + ], + [ + { + "params": [ + "apr" + ], + "type": "field" + } + ], + [ + { + "params": [ + "aum" + ], + "type": "field" + } + ] + ], + "tags": [] + } + ], + "title": "Panel Title", + "type": "table" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_name", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "name" + ], + "type": "tag" + }, + { + "params": [ + "previous" + ], + "type": "fill" + } + ], + "measurement": "hedged_camelot3_strategy", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "aum" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "name", + "operator": "=~", + "value": "/^$strategy$/" + }, + { + "condition": "AND", + "key": "rangeSpread", + "operator": "=~", + "value": "/^$rangeSpread$/" + }, + { + "condition": "AND", + "key": "debtRatioRange", + "operator": "=~", + "value": "/^$debtRatioRange$/" + }, + { + "condition": "AND", + "key": "collatRatio", + "operator": "=~", + "value": "/^$collatRatio$/" + } + ] + } + ], + "title": "Aum", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_name", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "name" + ], + "type": "tag" + }, + { + "params": [ + "previous" + ], + "type": "fill" + } + ], + "measurement": "hedged_camelot3_strategy", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "gasCosts" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "name", + "operator": "=~", + "value": "/^$strategy$/" + }, + { + "condition": "AND", + "key": "rangeSpread", + "operator": "=~", + "value": "/^$rangeSpread$/" + }, + { + "condition": "AND", + "key": "debtRatioRange", + "operator": "=~", + "value": "/^$debtRatioRange$/" + }, + { + "condition": "AND", + "key": "collatRatio", + "operator": "=~", + "value": "/^$collatRatio$/" + } + ] + } + ], + "title": "Gas Cost", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_name", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "name" + ], + "type": "tag" + }, + { + "params": [ + "previous" + ], + "type": "fill" + } + ], + "measurement": "hedged_camelot3_strategy", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "minRange" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "name", + "operator": "=~", + "value": "/^$strategy$/" + }, + { + "condition": "AND", + "key": "rangeSpread", + "operator": "=~", + "value": "/^$rangeSpread$/" + }, + { + "condition": "AND", + "key": "debtRatioRange", + "operator": "=~", + "value": "/^$debtRatioRange$/" + }, + { + "condition": "AND", + "key": "collatRatio", + "operator": "=~", + "value": "/^$collatRatio$/" + } + ] + } + ], + "title": "Ranges", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_name", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "name" + ], + "type": "tag" + }, + { + "params": [ + "previous" + ], + "type": "fill" + } + ], + "measurement": "hedged_camelot3_strategy", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "feeUSD" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + }, + { + "params": [], + "type": "cumulative_sum" + } + ] + ], + "tags": [ + { + "key": "name", + "operator": "=~", + "value": "/^$strategy$/" + }, + { + "condition": "AND", + "key": "rangeSpread", + "operator": "=~", + "value": "/^$rangeSpread$/" + }, + { + "condition": "AND", + "key": "debtRatioRange", + "operator": "=~", + "value": "/^$debtRatioRange$/" + }, + { + "condition": "AND", + "key": "collatRatio", + "operator": "=~", + "value": "/^$collatRatio$/" + } + ] + } + ], + "title": "Fees", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "previous" + ], + "type": "fill" + } + ], + "measurement": "hedged_camelot3_strategy", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "price0" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "name", + "operator": "=~", + "value": "/^$strategy$/" + } + ] + } + ], + "title": "ETH/USD", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_name", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "name" + ], + "type": "tag" + }, + { + "params": [ + "previous" + ], + "type": "fill" + } + ], + "measurement": "hedged_camelot3_strategy", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "debtRatio" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "name", + "operator": "=~", + "value": "/^$strategy$/" + }, + { + "condition": "AND", + "key": "rangeSpread", + "operator": "=~", + "value": "/^$rangeSpread$/" + }, + { + "condition": "AND", + "key": "debtRatioRange", + "operator": "=~", + "value": "/^$debtRatioRange$/" + }, + { + "condition": "AND", + "key": "collatRatio", + "operator": "=~", + "value": "/^$collatRatio$/" + } + ] + } + ], + "title": "Debt Ratio", + "type": "timeseries" + }, + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 38 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "Price: $tag_name", + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "name" + ], + "type": "tag" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "hedged_camelot3_strategy", + "orderByTime": "ASC", + "policy": "autogen", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "price1" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "name", + "operator": "=~", + "value": "/^$strategy$/" + }, + { + "condition": "AND", + "key": "rangeSpread", + "operator": "=~", + "value": "/^$rangeSpread$/" + }, + { + "condition": "AND", + "key": "debtRatioRange", + "operator": "=~", + "value": "/^$debtRatioRange$/" + }, + { + "condition": "AND", + "key": "collatRatio", + "operator": "=~", + "value": "/^$collatRatio$/" + } + ] + } + ], + "title": "USDC/USD", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 35, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "influxdb", + "uid": "${DS_INFLUXDB2}" + }, + "definition": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"name\"", + "hide": 0, + "includeAll": true, + "label": "Strategy", + "multi": true, + "name": "strategy", + "options": [], + "query": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"name\"", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "definition": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"rangeSpread\"", + "hide": 0, + "includeAll": true, + "label": "LP Range", + "multi": true, + "name": "rangeSpread", + "options": [], + "query": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"rangeSpread\"", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query", + "datasource": "${DS_INFLUXDB2}" + }, + { + "current": {}, + "definition": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"debtRatioRange\"", + "hide": 0, + "includeAll": true, + "label": "Debt Ratio", + "multi": true, + "name": "debtRatioRange", + "options": [], + "query": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"debtRatioRange\"", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query", + "datasource": "${DS_INFLUXDB2}" + }, + { + "current": {}, + "definition": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"collatRatio\"", + "hide": 0, + "includeAll": true, + "label": "Collat Ratio", + "multi": true, + "name": "collatRatio", + "options": [], + "query": "SHOW TAG VALUES FROM \"hedged_camelot3_strategy\" WITH KEY = \"collatRatio\"", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query", + "datasource": "${DS_INFLUXDB2}" + } + ] + }, + "time": { + "from": "now-6M", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Camelot V3", + "uid": "f334b0e8-efd9-40a6-87e5-15facc56d953", + "version": 6, + "weekStart": "" +} \ No newline at end of file diff --git a/src/backtests/univ3/lpSim/index.ts b/src/backtests/univ3/lpSim/index.ts new file mode 100644 index 0000000..0881380 --- /dev/null +++ b/src/backtests/univ3/lpSim/index.ts @@ -0,0 +1,31 @@ +import { Backtest } from '../../../lib/backtest.js'; +import { DataSourceInfo } from '../../../lib/datasource/types.js'; +import { HedgedUniswapStrategyRunner } from './strategyRunner.js'; + +const main = async () => { + const USDCWETH = '0xb1026b8e7276e7ac75410f1fcbbe21796e8f7526'; + const sources: DataSourceInfo[] = [ + { + chain: 'arbitrum', + protocol: 'camelot-dex', + resoution: '1h', + config: { + pairs: [USDCWETH], + }, + }, + ]; + const start = new Date('2023-06-20'); + const end = new Date(); + const bt = await Backtest.create(start, end, sources); + + // Configure Strategy + const strategy = new HedgedUniswapStrategyRunner(end); + bt.onBefore(strategy.before.bind(strategy)); + bt.onData(strategy.onData.bind(strategy)); + bt.onAfter(strategy.after.bind(strategy)); + + // Run + await bt.run(); +}; + +main(); diff --git a/src/backtests/univ3/lpSim/models.ts b/src/backtests/univ3/lpSim/models.ts new file mode 100644 index 0000000..4682751 --- /dev/null +++ b/src/backtests/univ3/lpSim/models.ts @@ -0,0 +1,12 @@ +import { ILogAny } from '../../../lib/utils/influx2x.js'; +import { InfluxBatcher } from '../../../lib/utils/influxBatcher.js'; + +export const Log = new InfluxBatcher( + 'hedged_camelot3_strategy', +); +export const Rebalance = new InfluxBatcher( + 'hedged_camelot3_rebalance', +); +export const Summary = new InfluxBatcher( + 'hedged_camelot3_summary', +); diff --git a/src/backtests/univ3/lpSim/stats.ts b/src/backtests/univ3/lpSim/stats.ts new file mode 100644 index 0000000..4060a02 --- /dev/null +++ b/src/backtests/univ3/lpSim/stats.ts @@ -0,0 +1,23 @@ +export class Stats { + // Calculate the average of all the numbers + static mean(values: number[]) { + const mean = values.reduce((sum, current) => sum + current) / values.length; + return mean; + } + + // Calculate variance + static variance(values: number[]) { + const average = Stats.mean(values); + const squareDiffs = values.map((value) => { + const diff = value - average; + return diff * diff; + }); + const variance = Stats.mean(squareDiffs); + return variance; + } + + // Calculate stand deviation + static stddev(variance: number) { + return Math.sqrt(variance); + } +} diff --git a/src/backtests/univ3/lpSim/strategyRunner.ts b/src/backtests/univ3/lpSim/strategyRunner.ts new file mode 100644 index 0000000..5e075f7 --- /dev/null +++ b/src/backtests/univ3/lpSim/strategyRunner.ts @@ -0,0 +1,108 @@ +import { UniV3PositionManager } from '../../../lib/protocols/UNIV3PositionManager.js'; +import { Uni3Snaphot } from '../../../lib/datasource/univ3Dex.js'; +import { stringify } from 'csv-stringify/sync'; +import fs from 'fs/promises'; +import { UniV3Hodl } from './univ3Hodl.js'; +import { Log, Rebalance, Summary } from './models.js'; +import { range } from '../../../lib/utils/utility.js'; +import { permutations } from '../../../lib/utils/permutations.js'; + +const SECONDS_IN_DAY = 60 * 60 * 24; +const MILLISECONDS_IN_DAY = SECONDS_IN_DAY * 1000; +const PERIOD = 8 * 7; // 8 weeks + +const POOLS = ['Camelotv3 WETH/USDC 0%']; + +export class HedgedUniswapStrategyRunner { + private uni = new UniV3PositionManager(); + private lastData!: Uni3Snaphot; + private lastStart?: number; + private strategies: UniV3Hodl[] = []; + + constructor(private end: Date) {} + + public async startNewStartForPool(pool: string) { + const rangeSpread = range(0.05, 0.25, 5); + const fixedSlippage = range(0.004, 0.01, 1); + + const variations = permutations([rangeSpread, fixedSlippage]); + + variations.forEach((e) => { + this.strategies.push( + new UniV3Hodl({ + name: `#${e[0]}: Camelotv3 WETH/USDC ${e[0] * 100}% | slippage : ${ + e[1] * 100 + }%`, + poolSymbol: pool, + initial: 10_000, + rangeSpread: e[0], + priceToken: 0, + fixedSlippage: e[1], + period: PERIOD, + }), + ); + }); + } + + public async before() { + await Log.dropMeasurement(); + } + + public async after() { + console.log( + 'end date:', + new Date(this.lastData.timestamp * 1000).toISOString(), + ); + await Log.exec(true); + await Rebalance.exec(true); + const summary = this.strategies.map((s) => s.summary); + console.log(summary); + const csv = stringify(summary, { header: true }); + fs.writeFile('./camelotv3_hedged.csv', csv); + + const series = this.strategies.map((s) => s.series).flat(); + const seriesCsv = stringify(series, { header: true }); + fs.writeFile('./camelotv3_hedged_series.csv', seriesCsv); + + await Summary.writePoints( + summary.map((s, i) => { + return { + tags: this.strategies[i].tags, + fields: s, + timestamp: new Date(this.lastData.timestamp * 1000), + }; + }), + ); + await Summary.exec(); + } + + public async onData(snapshot: Uni3Snaphot) { + if (!snapshot.data.univ3) return console.log('missing data', snapshot); + this.lastData = snapshot; + + this.uni.processPoolData(snapshot); + + const daysElapsed = Math.floor( + (snapshot.timestamp - this.lastStart!) / SECONDS_IN_DAY, + ); + const daysRemaining = + (this.end.getTime() - snapshot.timestamp * 1000) / MILLISECONDS_IN_DAY; + + // Create new strategies every 3 days + if ( + (this.strategies.length === 0 || daysElapsed > 3) && + daysRemaining > PERIOD + ) { + for (const pool of POOLS) { + console.log('start strat', pool, new Date(snapshot.timestamp * 1000)); + this.startNewStartForPool(pool); + } + this.lastStart = snapshot.timestamp; + } + + // Process the strategy + for (const strat of this.strategies) { + await strat.process(this.uni, snapshot); + } + } +} diff --git a/src/backtests/univ3/lpSim/univ3Hodl.ts b/src/backtests/univ3/lpSim/univ3Hodl.ts new file mode 100644 index 0000000..26b2cda --- /dev/null +++ b/src/backtests/univ3/lpSim/univ3Hodl.ts @@ -0,0 +1,239 @@ +import { Uni3Snaphot } from '../../../lib/datasource/univ3Dex.js'; +import { + UniV3Position, + UniV3PositionManager, +} from '../../../lib/protocols/UNIV3PositionManager.js'; +import { Log, Summary } from './models.js'; +import { Stats } from './stats.js'; + +const SECONDS_IN_DAY = 60 * 60 * 24; +const HARVEST_PERIOD = 60 * 60 * 24; // 1 day +const TWO_WEEKS = 60 * 60 * 24 * 14; +const ONE_YEAR = 60 * 60 * 24 * 365; + +type StrategyConfig = { + name: string; + poolSymbol: string; + initial: number; + rangeSpread: number; + priceToken: number; + fixedSlippage: number; + period: number; +}; + +export class UniV3Hodl { + public pos!: UniV3Position; + public expired = false; + public start!: number; + public highest: number; + public lastHarvest: number = 0; + public claimed = 0; + public idle: number = 0; // idle assets + public maxDrawdown = 0; + public series: any[] = []; + public tags: any = {}; + public summary: any; + + public rebalanceCount = 0; + public startPrice = 0; + public gasCosts = 0; + public tokenIndex: number = 0; + public config: StrategyConfig; + + constructor(config: StrategyConfig) { + this.config = config; + this.highest = config.initial; + this.tokenIndex = config.priceToken == 0 ? 1 : 0; + } + + public async process(uni: UniV3PositionManager, data: Uni3Snaphot) { + if (this.expired) return; + + if (!this.pool(data)) { + console.log('missing data for ' + this.config.name); + return; + } + + // open the first position + if (!this.pos) { + console.log('openning the first position'); + this.start = data.timestamp; + const pool = this.pool(data); + this.pos = uni.open( + this.config.initial / 2, + pool.close * (1 - this.config.rangeSpread), + pool.close / (1 - this.config.rangeSpread), + this.config.priceToken, + this.config.poolSymbol, + ); + this.pos.valueUsd = this.config.initial; //hack + } + + if (data.timestamp - this.lastHarvest >= HARVEST_PERIOD) { + await this.harvest(data); + } + // always log data + await this.log(uni, data); + + const daysElapsed = (data.timestamp - this.start) / SECONDS_IN_DAY; + if (daysElapsed > this.config.period) { + this.expired = true; + console.log('strategy expired'); + await this.end(uni, data); + } + } + + public pool(data: Uni3Snaphot) { + return data.data.univ3.find((p) => p.symbol === this.config.poolSymbol)!; + } + + public poolIndex(data: Uni3Snaphot) { + return data.data.velodrome.findIndex( + (p) => p.symbol === this.config.poolSymbol, + )!; + } + + private estTotalAssets(data: Uni3Snaphot) { + const result = this.idle + this.pos.valueUsd; + return result; + } + + private async harvest(data: Uni3Snaphot) { + this.claimed = this.pos.claimed; + this.lastHarvest = data.timestamp; + } + + private apy(data: Uni3Snaphot) { + const elapsed = data.timestamp - this.start; + if (elapsed < TWO_WEEKS) return 0; + const totalAssets = this.estTotalAssets(data); + const apy = (totalAssets / this.config.initial) ** (ONE_YEAR / elapsed) - 1; + return apy; + } + + private apr(data: Uni3Snaphot) { + const elapsed = data.timestamp - this.start; + return ( + (this.estTotalAssets(data) / this.config.initial - 1) / + (elapsed / ONE_YEAR) + ); + } + + public async log(mgr: UniV3PositionManager, data: Uni3Snaphot) { + const tokens: any = {}; + const prices: any = {}; + const pool = this.pool(data); + pool.tokens.forEach((token, i) => { + tokens[`token${i}`] = token.symbol; + prices[`price${i}`] = token.price; + }); + const totalAssets = this.estTotalAssets(data); + if (totalAssets === 0) { + console.log('total assets === 0???'); + return; + } + this.highest = this.highest < totalAssets ? totalAssets : this.highest; + const drawdown = -(this.highest - totalAssets) / this.highest; + const { tokens: _t, prices: _p, reserves: _r, ...poolSnap } = pool as any; + this.maxDrawdown = Math.max(this.maxDrawdown, -drawdown); + const profit = totalAssets - this.config.initial; + this.tags = { + name: this.config.name, + pool: this.config.poolSymbol, + ...tokens, + rangeSpread: (this.config.rangeSpread * 100).toFixed(2), + }; + const apy = this.apy(data); + const log = { + tags: this.tags, + fields: { + strategy: this.config.name, + ...this.pos.snapshot, + ...prices, + rewards: this.claimed, + drawdown, + //...poolSnap, + highest: this.highest, + apy, // TODO: get APY + aum: totalAssets, + minRange: this.pos.minRange, + maxRange: this.pos.maxRange, + gasCosts: this.gasCosts, + profit, + }, + timestamp: new Date(data.timestamp * 1000), + }; + if (apy !== 0) log.fields.apy = apy; + + try { + await Log.writePoint(log); + } catch (e) { + console.log('log error'); + await Log.writePoint(log); + } + this.series.push({ + name: this.config.name, + timestamp: data.timestamp, + aum: totalAssets, + rewards: this.claimed, + minRange: this.pos.minRange, + maxRange: this.pos.maxRange, + token0InLp: this.pos.token0Bal, + token1InLp: this.pos.token1Bal, + feeToken0: this.pos.feeToken0T, + feeToken1: this.pos.feeToken1T, + rangeSpread: this.config.rangeSpread, + gasCosts: this.gasCosts, + lpAmount: this.pos.lpAmount, + ...tokens, + ...prices, + ...this.pos.snapshot, + }); + } + + public async end(uni: UniV3PositionManager, data: Uni3Snaphot) { + const totalAssets = this.estTotalAssets(data); + console.log('Strategy closing position', this.estTotalAssets(data)); + const close = await uni.close(this.pos); + this.idle = this.idle + close; + + const variance = Stats.variance(this.series.map((e) => e.aum)); + const stddev = Stats.stddev(variance); + + // Create summary + const toDate = (time: number) => + new Date(time * 1000) + .toISOString() + .replace(':00.000Z', '') + .replace('T', ' '); + + if (isNaN(this.apy(data))) { + console.log(this.apy(data)); + process.exit(); + } + + this.summary = { + name: this.config.name, + symbol: this.config.poolSymbol, + initial: this.config.initial, + aum: totalAssets, + roi: (totalAssets - this.config.initial) / this.config.initial, + apy: this.apy(data), + apr: this.apr(data), + drawdown: this.maxDrawdown, + rewards: this.claimed, + start: toDate(this.start), + end: toDate(data.timestamp), + daysElapsed: (data.timestamp - this.start) / (60 * 60 * 24), // days + variance, + rangeSpread: this.config.rangeSpread, + stddev, + rebalanceCount: this.rebalanceCount, + }; + await Summary.writePoint({ + tags: this.tags, + fields: this.summary, + timestamp: new Date(data.timestamp * 1000), + }); + } +} diff --git a/src/lib/utils/permutations.ts b/src/lib/utils/permutations.ts new file mode 100644 index 0000000..53f9169 --- /dev/null +++ b/src/lib/utils/permutations.ts @@ -0,0 +1,22 @@ +/** + * @brief Returns all permutations of an array of arrays + */ +export function permutations(arr: number[][]): number[][] { + const result: number[][] = []; + + function generate(index: number, current: number[]): void { + if (index === arr.length) { + result.push(current.slice()); + return; + } + for (let i = 0; i < arr[index].length; i++) { + current.push(Number(arr[index][i].toFixed(4))); + generate(index + 1, current); + current.pop(); + } + } + + generate(0, []); + + return result; +} diff --git a/src/lib/utils/utility.ts b/src/lib/utils/utility.ts index c90effc..1e7b126 100644 --- a/src/lib/utils/utility.ts +++ b/src/lib/utils/utility.ts @@ -50,3 +50,10 @@ export class Numbers { export const waitFor = (delay: number): Promise => new Promise((resolve) => setTimeout(resolve, delay)); + +export const range = (start: number, end: number, iterations: number) => { + const step = (end - start) / iterations; + return Array(iterations) + .fill(0) + .map((_, idx) => start + idx * step); +};