# Overview

[![Twitter URL](https://img.shields.io/twitter/url.svg?label=Follow%20%40BellaProtocol\&style=social\&url=https%3A%2F%2Ftwitter.com%2FBellaProtocol)](https://twitter.com/BellaProtocol) [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/8ctd5geS8t)

## the "*Tuner*", a programmable, transaction-based Uniswap V3 Simulator with 100% Precision

> Before an orchestra, every musical instrument has to be *in-tune*, to ensure an outstanding performance.
>
> Before running a strategy, every parameter has to be *fine tuned*, to maximize the performance.

***Tuner*****&#x20;is a programmatic Uniswap V3 simulator that allows strategy backtesting on a transaction-to-transaction basis with arbitrary or historical data without the EVM, it runs independently yet completely retains the exact smart-contract behavior of the intricate design and implementation of Uniswap V3.**

### Documentation

* [Getting Started](https://docs.bella.fi/getting-started)
* [Configuration](https://docs.bella.fi/configuration)
* [Guides](https://docs.bella.fi/guides)
* Developer Articles(stay tuned)

***Tuner*****&#x20;is fundamentally a state machine, it can:**

> **Completely replicate the tick-level calculation**

* this means your strategy will run through the Uniswap V3 implementation logic instead of just the high-level mathematic model.

> **Maintain the identical tick-level precision of prices, fees, and positions of Uniswap V3**

* this means the result of your backtesting is true to the real performance with the minimum margin of deviations.

> **Run fast**

* the EVM is slow, the historical dataset is huge, the Ganache cannot do the job, so use *Tuner*.

> **Fast-forward and rewind transactions**

* this means you can easily repeat a small portion of your test with a different set of parameters without the need to start over.

> **Take or recover from a snapshot(state)**

* this means you can run continuous regression test as your strategies constantly evolves.

> **Branch out and runs in parallel**

* this means you can run multiple back-tests each with a different set of parameters at the same time and compare the performance.

> **Persist historical data and strategy execution records in a SQLite database**

* this means the strategists can do advanced statistical analysis both in real-time and after the testing.


# How "Tuner" Library Works?

The overall design of the simulator consists of several components (rough dependency graph shown below), this **does NOT** necessarily 100% reflect how everything is implemented, but to give you an intuitive understanding of the relationships between the components and their corresponding purposes:

**`SimulatorClient`** - *The high-level and easiest way to use the simulator*

|\_\_ **`ConfigurableCorePool`** - *To give **`CorePool`** snapshot and roadmap capabilities*

|\_\_ **`CorePool`** - *The re-implementation of* [*Uniswap-V3-Core logic*](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol)

|\_\_ **`MainnetDataDownloader`** - *The download-and-update utility to retrieve mainnet events*

|\_\_ **`EventDBManager`** - *To persist mainnet event data using SQLite*

|\_\_ **`SimulatorRoadMapManager`** - *To take snapshots and do state-change roadmap tracking*

|\_\_ **`SimulationDataManager`** - *To persist snapshots and roadmaps using SQLite*

There are 2 abstraction layers of the library:

## Top-Level: [`SimulatorClient`](https://github.com/Bella-DeFinTech/uniswap-v3-simulator/blob/main/docs/src/client/SimulatorClient.ts)

This is a convenient way to [get you started](https://github.com/Bella-DeFinTech/uniswap-v3-simulator/blob/main/docs/README.md#quick-start) immediately. We have wrapped the underlying logic of data downloading, data pre-processing, and simulated Uniswap V3 pool together instantiation together, so you don't have to deal with the details.

## Low-Level:

If you want more flexible ways to run your own simulation scenarios and have more granular control, you can wire things up manually as you like, by directly interacting with the below classes:

* **`ConfigurableCorePool`**
* **`MainnetDataDownloader`**
* **`SimulatorRoadMapManager`**

##


# Installing "Tuner"

{% embed url="<https://www.npmjs.com/package/@bella-defintech/uniswap-v3-simulator>" %}

```bash
yarn add @bella-defintech/uniswap-v3-simulator
```

```bash
yarn upgrade @bella-defintech/uniswap-v3-simulator
```


# Quick Start

```typescript
import {
  SimulationDataManager,
  SimulatorClient,
  SQLiteSimulationDataManager,
} from "@bella-defintech/uniswap-v3-simulator";

// 1. Instantiate a SimulationDataManager
// this is for handling the internal data (snapshots, roadmaps, etc.)
let simulationDataManager: SimulationDataManager =
  await SQLiteSimulationDataManager.buildInstance(
    "Your file path to save the internal data"
  );
let clientInstance: SimulatorClient = new SimulatorClient(
  simulationDataManager
);

// 2. Initialize a pool simulation from mainnet
// specify the core pool you want and time range of event logs to sync
let poolName = "test";

// This would be the contract address of a certain Uniswap V3 Pool
let poolAddress = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8";

// Specify an endBlock number
// the SimulatorClient will download data up to that block
// and save it to a sqlite database file locally
let endBlock = 12374077;

// 3. This method helps you:
//    Download event data of a certain Uniswap V3 pool from mainnet
//    Pre-process the data to figure out the inputs of swap events
await clientInstance.initCorePoolFromMainnet(
  poolName,
  poolAddress,
  "afterDeployment"
);

// 4. Build a simulated CorePool instance from the downloaded-and-pre-processed mainnet events
let configurableCorePool = await clientInstance.recoverFromMainnetEventDBFile(
  `${poolName}_${poolAddress}.db`,
  endBlock
);

// 5. Now you can do whatever you want with the simulated pool
//    here we simply print out the square root price of the pool at the specified block height
console.log(configurableCorePool.getCorePool().sqrtPriceX96.toString());
```


# Configuration

When Tuner is run, it searches for the closest tuner.config.js file starting from the Current Working Directory. This file normally lives in the root of your project. The `RPCProviderUrl` need to be specified, and it's recommended to setup an environment variable:

```javascript
module.exports = {
  RPCProviderUrl: process.env.MAINNET_PROVIDER_URL,
};
```


# (Basic)For anyone who is interested in the Uniswap v3 model


# Building a client instance

A `SimulatorClient` is the user entry point of the Tuner. It needs a `SimulationDataManager` to persist internal data supporting functionality of the simulator, while external data refer to state of some Uniswap v3 Core Pool on chain.

We provide an implementation of `SimulationDataManager` using SQLite, so it's recommended to provide a file path locally to save the internal data(in memory by default), then you can recover a pool from a snapshot. If you are familiar with the interface, it's ok to replace it with a customed implementation, e.g. some database connected remotely.

```typescript
// 1. Instantiate a SimulationDataManager
// this is for handling the internal data (snapshots, roadmaps, etc.)
let simulationDataManager: SimulationDataManager =
  await SQLiteSimulationDataManager.buildInstance(
    "Your file path to save the internal data"
  );
let clientInstance: SimulatorClient = new SimulatorClient(
  simulationDataManager
);
```

It's recommended to close the client when you finish with Tuner.

```typescript
await clientInstance.shutdown();
```


# About Core Pool Config

A `PoolConfig` is a group of key meta parameters of an Uniswap V3 Core Pool. In details, `TickSpacing`, `Token0Address`, `Token1Address`, and `FeeAmount`.

If you want to build a Core Pool from scratch to study something about the math model, you have to build it manually before build that pool. And it won't bother you if you want to get an instance from the state of some existed core pool on chain.

```typescript
let configurableCorePool: IConfigurableCorePool =
  clientInstance.initCorePoolFromConfig(
    new PoolConfig(60, "USDC", "ETH", FeeAmount.MEDIUM)
  );
```


# Getting a Core Pool instance

A simple way to get a Core Pool instance is to build a new one according to the `PoolConfig`.

```typescript
let configurableCorePool: IConfigurableCorePool =
  clientInstance.initCorePoolFromConfig(
    new PoolConfig(60, "USDC", "ETH", FeeAmount.MEDIUM)
  );
```

And then don't forget to initialize that before executing any interaction.

```typescript
let sqrtPriceX96ForInitialization = JSBI.BigInt("4295128739");
await configurableCorePool.initialize(sqrtPriceX96ForInitialization);
```


# Interacting with Core Pool

A `CorePool` corresponds to a contract of Uniswap V3 Core Pool. You can inspect its state like `sqrtPriceX96`, `tickCurrent`, `lquidity` and more detailed information on any `Tick` or `Position`.

```typescript
let corePoolView: CorePoolView = configurableCorePool.getCorePool();

corePoolView.tickCurrent;

corePoolView.getTick(tickIndex);

corePoolView.getPosition(owner, tickLower, tickUpper);
```

Also, you can interact with the pool by `mint`, `burn` and `swap`. However, while interacting with the pool, we recommend using `ConfigurableCorePool` to get full functionality of the Tuner.

Based on the `CorePool`, a `ConfigurableCorePool` is a state machine providing utilities like post-processor, fork, snapshot, step back and recover.

```typescript
let amount0: JSBI, amount1: JSBI;

({ amount0, amount1 } = await configurableCorePool.mint(
  testUser,
  tickLower,
  tickUpper,
  liquidity
));

({ amount0, amount1 } = await configurableCorePool.burn(
  testUser,
  tickLower,
  tickUpper,
  liquidity
));

({ amount0, amount1 } = await configurableCorePool.swap(
  zeroForOne,
  amountSpecified,
  sqrtPriceLimitX96
));
```

Since Tuner is focusing on the math model instead of token or stratgy, feel free to `mint`, `burn` and `swap` as you like, then observe whether things go as you expect.


# (Typical)For a quant developer who works on a real pool on mainnet


# Fetching all the data of a certain pool from Ethereum

Tuner syncs the full state of the Pool from the event logs on chain, aiming to reproduce 100% as things happened in the mainnet.

For a quant developer, Tuner offers an easy-to-use API to download/update and persist event logs, and then build the core pool instance for the user. According to transaction volume of wanted core pool, the first run will cost some time. See [Performance](/performance/performance).

Specifically, you can set block number or other key dates as the upper limit from the deployment of your specified core pool, as time range of event logs. The SimulatorClient will download data up to that block and save it to a SQLite database file locally.

Note: The database for event logs(external data) and the one to support functionality of Tuner(internal data) are different SQLite database files.

```typescript
export type EndBlockTypeWhenInit =
  | number
  | "latest"
  | "afterDeployment"
  | "afterInitialization";

export type EndBlockTypeWhenRecover =
  | number
  | "latestOnChain"
  | "latestDownloaded"
  | "afterDeployment"
  | "afterInitialization";
```

For downloading mainnet data, we had to pre-process the data because mainnet events of swaps only represent the results of the swap, we had to use a try-and-error logic to test out the actual inputs of that swap.

This means, there is more information added on top of the mainnet events, and this extra information (correct inputs of the swap that emit the event recorded on mainnet) is only added during the download process.

`SimulatorClient` will handle with processes above and you just need to call `clientInstance.initCorePoolFromMainnet` to download event logs for a new pool or `clientInstance.recoverFromMainnetEventDBFile` to update event logs for an existed pool. They both give you an instance of `ConfigurableCorePool` finally.

An example of the whole process will be:

```typescript
import {
  EndBlockTypeWhenRecover,
  EventDataSourceType,
  SimulationDataManager,
  SimulatorClient,
  SQLiteSimulationDataManager,
} from "@bella-defintech/uniswap-v3-simulator";

// 1. Instantiate a SimulationDataManager
// this is for handling the internal data (snapshots, roadmaps, etc.)
let simulationDataManager: SimulationDataManager =
  await SQLiteSimulationDataManager.buildInstance(
    "Your file path to save the internal data"
  );
let clientInstance: SimulatorClient = new SimulatorClient(
  simulationDataManager
);

// 2. Specify the core pool you want and time range of event logs to sync
let poolName = "events-test";

// This would be the contract address of a certain Uniswap V3 Pool
let poolAddress = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8";

let endBlock: EndBlockTypeWhenRecover = 12374077;

// This is the rpc provider url for ethers.js, you can customize it here or use the value in tuner.config.js
let RPCProviderUrl: string | undefined = "Your customed RPCProviderUrl";

// You can specify data source of events here, and Uniswap v3 Subgraph as default is recommended rather than RPC for at least 75% time saving
// Just a reminder, RPC endpoint is necessary for the simulator even if you choose to download events from Subgraph
let eventDataSourceType: EventDataSourceType = EventDataSourceType.SUBGRAPH;

// 3(a). This method helps you:
//    Download event data of a certain Uniswap V3 pool from mainnet
//    Pre-process the data to figure out the inputs of swap events
//    Finally get the core pool instance
if (!exists(`${poolName}_${poolAddress}.db`)) {
  let configurableCorePool: ConfigurableCorePool =
    await clientInstance.initCorePoolFromMainnet(
      poolName,
      poolAddress,
      endBlock,
      RPCProviderUrl,
      eventDataSourceType
    );
}

// 3(b). This method helps you:
//    Update and Pre-process event data of a certain Uniswap V3 pool from mainnet if necessary
//    Build a simulated CorePool instance from the downloaded-and-pre-processed mainnet events
let configurableCorePool: ConfigurableCorePool =
  await clientInstance.recoverFromMainnetEventDBFile(
    `${poolName}_${poolAddress}.db`,
    endBlock,
    RPCProviderUrl,
    eventDataSourceType
  );

// 4. It's recommended to close the client when you finish with Tuner
await clientInstance.shutdown();
```

Note: If a bad gateway error(usually due to the hosted service of subgraph) happens, just wait a few seconds and then give it a re-run. Tuner has taken error handling into consideration to make sure integrality and consistency of events.


# Getting a pool instance with the data fetched

Usually before work begins, the preparation includes the following steps:

1. Using mainnet data to initialize a core pool
2. Replaying events up to the specified block

`SimulatorClient`(`clientInstance.initCorePoolFromMainnet` and `clientInstance.recoverFromMainnetEventDBFile`) is designed to do that. See [Fetching all the data of a certain pool from Ethereum](/guides/typical-for-a-quant-developer-who-works-on-a-real-pool-on-mainnet/fetching-all-the-data-of-a-certain-pool-from-ethereum).

If you have tried `MainnetDataDownloader` to download or update event logs(See [Uniswap-v3-Events-Downloader](https://github.com/Bella-DeFinTech/uniswap-v3-simulator/tree/main/examples/Uniswap-v3-Events-Downloader)), then you are supposed to get the pool instance in this way:

```typescript
// the database name containing downloaded-and-pre-processed mainnet events
let mainnetEventDBFilePath =
  "events_0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8.db";

let simulationDataManager: SimulationDataManager =
  await SQLiteSimulationDataManager.buildInstance(
    "Your file path to save the internal data"
  );
let clientInstance: SimulatorClient = new SimulatorClient(
  simulationDataManager
);

// Specify an endBlock number
// the SimulatorClient will replay events up to that block
let endBlock = 12374077;

let configurableCorePool: ConfigurableCorePool =
  await clientInstance.recoverFromMainnetEventDBFile(
    mainnetEventDBFilePath,
    endBlock
  );

// Now you can do whatever you want with the simulated pool
// here we simply print out the square root price of the pool at the specified block height
console.log(configurableCorePool.getCorePool().sqrtPriceX96.toString());
```


# Loading and streaming events into a pool

Suppose a quant developer is going to backtest some strategy on mainnet pool.

This includes the following steps:

1. Using mainnet data to initialize a core pool
2. Replaying events up to a certain block as a checkpoint
3. Do some interaction as the strategy asks as interpolation to real transactions
4. Replaying events until the next checkpoint
5. Repeat step3-4 until events are run out
6. Evaluate performance of the strategy

Tuner has offered an example project called `uniswap-v3-bot`. It similarly follow the process above to build a strategy platform for backtesting, dry-run and run, where a user adds a new strategy by implementing some callback interfaces(`trigger`, `cache`, `act` and `evaluate`). See [Uniswap-v3-Strategy-Backtest](https://github.com/Bella-DeFinTech/uniswap-v3-simulator/tree/main/examples/Uniswap-v3-Strategy-Backtest).

When getting a core pool instance by `clientInstance.initCorePoolFromMainnet` or `clientInstance.recoverFromMainnetEventDBFile`, Tuner has already automatically replayed events from the first record to the last one in `endBlock`. For replaying following events, you can load them by `EventDBManager`, and decide how to use them on your own.

An example of streaming process will be:

```typescript
import {
  ConfigurableCorePool,
  EventDBManager,
  EventType,
  SimulationDataManager,
  SimulatorClient,
  SQLiteSimulationDataManager,
} from "@bella-defintech/uniswap-v3-simulator";
import { LiquidityEvent } from "@bella-defintech/uniswap-v3-simulator/dist/entity/LiquidityEvent";
import { SwapEvent } from "@bella-defintech/uniswap-v3-simulator/dist/entity/SwapEvent";
import JSBI from "jsbi";

// the database name containing downloaded-and-pre-processed mainnet events
let mainnetEventDBFilePath =
  "events_0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8.db";

// build a client instance
let simulationDataManager: SimulationDataManager =
  await SQLiteSimulationDataManager.buildInstance(
    "Your file path to save the internal data"
  );
let clientInstance: SimulatorClient = new SimulatorClient(
  simulationDataManager
);

// Specify an endBlock number
// the SimulatorClient will replay events up to that block
let endBlock0 = 12374077;

// get a pool instance
let configurableCorePool: ConfigurableCorePool =
  await clientInstance.recoverFromMainnetEventDBFile(
    mainnetEventDBFilePath,
    endBlock0
  );

// get an EventDBManager instance to load events
let eventDB = await EventDBManager.buildInstance(mainnetEventDBFilePath);

// get and sort event by block number
let events: (LiquidityEvent | SwapEvent)[] = [];
let startBlock = 1000,
  endBlock = 2000;
let mintEvents: LiquidityEvent[] =
  await eventDB.getLiquidityEventsByBlockNumber(
    EventType.MINT,
    startBlock,
    endBlock
  );
let burnEvents: LiquidityEvent[] =
  await eventDB.getLiquidityEventsByBlockNumber(
    EventType.BURN,
    startBlock,
    endBlock
  );
let swapEvents: SwapEvent[] = await eventDB.getSwapEventsByBlockNumber(
  startBlock,
  endBlock
);
events.push(...mintEvents);
events.push(...burnEvents);
events.push(...swapEvents);
events.sort(function (a, b) {
  return a.blockNumber == b.blockNumber
    ? a.logIndex - b.logIndex
    : a.blockNumber - b.blockNumber;
});

// replay events
for (let index = 0; index < events.length; index++) {
  // avoid stack overflow for the possible recovering operation
  if (index % 1000 == 0) {
    configurableCorePool.takeSnapshot("");
  }

  let event = events[index];
  switch (event.type) {
    case EventType.MINT:
      await configurableCorePool.mint(
        event.recipient,
        event.tickLower,
        event.tickUpper,
        event.liquidity
      );
      break;
    case EventType.BURN:
      await configurableCorePool.burn(
        event.msgSender,
        event.tickLower,
        event.tickUpper,
        event.liquidity
      );
      break;
    case EventType.SWAP:
      let zeroForOne: boolean = JSBI.greaterThan(event.amount0, JSBI.BigInt(0))
        ? true
        : false;
      await configurableCorePool.swap(
        zeroForOne,
        event.amountSpecified
        //,event.sqrt_price_x96
      );
      break;
    default:
      // @ts-ignore: ExhaustiveCheck
      const exhaustiveCheck: never = event;
  }
}
```

Note: Tuner doesn't export `LiquidityEvent` | `SwapEvent` directly but you can sitll use them. For external data(event logs), we recommend you to implement your `EventDBManager` to load and manage event logs, according to your context.


# (Advanced)For a better user experience as a state machine


# PoolState & Transition

A `ConfigurableCorePool` is a state machine based on the math model aka `CorePool` of Uniswap v3 contract implementation.

Every core pool state corresponds to a `PoolState`. Every interaction(`mint`, `burn`, `swap`, `collect` and `fork`) corresponds to a `Transition`. A `Record` contains information about the action. A `Transition` makes a `PoolState` move to next `PoolState` according to the `Record`.


# Post-processor

A post-processor registered in `ConfigurableCorePool` works as an interceptor to execute callback function after executing an interaction. You can customize your handler function with `ConfigurableCorePool`(pool state after the interaction) and `TransitionView` exposed by the post-processor.

Take for example, when locating errors, you can check the state after executing an interaction from a large number of events, take the snapshot, think about what happened exactly and resume from that state later.

You can set a post-processor after every interaction in this way.

```typescript
configurableCorePool.updatePostProcessor(
  (pool: ConfigurableCorePool, transition: TransitionView) => {
    console.log(pool.getPoolState().id);
    console.log(transition.getRecord().id);
    return Promise.resolve();
  }
);
```

Or set it with a specific interaction.

```typescript
await configurableCorePool.mint(
  "0x01",
  -887272,
  887272,
  JSBI.BigInt("10860507277202"),
  (pool: ConfigurableCorePool, transition: TransitionView) => {
    console.log(pool.getPoolState().id);
    console.log(transition.getRecord().id);
    return Promise.resolve();
  }
);
```

Note: If any error happens during the interaction or the post process, the `ConfigurableCorePool` will recover as the state before that interaction happened, just like contract transaction reverts.


# Forking & Retracing

During the lifetime of a program, Tuner will record every step that a `ConfigurableCorePool` went through in memory. With an id of `PoolState`, you can let the pool recover to that state.

```typescript
// current state: foo
let poolStateId: string = configurableCorePool.getPoolState().id;

// some transactions...
// current state: bar

configurableCorePool.recover(poolStateId);
// current state: foo
```

Or just let the pool step back to last state.

```typescript
// current state: foo
let poolStateId: string = configurableCorePool.getPoolState().id;

// one transaction...
// current state: bar

configurableCorePool.stepBack();
// current state: foo
```

Sometime we want multiple instances to try out various of possibilities from current state, it's good time using `fork` to do that.

```typescript
let forkedConfigurableCorePool: ConfigurableCorePool =
  configurableCorePool.fork();

// some transactions with configurableCorePool...

// some transactions with forkedConfigurableCorePool...
```

The forked pool will remain the same state as its parent and the two pools act independently.

Note: `poolState.fromTransition` of a pool built from a `PoolConfig` or recovered from a snapshot will be `undefined` while `poolState.fromTransition` of a forked pool will have a `Record` with `ActionType.FORK` and its parent as source `PoolState`.


# Persisting & Recovering

If a state is important enough that you want to test something on it across multiple programs, e.g. suppose you want to test something important on the state after replaying 200,000 events from the deployment of a pool, you can replay those events for once, then persist everything as a snapshot and directly resume from then on to make the preparation faster.

```typescript
configurableCorePool.takeSnapshot("description for snapshot");
```

Then don't forget to persist it so that you can recover/resume later.

```typescript
let snapshotId: string = await configurableCorePool.persistSnapshot();
```

Later you can recover the pool from any snapshot in the internal database(local database file with default SQLite implementation).

```typescript
let recoveredConfigurableCorePool: ConfigurableCorePool =
  await clientInstance.recoverCorePoolFromSnapshot(snapshotId);
```

If you forget the snapshotId, you can list and check all snapshots by information like description or created timestamp.

```typescript
let snapshotProfiles: SnapshotProfile[] =
  await clientInstance.listSnapshotProfiles();
```


# SimulatorRoadmapManager

If we take every `ConfigurableCorePool` as a state machine, then a `SimulatorRoadmapManager` can be taken as `PoolState` container as well as `ConfigurableCorePool` manager. At this point, every state machine can be expanded as a roadmap of states.

First let's get the instance of `SimulatorRoadmapManager` from `SimulatorClient`.

```typescript
let simulatorRoadmapManager: SimulatorRoadmapManager =
  clientInstance.simulatorRoadmapManager;
```

You can list and check all `ConfigurableCorePool` created by Tuner within the program.

```typescript
let pools: ConfigurableCorePool[] = await simulatorRoadmapManager.listRoutes();
```

With a `ConfigurableCorePool` id, Tuner can print route from the first pool state to current pool state of the state machine in pretty format.

```typescript
await simulatorRoadmapManager.printRoute(configurableCorePool.id);
```

Also you can persist the route for selecting them in the internal database later.

```typescript
let roadmapId = await simulatorRoadmapManager.persistRoute(
  configurableCorePool.id,
  "description for roadmap"
);
```

Then load the roadmap and print the route in pretty format.

```typescript
await simulatorRoadmapManager.loadAndPrintRoute(roadmapId);
```

Note: As the storage scale of Uniswap V3 CorePool(up to 160,000+ Ticks and unlimited Positions), frequent persistence of route as well as snapshot is not recommended. Please pay attention to space cost.


# Performance

Environment: AWS EC2 t3.xlarge on us-east-1, RPC provider by Alchemy, Subgraph by hosted service on the Graph

Downloading 260,000+ events of WETH-USDC-3000(upper-middle transaction volume within all Uniswap v3 core pools) until #14027607 (Jan 18, 2022 approximately):

* Tuner v0.1.3 **461.6m(>7 hour)**
* Tuner v0.1.4 **51.1m(<1 hour)**

Replaying over 124,000 mainnet events: **within 16 second**


# Uniswap-v3-Risk-Analysis(will be available soon)


# Contributing

Please create an issue on github repo for any questions.

Thanks and enjoy!


