Skip to content

Commit

Permalink
Merge branch 'main' into matt-install-pypechain
Browse files Browse the repository at this point in the history
  • Loading branch information
sentilesdal authored Oct 12, 2023
2 parents e839f98 + af28db1 commit 22808d8
Show file tree
Hide file tree
Showing 32 changed files with 781 additions and 271 deletions.
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ We strive for verbose and readable comments and docstrings.
Our documentation follows the [Numpy format](https://numpydoc.readthedocs.io/en/latest/format.html).
The hosted docs are automatically generated using [Sphinx](https://www.sphinx-doc.org/en/master/tutorial/automatic-doc-generation.html).

# Contributor git workflow:
# Contributor git workflow

We follow a standard [feature branch rebase workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow) that prioritizes short PRs with isolated improvements.
Commits to `main` should **only** be made in the form of squash-merges from pull requests.
Expand All @@ -29,3 +29,14 @@ Once the PR is approved, we perform a final rebase, if necessary, and then a _sq

If two people are working in a branch then you should `git pull --rebase origin feature-branch` _before_ `git push origin feature-branch`.
We also recommend that you start each working session with a `pull` and end it with a `push`, so that your colleagues can work asynchronously while you are not.

# Contributing a new bot policy

We welcome contributions of new bot policies, alongside those in `lib/agent0/agent0/hyperdrive/policies`.

Submit a pull request that meets the following criteria:

1. Do something an existing policy doesn't already accomplish
2. Be well-documented and follow our existing [STYLEGUIDE.md](STYLEGUIDE.md).
3. Pass all linting checks, including `black` and `pylint`.
4. Describe the bot's behavior in a `describe` [classmethod](https://docs.python.org/3/library/functions.html#classmethod) similarly to existing bots, ending in a `super().describe()` call.
5 changes: 5 additions & 0 deletions STYLEGUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Style guide

1. Use [type hints](https://docs.python.org/3/library/typing.html).
2. Meet the maximum character limit of 120 characters per line. [Black](https://pypi.org/project/black/) will enforce this.
3. Write docstrings in [Numpy format](https://numpydoc.readthedocs.io/en/latest/format.html).
11 changes: 11 additions & 0 deletions lib/agent0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,14 @@ Run the script once to generate environment file, with filename set to `ENV_FILE
> ```bash
> DEVELOP=true python lib/agent0/examples/hyperdrive_agents.py
> ```

## Liquidating bots

The `LIQUIDATION` flag allows you to run your bots in liquidation mode. When this flag is true,
the bots will attempt to close out all open positions. The script will exit when this is complete.

>**💡NOTE:**
>If your bot has an LP position open, it's very likely your bot will repeatedly throw an error
>when in liquidation mode. This is due to attempting to close out withdrawal shares that are currently
>not available to withdraw. You can keep your script running in this case; the script will exit when all
>trades are successful.
1 change: 1 addition & 0 deletions lib/agent0/agent0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
build_account_key_config_from_agent_config,
initialize_accounts,
)
from .hyperdrive.policies.zoo import Zoo
17 changes: 17 additions & 0 deletions lib/agent0/agent0/base/policies/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import logging
from textwrap import dedent, indent
from dataclasses import dataclass
from typing import TYPE_CHECKING, Generic, TypeVar

Expand Down Expand Up @@ -66,3 +67,19 @@ def action(self, interface: MarketInterface, wallet: Wallet) -> tuple[list[Trade
and the second element defines if the agent is done trading
"""
raise NotImplementedError

@classmethod
def describe(cls, raw_description: str | None = None) -> str:
"""Describe the policy in a user friendly manner that allows newcomers to decide whether to use it.
Returns
-------
str
A description of the policy"""
if raw_description is None:
raise NotImplementedError(
"This method is meant to be called only by subclasses which provide a `raw_description`."
)
dedented_text = dedent(raw_description).strip()
indented_text = indent(dedented_text, " ") # Adding 2-space indent
return indented_text
29 changes: 21 additions & 8 deletions lib/agent0/agent0/hyperdrive/agents/hyperdrive_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ def __init__(self, account: LocalAccount, policy: Policy | None = None):
balance=Quantity(amount=self.policy.budget, unit=TokenType.BASE),
)

@property
def liquidation_trades(self) -> list[Trade[HyperdriveMarketAction]]:
def get_liquidation_trades(self) -> list[Trade[HyperdriveMarketAction]]:
"""List of trades that liquidate all open positions
Returns
Expand All @@ -82,46 +81,60 @@ def liquidation_trades(self) -> list[Trade[HyperdriveMarketAction]]:
for maturity_time, long in self.wallet.longs.items():
logging.debug("closing long: maturity_time=%g, balance=%s", maturity_time, long)
if long.balance > 0:
# TODO: Deprecate the old wallet in favor of this new one
action_list.append(
Trade(
market_type=MarketType.HYPERDRIVE,
market_action=HyperdriveMarketAction(
action_type=HyperdriveActionType.CLOSE_LONG,
trade_amount=long.balance,
wallet=self.wallet, # type: ignore
wallet=self.wallet,
maturity_time=maturity_time,
),
)
)
for maturity_time, short in self.wallet.shorts.items():
logging.debug("closing short: maturity_time=%g, balance=%s", maturity_time, short.balance)
if short.balance > 0:
# TODO: Deprecate the old wallet in favor of this new one
action_list.append(
Trade(
market_type=MarketType.HYPERDRIVE,
market_action=HyperdriveMarketAction(
action_type=HyperdriveActionType.CLOSE_SHORT,
trade_amount=short.balance,
wallet=self.wallet, # type: ignore
wallet=self.wallet,
maturity_time=maturity_time,
),
)
)
if self.wallet.lp_tokens > 0:
logging.debug("closing lp: lp_tokens=%s", self.wallet.lp_tokens)
# TODO: Deprecate the old wallet in favor of this new one
action_list.append(
Trade(
market_type=MarketType.HYPERDRIVE,
market_action=HyperdriveMarketAction(
action_type=HyperdriveActionType.REMOVE_LIQUIDITY,
trade_amount=self.wallet.lp_tokens,
wallet=self.wallet, # type: ignore
wallet=self.wallet,
),
)
)
if self.wallet.withdraw_shares > 0:
logging.debug("closing lp: lp_tokens=%s", self.wallet.lp_tokens)
action_list.append(
Trade(
market_type=MarketType.HYPERDRIVE,
market_action=HyperdriveMarketAction(
action_type=HyperdriveActionType.REDEEM_WITHDRAW_SHARE,
trade_amount=self.wallet.withdraw_shares,
wallet=self.wallet,
),
)
)

# If no more trades in wallet, set the done trading flag
if len(action_list) == 0:
self.done_trading = True

return action_list

def get_trades(self, interface: HyperdriveInterface) -> list[Trade[HyperdriveMarketAction]]:
Expand Down
15 changes: 11 additions & 4 deletions lib/agent0/agent0/hyperdrive/exec/execute_agent_trades.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ def assert_never(arg: NoReturn) -> NoReturn:


async def async_execute_single_agent_trade(
agent: HyperdriveAgent,
hyperdrive: HyperdriveInterface,
agent: HyperdriveAgent, hyperdrive: HyperdriveInterface, liquidate: bool
) -> list[TradeResult]:
"""Executes a single agent's trade. This function is async as
`match_contract_call_to_trade` waits for a transaction receipt.
Expand All @@ -45,14 +44,19 @@ async def async_execute_single_agent_trade(
The HyperdriveAgent that is conducting the trade
hyperdrive : HyperdriveInterface
The Hyperdrive API interface object
liquidate: bool
If set, will ignore all policy settings and liquidate all open positions
Returns
-------
list[TradeResult]
Returns a list of TradeResult objects, one for each trade made by the agent
TradeResult handles any information about the trade, as well as any errors that the trade resulted in
"""
trades: list[types.Trade[HyperdriveMarketAction]] = agent.get_trades(interface=hyperdrive)
if liquidate:
trades: list[types.Trade[HyperdriveMarketAction]] = agent.get_liquidation_trades()
else:
trades: list[types.Trade[HyperdriveMarketAction]] = agent.get_trades(interface=hyperdrive)

# Make trades async for this agent. This way, an agent can submit multiple trades for a single block

Expand Down Expand Up @@ -112,6 +116,7 @@ async def async_execute_single_agent_trade(
async def async_execute_agent_trades(
hyperdrive: HyperdriveInterface,
agents: list[HyperdriveAgent],
liquidate: bool,
) -> list[TradeResult]:
"""Hyperdrive forever into the sunset.
Expand All @@ -121,6 +126,8 @@ async def async_execute_agent_trades(
The Hyperdrive API interface object
agents : list[HyperdriveAgent]
A list of HyperdriveAgent that are conducting the trades
liquidate: bool
If set, will ignore all policy settings and liquidate all open positions
Returns
-------
Expand All @@ -131,7 +138,7 @@ async def async_execute_agent_trades(
# Make calls per agent to execute_single_agent_trade
# Await all trades to finish before continuing
gathered_trade_results: list[list[TradeResult]] = await asyncio.gather(
*[async_execute_single_agent_trade(agent, hyperdrive) for agent in agents if not agent.done_trading]
*[async_execute_single_agent_trade(agent, hyperdrive, liquidate) for agent in agents if not agent.done_trading]
)
# Flatten list of lists, since agent information is already in TradeResult
trade_results = [item for sublist in gathered_trade_results for item in sublist]
Expand Down
10 changes: 10 additions & 0 deletions lib/agent0/agent0/hyperdrive/exec/run_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
# TODO consolidate various configs into one config?
# Unsure if above is necessary, as long as key agent0 interface is concise.
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
def run_agents(
environment_config: EnvironmentConfig,
agent_config: list[AgentConfig],
account_key_config: AccountKeyConfig,
eth_config: EthConfig | None = None,
contract_addresses: HyperdriveAddresses | None = None,
load_wallet_state: bool = True,
liquidate: bool = False,
) -> None:
"""Entrypoint to run agents.
Expand All @@ -56,6 +58,8 @@ def run_agents(
defined in eth_config.
load_wallet_state: bool
If set, will connect to the db api to load wallet states from the current chain
liquidate: bool
If set, will ignore all policy settings and liquidate all open positions
"""
# See if develop flag is set
develop_env = os.environ.get("DEVELOP")
Expand Down Expand Up @@ -109,6 +113,11 @@ def run_agents(
agent.checksum_address, balances, hyperdrive.base_token_contract
)

# If we're in liquidation mode, we explicitly set halt on errors to false
# This is due to an expected error when redeeming withdrawal shares
if liquidate:
environment_config.halt_on_errors = False

# run the trades
last_executed_block = BlockNumber(0)
while True:
Expand All @@ -124,6 +133,7 @@ def run_agents(
environment_config.halt_on_errors,
environment_config.halt_on_slippage,
last_executed_block,
liquidate,
)


Expand Down
9 changes: 8 additions & 1 deletion lib/agent0/agent0/hyperdrive/exec/trade_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
# TODO: Suppress logging from ethpy here as agent0 handles logging


# TODO cleanup this function
# pylint: disable=too-many-arguments
def trade_if_new_block(
hyperdrive: HyperdriveInterface,
agent_accounts: list[HyperdriveAgent],
halt_on_errors: bool,
halt_on_slippage: bool,
last_executed_block: int,
liquidate: bool,
) -> int:
"""Execute trades if there is a new block.
Expand All @@ -40,6 +43,8 @@ def trade_if_new_block(
don't raise an exception if slippage happens.
last_executed_block : int
The block number when a trade last happened
liquidate: bool
If set, will ignore all policy settings and liquidate all open positions
Returns
-------
Expand All @@ -62,7 +67,9 @@ def trade_if_new_block(
)
# To avoid jumbled print statements due to asyncio, we handle all logging and crash reporting
# here, with inner functions returning trade results.
trade_results: list[TradeResult] = asyncio.run(async_execute_agent_trades(hyperdrive, agent_accounts))
trade_results: list[TradeResult] = asyncio.run(
async_execute_agent_trades(hyperdrive, agent_accounts, liquidate)
)
last_executed_block = latest_block_number

for trade_result in trade_results:
Expand Down
16 changes: 2 additions & 14 deletions lib/agent0/agent0/hyperdrive/policies/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
"""Policies for expert system trading bots"""
from __future__ import annotations

from typing import NamedTuple

from .arbitrage import ArbitragePolicy

# Base policy to subclass from
from .hyperdrive_policy import HyperdrivePolicy
from .random_agent import RandomAgent


class Policies(NamedTuple):
"""All policies in elfpy."""

random_agent = RandomAgent
arbitrage_policy = ArbitragePolicy
from .hyperdrive_policy import HyperdrivePolicy # Base policy to subclass from
from .zoo import Zoo
20 changes: 19 additions & 1 deletion lib/agent0/agent0/hyperdrive/policies/arbitrage.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from numpy.random._generator import Generator as NumpyGenerator


class ArbitragePolicy(HyperdrivePolicy):
class Arbitrage(HyperdrivePolicy):
"""Agent that arbitrages based on the fixed rate
.. note::
Expand All @@ -29,6 +29,24 @@ class ArbitragePolicy(HyperdrivePolicy):
I close all open longs and open a new short for `trade_amount` bonds
"""

@classmethod
def description(cls) -> str:
"""Describe the policy in a user friendly manner that allows newcomers to decide whether to use it.
Returns
-------
str
A description of the policy"""
raw_description = """
Take advantage of deviations in the fixed rate from specified parameters.
The following 3 parameters in Config define its operation:
- When `high_fixed_rate_thresh`, open shorts are closed, and a long is opened.
- When `low_fixed_rate_thresh`, open longs are closed, and a short is opened.
- Trade size is fixed by `trade_amount`.
Additionally, longs and shorts are closed if they are matured.
"""
return super().describe(raw_description)

@dataclass
class Config(HyperdrivePolicy.Config):
"""Custom config arguments for this policy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,26 @@
from numpy.random._generator import Generator as NumpyGenerator


class RandomAgent(HyperdrivePolicy):
class Random(HyperdrivePolicy):
"""Random agent."""

@classmethod
def description(cls) -> str:
"""Describe the policy in a user friendly manner that allows newcomers to decide whether to use it.
Returns
-------
str
A description of the policy"""
raw_description = """
A simple demonstration agent that chooses its actions randomly.
It can take 7 actions: open/close longs and shorts, add/remove liquidity, and redeem withdraw shares.
Trade size is randomly drawn from a normal distribution with mean of 10% of budget and standard deviation of 1% of budget.
A close action picks a random open position of the given type (long or short) and attempts to close its entire size.
Withdrawals of liquidity and redemption of withdrawal shares is sized the same as an open position: N(0.1, 0.01) * budget.
"""
return super().describe(raw_description)

@dataclass
class Config(HyperdrivePolicy.Config):
"""Custom config arguments for this policy
Expand Down
Loading

0 comments on commit 22808d8

Please sign in to comment.