Create an Agent
This tutorial is focused on creating an agent from scratch and using it in a simulation.
It is possible to clone an existing agent (or existing strategy) from the library and simply edit it with the desired behaviour. This tutorial does not cover the use of the library but only the creation of local files.
In order to create a new agent, the user has to handle 4 parts:
- Agent Code (python file)
- Agent Strategy Code (python file)
- Agent Configuration (yaml file)
- Agent Metrics (python file)
Additionally this tutorial will provide guidance on the following:
- Best Practices
- Quick Tips & Access
- Can’t Dos
- Dev’ing and Troubleshooting
- The Agent Code
Agents
An Agent is an instantiation of an agent class coded in python. Apart from its strategy, the Agent houses the wallet and other attributes relevant to execute the strategy within the simulation environment.
1.1. Agent Code: The Structure
All Agents code should live under the same folder in the simulation folder and each agent class should have it’s own folder containing at least two (2) files:
- main.py
- This contains the code
- almanak-library.yaml
- This contains library metadata and dependencies information.
- This file is mandatory even if library features are not used
almanak-library.yaml
name: "Arb Agent"
version: "1.0.0"
type: "agent"
author: "Almanak AG"
description:
"This Arbitrageur does the job of both an Arb and a Trader by making sure it trades a specific volume. If the arb opportunities < required volume then it will swap until the volume is met."
license: "MIT"
supported_chains:
- engine: "evm"
chainId: "1"
dependencies:
protocols:
- "library://almanak/protocols/uniswap-v3:1.0.0"
It is important to note that the multiple instances (units) of an agent class can be spawn at runtime via the configuration. Simply put, the user codes a single Trader logic, which can then be configured to have 100 instances of such Trader, although with different wallets.
1.2. Agent Code: The Tools
To enable the actions of an Agent and fetch all information necessary to aggregate everything for the strategy to determine what to do. Multiple sources can be leveraged:
- Agent (AgentHelper)
- Retrieving information from the Agent attributes, initialized when starting the simulation.
- Settings: self.agentHelper.get_settings()
- e.g. volume_to_check = self.agentHelper.get_settings().get("volume_to_check")
- Address: agent_address = self.agentHelper.get_address(environment.get_alias())
- An agent has an address for each environment.
- Strategy
- Environment (EnvironmentHelper):
- self.agentHelper.get_environment("ethereum1")
- Simulation (SimulationStateHelper):
- print("Current Step:", simulation_state.current_step)
- Protocol(s) (Protocol Interfaces):
- Dex: protocol_dex.dex.quote_input(...)
- Lending: protocol.lending.borrow(...)
- Custom: protocol.custom.a_function(...)
- Prices (Price Feed):
- accessible via SimulationStateHelperInterface.get_price_feed()
Tokens (TokenInterface):
get_balance, approve, address, symbol, address, …
Get the token classes from the environment. token0 = environment.get_token(uniswapv3_pool_token0) token1 = environment.get_token(uniswapv3_pool_token1)
1.4. Agent Code: Agent Step
The Agent Step function receives environmentHelper: EnvironmentHelperInterface containing the necessary functions to interact with the environment as well as simulation_state: SimulationStateHelperInterface containing information such as the current step the simulation is currently performing.
We recommend starting such function with the following (here, the main protocol is a DEX):
agent = self.agentHelper environment = agent.get_environment(environmentHelper.get_alias()) agent_address = agent.get_address(environment.get_alias()) protocol_dex = environmentHelper.get_protocol(self.protocol_dex_name)
All the on-chain interaction will happen via protocol SDKs [TODO: Insert Docs Link].
- The Agent Strategy Code
[Coming Soon]
- The Agent Configuration
The configuration of the agent(s) is done in the main configuration.yaml containing all the simulation configuration. Here is an example of the configuration for an arb agent using an arb-cyclic strategy with a few custom settings such as the Uniswap V3 pool, the main asset pair of interest and volume related settings used in the strategy.
agents:
- address: '0xA69babEF1cA67A37Ffaf7a485DfFF3382056e78C'
agentType: regular
environments:
- address: '0xA69babEF1cA67A37Ffaf7a485DfFF3382056e78C'
alias: ethereum1
wallet:
tokens:
- amount: 100000000000000000000
mint: true
token: WETH
- amount: 1000000000000
mint: true
token: USDT
settings:
cex_token0: ETH
cex_token1: USDT
cex_trade_fee: 0.001
protocol_dex_name: Uniswap V3
uniswapv3_pool_fee: 3000
uniswapv3_pool_token0: WETH
uniswapv3_pool_token1: USDT
volume_buffer: 0.9
volume_to_check:
- 0.3
- 0.6
- 0.9
volume_to_trade: 1
source: file://agent/arb/
strategy: file://strategy/arb-cyclic/
4. Best Practices
4.1. Agent Class Variables vs State Variables vs Settings
There are 3 different ways of accessing agents variables:
Class variables: self.my_variable
State variables: self.agent_helper.get_variable("my_variable")
Settings: self.agentHelper.get_settings().get("my_variable")
It is important to keep in mind that the object gets re-instantiated at each step. The init() function of the class gets called each time to reconstruct the object.
4.2. init vs Initialization
As mentioned above (in 4.1) the agent object gets re-instantiated at each step, therefore, as a consequence the object variables (self.my_variable) are lost in-between steps (and also in-between initialization and step #1) and the init function is called quite frenquently on possibly a high number of agents.
In the init function we should always find the strategy, agentHelper and metricHelper affectation. Then optionally we recommend to store the settings in class variables of the same names, should such setting be used across sub-functions within the agent code.
class Liquidator(AgentInterface): def init(self, strategy: StrategyInterface, agentHelper: AgentHelperInterface, metricHelper: MetricHelperInterface): super().init(strategy, agentHelper, metricHelper) self.strategy = strategy self.agentHelper = agentHelper self.liquidate = False
get the settings from the agent
settings = self.agentHelper.get_settings() self.address = settings.get("address") self.protocol_lending: str = settings.get("protocol_money_market") self.protocol_amm: str = settings.get("protocol_amm") self.account_to_monitor: str = settings.get("account_to_monitor") self.collateral: str = settings.get("collateral") self.debt: str = settings.get("debt") self.amm_pool: str = settings.get("pool")
def agent_initialization(self, simulation_state: SimulationStateHelperInterface): super().agent_initialization(simulation_state)
''' This class represents a potential liquidator to act on top of Aave & liquidate one account once the price of the collateral falls below account health. '''
The agent_initialization function is where any on-chain activity should be done. This function is only called once, before doing any simulation steps. Any “starting” action and/or on-chain state should be performed in here.
- Quick Tips & Access
Here is a cheat list of convenient things the user will need over and over again!
Simulation Status
Current Step (int): simulation_state.current_step
Tokens
Token Contract (class):
Prices
get_current_price
get_volatility
- Can’t Dos
Can’t wait for transaction receipt in a blocking way.
Can’t mine a block.
- Dev’ing & Troubleshooting
[Coming Soon]