All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.roboquant.ta.BettingAgainstBetaPolicy.kt Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
/*
 * Copyright 2020-2023 Neural Layer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.roboquant.ta

import org.hipparchus.stat.correlation.Covariance
import org.roboquant.brokers.Account
import org.roboquant.brokers.Position
import org.roboquant.brokers.diff
import org.roboquant.common.*
import org.roboquant.feeds.Event
import org.roboquant.orders.MarketOrder
import org.roboquant.orders.Order
import org.roboquant.policies.BasePolicy
import org.roboquant.strategies.Signal
import java.time.Instant
import kotlin.math.min

/**
 * Betting against Beta (BaB) is a strategy based on the premise that high beta stocks might be overvalued and
 * low beta stocks undervalued. So this strategy goes long on low beta stocks and short on high beta stocks.
 *
 * It will then hold these positions for a number of days before re-evaluating the strategy. After re-evaluation, the
 * strategy will then generate the market orders required to achieve the desired new portfolio composition.
 *
 * Since this strategy controls the complete portfolio and not just generates signals, it is implemented as a policy
 * and not a strategy. It doesn't use leverage or buying power, when re-balancing it just re-balances the total equity
 * of the account across the long and short positions.
 *
 * > Betting against Beta was first described in the Journal of Financial Economics
 *
 * @property market the asset presenting the market
 * @property holdingPeriod the holding period, default is 20.days
 * @property maxPositions the maximum number of open positions
 * @property windowSize the windowSize, default is 120
 *
 * @constructor Create new instance of Betting Against Beta
 */
open class BettingAgainstBetaPolicy(
    assets: Collection,
    private val market: Asset,
    private val holdingPeriod: TradingPeriod = 20.days,
    private val maxPositions: Int = 20,
    private val windowSize: Int = 120,
) : BasePolicy() {

    private var rebalanceDate = Instant.MIN

    private val data = mutableMapOf()

    init {
        require(market in assets) { "The selected market asset $market also has to be part of all assets" }
    }

    /**
     * Calculate the betas for the assets. If a beta cannot be calculated, for example due to missing data,
     * it will not be included in the returned result.
     *
     * @return
     */
    private fun calculateBetas(): List> {
        val betas = mutableListOf>()
        val x = data.getValue(market).toDoubleArray().returns()
        for ((asset, data2) in data) {
            if (asset != market) {
                val y = data2.toDoubleArray().returns()
                val beta = Covariance().covariance(x, y)
                if (!beta.isNaN()) betas.add(Pair(asset, beta))
            }
        }
        // Sort the list in place by beta value, low to high
        betas.sortBy { it.second }
        return betas
    }


    /**
     * Re-balance the portfolio based on the calculated betas.
     *
     * @param betas
     * @param account
     * @return
     */
    private fun rebalance(betas: List>, account: Account, event: Event): List {
        // maximum number of short and long assets we want to have in portfolio. Since there cannot be overlap,
        // the maximum number is always equal or smaller than half.
        val max = min(betas.size / 2, maxPositions / 2)

        // exposure per position.
        val exposure = account.equity.convert(account.baseCurrency, time = event.time) / (max * 2)

        fun getPosition(asset: Asset, price: Double, direction: Int): Position? {
            val assetAmount = exposure.convert(asset.currency, event.time).value
            val size = asset.contractSize(assetAmount, price) * direction
            return if (!size.iszero) Position(asset, size) else null
        }

        val targetPortfolio = mutableListOf()
        // Generate the long positions assets with a low beta
        betas.subList(0, max).forEach { (asset, _) ->
            val price = event.getPrice(asset)
            if (price != null) {
                val position = getPosition(asset, price, 1)
                targetPortfolio.addNotNull(position)
            }
        }

        // Generate the short positions for assets with a high beta
        betas.reversed().subList(0, max).forEach { (asset, _) ->
            val price = event.getPrice(asset)
            if (price != null) {
                val position = getPosition(asset, price, -1)
                targetPortfolio.addNotNull(position)
            }
        }

        // Get the difference of target portfolio state and the current one
        val diff = account.positions.diff(targetPortfolio)

        // Transform difference into Orders
        return diff.map { createOrder(it.key, it.value, account, event) }.filterNotNull()
    }

    /**
     * Override this method if you want to change the default creation of a [MarketOrder] with a different
     * order type like LimitOrders. Return null if you don't want to create an order for a certain asset.
     */
    open fun createOrder(asset: Asset, size: Size, account: Account, event: Event): Order? {
        return MarketOrder(asset, size)
    }

    /**
     * Create zero or more orders based on the received signals.
     *
     * @param signals
     * @param account the data at a point in time (as generated by the universe)
     * @return
     */
    override fun act(signals: List, account: Account, event: Event): List {

        // First we update the buffers
        data.addAll(event, windowSize, "DEFAULT")

        // Check if it is time to re-balance the portfolio
        if (event.time >= rebalanceDate && data[market]!!.isFull()) {
            val betas = calculateBetas()

            // Update the re-balance date
            rebalanceDate = event.time + holdingPeriod
            return rebalance(betas, account, event)
        }
        return emptyList()
    }

    /**
     * Clear any state
     */
    override fun reset() {
        data.clear()
        rebalanceDate = Instant.MIN
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy