zelos-alpha / demeter

Better backtest toolkit for Uniswap v3 and Aave.
https://medium.com/zelos-research
MIT License
32 stars 7 forks source link

Add 'price' field in the output JSON for add_liquidity and remove_liquidity action types #5

Closed vinayyak closed 11 months ago

vinayyak commented 11 months ago

Hi, when I run the backtest for a USDC/ETH pool, the output JSON & CSV shows me buy and sell transactions with a 'price' field in the block but this field is not present in the add or remove liquidity action type blocks in the JSON. Would be great if this could be added so it can be tracked what the price of ETH was when the add or remove liquidity action type was triggered. Tried doing this myself by editing the Actuator class but did not succeed.

32ethers commented 11 months ago

Acutally we have notify function in Strategy class, where you can get read and write actions when it was throwed.

So you can write a strategy like this

class DemoStrategy(Strategy):
    def initialize(self):
        new_trigger = AtTimeTrigger(time=datetime(2023, 8, 15, 12, 0, 0), do=self.add_liq)
        self.triggers.append(new_trigger)

    def add_liq(self, row_data: RowData):
        lp_market: UniLpMarket = self.markets[market_key]
        new_position, amount0_used, amount1_used, liquidity = lp_market.add_liquidity(1000, 4000)

    def notify(self, action: BaseAction):
        """
        When a new action(add/remove liquidity) is executed, you can be notified by this call back.
        """
        # add price to actions, so price will be kept in csv file. 
        action.price = "{:.3f}".format(actuator.token_prices.loc[action.timestamp][eth.name])
        pass

here action get a new attribute "price", as python pass object by reference, this attribute will be kept and write to csv

vinayyak commented 11 months ago

Hi, Thanks for your help and reply, tried out the notify function and it worked for JSON but the price did not show up in the CSV. I think it might be related to the save_result method from the Actuator Class:

    def save_result(self, path: str, account=True, actions=True) -> List[str]:
        """
        save back test result
        :param path: path to save
        :type path: str
        :param account: Save account status or not
        :type account: bool
        :param actions: Save actions or not
        :type actions: bool
        :return:
        :rtype:
        """
        if not self.__backtest_finished:
            raise DemeterError("Please run strategy first")
        file_name_head = "backtest-" + datetime.now().strftime('%Y%m%d-%H%M%S')
        if not os.path.exists(path):
            os.mkdir(path)
        file_list = []
        if account:
            file_name = os.path.join(path, file_name_head + ".account.csv")
            self._account_status_df.to_csv(file_name)
            file_list.append(file_name)
        if actions:
            # save pkl file to load again
            pkl_name = os.path.join(path, file_name_head + ".action.pkl")
            with open(pkl_name, "wb") as outfile1:
                pickle.dump(self._action_list, outfile1)
            # save json to read
            actions_json_str = orjson.dumps(self._action_list,
                                            option=orjson.OPT_INDENT_2,
                                            default=json_default)
            json_name = os.path.join(path, file_name_head + ".action.json")
            with open(json_name, "wb") as outfile:
                outfile.write(actions_json_str)

            file_list.append(json_name)
            file_list.append(pkl_name)

        self.logger.info("files have saved to", file_list)
        return file_list

Here, from my observation: I can see an Actions getting passed in the pickle and JSON files but not the CSV. Would be great if you could clarify my doubt. Thanks again for your help!

32ethers commented 11 months ago

Oops, actually csv and json are two different things.

csv keeps asset change. such as net_value, etc.

but json and pkl keeps all actions. such as add liquidity, collect. Action class has differenet attributes. so it would be a mass if kept in csv

If you want to keep price in account status list, just call market.get_account_status_dataframe()(some name like this, and easy to find), then you get a dataframe object.

Next look in to market.price, it's also a dataframe and have the same timestamp index to account_status_dataframe . so it's very simple to append the price column to account list dataframe

then you call dataframe.to_csv(), this csv will contain price

vinayyak commented 11 months ago

Yup, I was reading the Actuator code today again and also figured that the CSV was printing the account status and the JSON was printing the action list. Will implement the tips you have given to get the price into the DataFrame. Thanks for your help again @32ethers Great Library, Cheers!