blankly-finance / blankly

🚀 💸 Easily build, backtest and deploy your algo in just a few lines of code. Trade stocks, cryptos, and forex across exchanges w/ one package.
https://package.blankly.finance
GNU Lesser General Public License v3.0
2.03k stars 261 forks source link

market_order_target #224

Open femtotrader opened 1 year ago

femtotrader commented 1 year ago

Hello,

According to doc https://docs.blankly.finance/orders/order/ a market_order method exists but it could be a nice improvement to add market_order_target with target parameter (relative size of the position after the trade) being a float between -1 and 1. target = 1 means buy with 100% of portfolio value target = -1 means sell with 100% of portfolio value (ie sell shorting)

Kind regards

EmersonDove commented 1 year ago

Could definitely be an upgrade. I would recommend writing this utility though on your own because it's just one extra API call to size that portfolio. If you're interested in contributing, I would be happy to accept a contribution to the spot base class to have this functionality.

femtotrader commented 1 year ago

I tried to monkey patch the code using:

import blankly
from blankly.data.data_reader import PriceReader
from blankly import Strategy, StrategyState
from blankly.enums import Side
from blankly.exchanges.orders.market_order import MarketOrder

def init(symbol, state: StrategyState):
    print(symbol, state)
    state.variables["run_once"] = True

def market_order_target(interface,
                     symbol: str,
                     target: float) -> MarketOrder:
    """
    Used for buying or selling market orders
    Args:
        symbol: asset to buy or sell
        target: relative size of the position after the trade
    """
    pos_weight = interface.get_positions(symbol) / interface.cash
    buy_pos = (target - pos_weight) * interface.cash
    if buy_pos > 0:
        return interface.market_order(symbol, Side.BUY, buy_pos)
    elif buy_pos < 0:
        return interface.market_order(symbol, Side.SELL, -buy_pos)
    return None

def price_event(price, symbol, state: StrategyState):
    #print(price, symbol, state)
    if state.variables["run_once"]:
        # state.interface.market_order(symbol, side='buy', size=1.0)
        type(state.interface).market_order_target = market_order_target
        #market_order_target(state.interface, symbol, 1)
        state.interface.market_order_target(symbol, 1)

    if state.variables["run_once"]:
        state.variables["run_once"] = False

if __name__ == "__main__":

    # Run on the keyless exchange, starting at 100k
    exchange = blankly.KeylessExchange(price_reader=PriceReader('./XBTUSDT_1Min.csv', 'BTC-USD'))

    # Use our strategy helper
    strategy = Strategy(exchange)

    # Make the price event function above run every minute (60s)
    strategy.add_price_event(price_event, symbol='BTC-USD', resolution=60, init=init)

    # Backtest the strategy
    results = strategy.backtest(start_date=1576778778, end_date=1656633557, initial_values={'USD': 10000})

    print(results)

but I'm getting the following error:

AttributeError: 'PaperTradeInterface' object has no attribute 'get_positions'

Any idea what is wrong with my code ?

Kind regards

femtotrader commented 1 year ago

it doesn't fix the AttributeError mentioned previously but

pos_weight = interface.get_positions(symbol) / interface.cash

should be changed to

pos_weight = interface.get_positions(symbol) / portfolio_value(interface, quote_asset)

where portfolio_value is defined as

def portfolio_value(interface, quote_asset):
    portfolio_value = 0.0
    for base_asset, values in interface.account.items():
        if values["available"] != 0:
            if base_asset == quote_asset:
                portfolio_value += values["available"]
            else:
                price = interface.get_price(f"{base_asset}-{quote_asset}")
                portfolio_value += price * values["available"]
    return portfolio_value

maybe such a function which returns portfolio value should also be defined.

EmersonDove commented 1 year ago

This looks correct, however the .get_positions is only going to work for futures. I just add to the docs to clarify that. I would recommend running (for example) .get_account('BTC') instead which should give you a result that works inside of your function.

femtotrader commented 1 year ago

I wonder if a buildin portfolio_value function/method doesn't ever exist in blankly. I wonder also if in portfolio_value I shouldn't also consider "hold"

Here is WIP

import requests
import pandas as pd
import blankly
from blankly.data.data_reader import PriceReader
from blankly import Strategy, StrategyState
from blankly.enums import Side
from blankly.exchanges.orders.market_order import MarketOrder
from blankly import utils

def portfolio_value(interface, quote_asset):
    portfolio_value = 0.0
    for base_asset, account_values in interface.account.items():
        if account_values["available"] != 0:
            if base_asset == quote_asset:
                values = account_values["available"]
                portfolio_value += values
            else:
                symbol = f"{base_asset}-{quote_asset}"
                price = interface.get_price(symbol)
                print(f"price: {price}")
                values = price * account_values["available"]
                portfolio_value += values
    return portfolio_value

def market_order_target(interface,
                     symbol: str,
                     target: float) -> MarketOrder:
    quote_asset = utils.get_quote_asset(symbol)
    base_asset = utils.get_base_asset(symbol)
    pv = portfolio_value(interface, quote_asset)
    pos_weight = (interface.get_account(base_asset)["available"]* interface.get_price(symbol)) / pv
    buy_pos = (target - pos_weight) * pv
    price = interface.get_price(symbol)
    filter = interface.get_order_filter(symbol)
    increment = filter["market_order"]["base_increment"]
    precision = utils.increment_to_precision(increment)
    size = blankly.trunc(abs(buy_pos) / price, precision)
    if buy_pos > 0:
        if size >= filter["market_order"]["base_min_size"]:
            return interface.market_order(symbol, Side.BUY, size)
    elif buy_pos < 0:
        if size >= filter["market_order"]["base_min_size"]:
            return interface.market_order(symbol, Side.SELL, size)
    return None

def init(symbol, state: StrategyState):
    print(symbol, state)
    state.variables["run_once"] = True
    state.variables["runs"] = 0

def price_event(price, symbol, state: StrategyState):
    state.variables["runs"] += 1

    #print(price, symbol, state)
    if state.variables["run_once"]:
        print(state.base_asset)
        print(state.quote_asset)
        print(symbol)

        #state.interface.market_order(symbol, side='buy', size=1.0)
        type(state.interface).market_order_target = market_order_target
        ##market_order_target(state.interface, symbol, 1.0)

        state.variables["run_once"] = False

    if state.variables["runs"] == 100_000:
        target = 0.25
        print(f"target: {target}")
        state.interface.market_order_target(symbol, target)

    elif state.variables["runs"] == 200_000:
        target = 0.5
        print(f"target: {target}")
        state.interface.market_order_target(symbol, target)

    elif state.variables["runs"] == 300_000:
        target = 0.75
        print(f"target: {target}")
        state.interface.market_order_target(symbol, target)

    elif state.variables["runs"] == 400_000:
        target = 1.0
        print(f"target: {target}")
        state.interface.market_order_target(symbol, target)

if __name__ == "__main__":
    # This downloads an example CSV
    #data = requests.get(
    #    'https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/demo_data.csv?alt=media&token=acfa5c39-8f08-45dc-8be3-2033dc2b7b28').text
    #with open('./price_examples.csv', 'w') as file:
    #    file.write(data)

    # Run on the keyless exchange, starting at 100k
    #exchange = blankly.KeylessExchange(price_reader=PriceReader('./price_examples.csv', 'BTC-USD'))
    """
    df = pd.read_csv("XBTUSDT.csv", names=["time", "price", "volume"])
    df["time"] = pd.to_datetime(df["time"], unit="s")
    df = df.set_index("time")
    prices = df.resample("1Min")["price"].ohlc().fillna(method="ffill")
    volume = df.resample("1Min")["volume"].sum().fillna(value=0)
    volume.name = "volume"
    df_1Min = pd.concat([prices, volume], axis=1)
    df_1Min = df_1Min.reset_index()
    df_1Min["time"] = df_1Min["time"].map(pd.Timestamp.timestamp)
    df_1Min.to_csv("XBTUSDT_1Min.csv")
    """
    exchange = blankly.KeylessExchange(price_reader=PriceReader('./XBTUSDT_1Min.csv', 'BTC-USD'))

    # Use our strategy helper
    strategy = Strategy(exchange)

    # Make the price event function above run every minute (60s)
    strategy.add_price_event(price_event, symbol='BTC-USD', resolution=60, init=init)

    # Backtest the strategy
    #results = strategy.backtest(start_date=1588377600, end_date=1650067200, initial_values={'USD': 10000})
    results = strategy.backtest(start_date=1576778778, end_date=1656633557, initial_values={'USD': 10000})

    print(results)