Build a backtest and live framework

Creating a Backtesting Framework in Python

Basic Layout of the Backtesting Framework

Object-oriented classes:

  • Engine: it is the main class that will be used to run our backtest.
  • Strategy: this base class will serve as the building block for implementing the logic of our trading strategies.
  • Order: When buying or selling, we first create an order object. If the order is filled, we create a trade object.
  • Trade: The corresponding trade object will be created whenever an order is filled.

You might wonder why we are creating both Order and Trade classes. The reason is simple: it is standard practice for backtesting engines to assume that an order is created on close and filled on the next open. Doing so is good practice and will avoid look-ahead bias!

“””When buying or selling, we first create an order object. If the order is filled, we create a trade object.”””
“””Trade objects are created when an order is filled.”””

Implementing the Engine Class

import pandas as pd
from tqdm import tqdm

class Engine():
    def __init__(self, initial_cash=100_000):
        self.strategy = None
        self.cash = initial_cash
        self.data = None
        self.current_idx = None
        
    def add_data(self, data:pd.DataFrame):
        # Add OHLC data to the engine
        self.data = data
        
    def add_strategy(self, strategy):
        # Add a strategy to the engine
        self.strategy = strategy
    
    def run(self):
        # We need to preprocess a few things before running the backtest
        self.strategy.data = self.data
        
        for idx in tqdm(self.data.index):
            self.current_idx = idx
            self.strategy.current_idx = self.current_idx
            # fill orders from previus period
            self._fill_orders()
            
            # Run the strategy on the current bar
            self.strategy.on_bar()
            print(idx)
            
    def _fill_orders(self):
        # Fill orders from the previous period
        pass

Although incomplete, we now have something we can execute, so let’s go ahead and do that!

import yfinance as yf

data = yf.Ticker('AAPL').history(start='2020-01-01', end='2022-12-31', interval='1d')
e = Engine()
e.add_data(data)
e.add_strategy(Strategy())
e.run()

Implementing the Strategy Class

class Strategy():
    def __init__(self):
        self.current_idx = None
        self.data = None
        self.orders = []
        self.trades = []
    
    def buy(self,ticker,size=1):
        self.orders.append(
            Order(
                ticker = ticker,
                side = 'buy',
                size = size,
                idx = self.current_idx
            ))

    def sell(self,ticker,size=1):
        self.orders.append(
            Order(
                ticker = ticker,
                side = 'sell',
                size = -size,
                idx = self.current_idx
            ))
        
    @property
    def position_size(self):
        return sum([t.size for t in self.trades])
        
    def on_bar(self):
        """This method will be overriden by our strategies.
        """
        pass

Implementing the Order and Trade Classes

class Order():
    def __init__(self, ticker, size, side, idx):
        self.ticker = ticker
        self.side = side
        self.size = size
        self.type = 'market'
        self.idx = idx
        
class Trade():
    def __init__(self, ticker,side,size,price,type,idx):
        self.ticker = ticker
        self.side = side
        self.price = price
        self.size = size
        self.type = type
        self.idx = idx

    def __repr__(self):
        # Trade class string representation dunder method
        return f'<Trade: {self.idx} {self.ticker} {self.size}@{self.price}>'

Refining the Engine Class

We’ll now return to our Engine class and implement the _fill_orders() method.

Whenever there are orders to be filled, we will also check for the following conditions:

  • If we’re buying, our cash balance has to be large enough to cover the order.
  • If we are selling, we must have enough shares to cover the order.

In other words, our backtesting engine will be restricted to long-only strategies. I introduced this restriction to show how to incorporate such a feature, but it is by no means a requirement. If you want to test long-short strategies, you can safely comment out these lines of code. Even better, you could add an “allow_short_trading” boolean parameter to the engine class and let the end-user decide.

def _fill_orders(self):
    """this method fills buy and sell orders, creating new trade objects and adjusting the strategy's cash balance.
    Conditions for filling an order:
    - If we're buying, our cash balance has to be large enough to cover the order.
    - If we are selling, we have to have enough shares to cover the order.
    
    """
    for order in self.strategy.orders:
        can_fill = False
        if order.side == 'buy' and self.cash >= self.data.loc[self.current_idx]['Open'] * order.size:
                can_fill = True 
        elif order.side == 'sell' and self.strategy.position_size >= order.size:
                can_fill = True
        if can_fill:
            t = Trade(
                ticker = order.ticker,
                side = order.side,
                price= self.data.loc[self.current_idx]['Open'],
                size = order.size,
                type = order.type,
                idx = self.current_idx)

            self.strategy.trades.append(t)
            self.cash -= t.price * t.size
    self.strategy.orders = []

試試看

class BuyAndSellSwitch(Strategy):
    def on_bar(self):
        if self.position_size == 0:
            self.buy('AAPL', 1)
            print(self.current_idx,"buy")
        else:
            self.sell('AAPL', 1)
            print(self.current_idx,"sell")

data = yf.Ticker('AAPL').history(start='2022-12-01', end='2022-12-31', interval='1d')
e = Engine()
e.add_data(data)
e.add_strategy(BuyAndSellSwitch())
e.run()            

# We can check the list of trades that were executed:
# e.strategy.trades

Calculating the Total Return

# strategy.py

def _get_stats(self):
    metrics = {}
    total_return =100 * ((self.data.loc[self.current_idx]['Close'] * self.strategy.position_size + self.cash) / self.initial_cash -1)
    metrics['total_return'] = total_return
    return metrics

Sanity check: comparing results with Backtesting.py


Implementing Limit Orders

This feature impacts quite a few parts of our backtesting engine. Let’s try not to break everything!

1) Add buy_limit and sell_limit methods to our Strategy class

# strategy.py

def buy_limit(self,ticker,limit_price, size=1):
    self.orders.append(
        Order(
            ticker = ticker,
            side = 'buy',
            size = size,
            limit_price=limit_price,
            order_type='limit',
            idx = self.current_idx
        ))

def sell_limit(self,ticker,limit_price, size=1):
    self.orders.append(
        Order(
            ticker = ticker,
            side = 'sell',
            size = -size,
            limit_price=limit_price,
            order_type='limit',
            idx = self.current_idx
        ))

2) Add limit_price and order_type attributes to the Order Class

# order.py

class Order():
    def __init__(self, ticker, size, side, idx, limit_price=None, order_type='market'):
        ...
        self.type = order_type
        self.limit_price = limit_price

3) Update the _fill_orders() method

# engine.py

def _fill_orders(self):
    for order in self.strategy.orders:
        # FOR NOW, SET FILL PRICE TO EQUAL OPEN PRICE. THIS HOLDS TRUE FOR MARKET ORDERS
        fill_price = self.data.loc[self.current_idx]['Open']
        can_fill = False
        if order.side == 'buy' and self.cash >= self.data.loc[self.current_idx]['Open'] * order.size:
            if order.type == 'limit':
                # LIMIT BUY ORDERS ONLY GET FILLED IF THE LIMIT PRICE IS GREATER THAN OR EQUAL TO THE LOW PRICE
                if order.limit_price >= self.data.loc[self.current_idx]['Low']:
                    fill_price = order.limit_price
                    can_fill = True
                    print(self.current_idx, 'Buy Filled. ', "limit",order.limit_price," / low", self.data.loc[self.current_idx]['Low'])

                else:
                    print(self.current_idx,'Buy NOT filled. ', "limit",order.limit_price," / low", self.data.loc[self.current_idx]['Low'])
            else:        
                can_fill = True 
        elif order.side == 'sell' and self.strategy.position_size >= order.size:
            if order.type == 'limit':
                #LIMIT SELL ORDERS ONLY GET FILLED IF THE LIMIT PRICE IS LESS THAN OR EQUAL TO THE HIGH PRICE
                if order.limit_price <= self.data.loc[self.current_idx]['High']:
                    fill_price = order.limit_price
                    can_fill = True
                    print(self.current_idx,'Sell filled. ', "limit",order.limit_price," / high", self.data.loc[self.current_idx]['High'])

                else:
                    print(self.current_idx,'Sell NOT filled. ', "limit",order.limit_price," / high", self.data.loc[self.current_idx]['High'])
            else:
                can_fill = True
                
        if can_fill:
            t = Trade(
                ticker = order.ticker,
                side = order.side,
                price= fill_price,
                size = order.size,
                type = order.type,
                idx = self.current_idx)

            self.strategy.trades.append(t)
            self.cash -= t.price * t.size

    self.strategy.orders = []

4) Add a close property in the Strategy class to retrieve the latest close

# strategy.py

@property
def close(self):
    return self.data.loc[self.current_idx]['Close']

5) Testing the feature

# main.py

import yfinance as yf
class BuyAndSellSwitch(Strategy):
    def on_bar(self):
        if self.position_size == 0:
            limit_price = self.close * 0.995
            self.buy_limit('AAPL', size=100,limit_price=limit_price)
            print(self.current_idx,"buy")
        else:
            limit_price = self.close * 1.005
            self.sell_limit('AAPL', size=100,limit_price=limit_price)
            print(self.current_idx,"sell")
            
data = yf.Ticker('AAPL').history(start='2022-12-01', end='2022-12-31', interval='1d')
e = Engine()
e.add_data(data)
e.add_strategy(BuyAndSellSwitch())
e.run()   

5) Testing the feature

# main.py

import yfinance as yf
class BuyAndSellSwitch(Strategy):
    def on_bar(self):
        if self.position_size == 0:
            limit_price = self.close * 0.995
            self.buy_limit('AAPL', size=100,limit_price=limit_price)
            print(self.current_idx,"buy")
        else:
            limit_price = self.close * 1.005
            self.sell_limit('AAPL', size=100,limit_price=limit_price)
            print(self.current_idx,"sell")
            
data = yf.Ticker('AAPL').history(start='2022-12-01', end='2022-12-31', interval='1d')
e = Engine()
e.add_data(data)
e.add_strategy(BuyAndSellSwitch())
e.run()   

Adding Output Metrics

After each iteration in the run() method of the Engine class, add the cash holdings to the cash_series:

# engine.py

class Engine():
    def __init__(self, initial_cash=100_000):
        ...
        self.cash_series = {}
        self.stock_series = {}

1) Buy & Hold Benchmark

# engine.py

def _get_stats(self):
    ...
    # Buy & hold benchmark
    portfolio_bh = self.initial_cash / self.data.loc[self.data.index[0]]['Open'] * self.data.Close
    ...

2) Exposure to the Asset [%]

# engine.py

def _get_stats(self):
    ...
    # Create a dataframe with the cash and stock holdings at the end of each bar
    portfolio = pd.DataFrame({'stock':self.stock_series, 'cash':self.cash_series})
    # Add a third column with the total assets under managemet
    portfolio['total_aum'] = portfolio['stock'] + portfolio['cash']
    # Caclulate the total exposure to the asset as a percentage of our total holdings
    metrics['exposure_pct'] = ((portfolio['stock'] / portfolio['total_aum']) * 100).mean()
    ...

3) Annualized Returns

# Calculate annualized returns
p = portfolio.total_aum
metrics['returns_annualized'] = ((p.iloc[-1] / p.iloc[0]) ** (1 / ((p.index[-1] - p.index[0]).days / 365)) - 1) * 100
p_bh = portfolio_bh
metrics['returns_bh_annualized'] = ((p_bh.iloc[-1] / p_bh.iloc[0]) ** (1 / ((p_bh.index[-1] - p_bh.index[0]).days / 365)) - 1) * 100

4) Annualized Volatility

For this calculation, I’m assuming we’re using daily data of an asset that trades only during working days (i.e., stocks). If you’re trading cryptocurrencies, use 365 instead of 252.

# Annual Volatility
# self.trading_days = 252 # for stock
self.trading_days = 365 # for crypto
metrics['volatility_ann'] = p.pct_change().std() * np.sqrt(self.trading_days) * 100
metrics['volatility_bh_ann'] = p_bh.pct_change().std() * np.sqrt(self.trading_days) * 100

5) Sharpe Ratio

# Sharpe ratio
        self.risk_free_rate = 0
        metrics['sharpe_ratio'] = (metrics['returns_annualized'] - self.risk_free_rate) / metrics['volatility_ann']
        metrics['sharpe_ratio_bh'] = (metrics['returns_bh_annualized'] - self.risk_free_rate) / metrics['volatility_bh_ann']

6) Maximum Drawdown

    def _get_stats(self):
        ...
        metrics["max_drawdown"] = self.get_max_drawdown(self.data["Close"])
        return metrics
        
    def get_max_drawdown(self, close):
        # Maximum Drawdown
        roll_max = close.cummax()
        daily_drawdown = close / roll_max - 1.0
        max_daily_drawdown = daily_drawdown.cummin()
        return max_daily_drawdown.min() * 100

Implementing an SMA Crossover Strategy

Reference:
https://www.qmr.ai/building-a-backtesting-framework-in-python-from-scratch/
https://www.qmr.ai/building-a-backtesting-framework-in-python-part-ii/
https://www.qmr.ai/is-backtesting-accurate/

Note:
There are two types of backtester implementation:
1. for loop
2. event-driven

There are series of articles about event-driven backtester implementation here: https://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-I/

Lumibot

Supported broker:

  1. Alpaca
  2. Interactive Broker
  3. ccxt (Binance, … )

Trading Bot w/ Lumibot & Alpaca Part 1

Youtube: Building a LIVE Algorithmic Trading Bot with Python, Lumibot and Alpaca: A Step by Step Guide

# lumibot_buy_hold.py (backtesting)

from config import ALPACA_CONFIG
from datetime import datetime
from lumibot.backtesting import YahooDataBacktesting
from lumibot.brokers import Alpaca
from lumibot.strategies import Strategy
from lumibot.traders import Trader


class BuyHold(Strategy):

    def initialize(self):
        self.sleeptime = "1D"

    def on_trading_iteration(self):
        if self.first_iteration:
            symbol = "GOOG"
            price = self.get_last_price(symbol)
            quantity = self.cash // price
            order = self.create_order(symbol, quantity, "buy")
            self.submit_order(order)


if __name__ == "__main__":
    trade = False
    if trade:
        broker = Alpaca(ALPACA_CONFIG)
        strategy = BuyHold(broker=broker)
        trader = Trader()
        trader.add_strategy(strategy)
        trader.run_all()
    else:
        start = datetime(2022, 1, 1)
        end = datetime(2022, 12, 31)
        BuyHold.backtest(
            YahooDataBacktesting,
            start,
            end
        )

Trading Bot w/ Lumibot & Alpaca Part 2

Youtube: How to Build a Python Momentum Algorithm Trading Bot w/ Lumibot & Alpaca

Alpaca & Flask Part 1

Youtube: Python TradingViewCharts In Your Flask Application with Alpaca API

Alpaca & Flask Part 2

Youtube: Python Paper Trading Interface | Stock Trading with Alpaca and Flask

Binance – historical crypto trade data

來自外部數據提供商的歷史交易或報價水平數據可能很昂貴。 幸運的是,幣安提供了一個免費的數據源。 使用交易數據並自行匯總蠟燭通常可以提高您使用的數據集的質量,因為您不會混合來自不同交易所的價格。

下載頁面:https://binance.com/en/landing/data
下載網址:
https://data.binance.vision/?prefix=data/spot/daily/trades/
https://data.binance.vision/?prefix=data/futures/um/daily/trades/

  1. Spot Data
  2. USDⓈ-M Futures Data
  3. COIN-M Futures Data

Aggregating Trade data to OHLC with Pandas

import pandas as pd
df = pd.read_csv("ETHUSDT-trades-2022-09-30.csv",
                 name=["id","price","qty","quoteQty","time","makerBuy","bestPrice"])
df["time"] = pd.to_datetime(df["time"], unit="ms")
df.set_index("time", inplace=True)

# df.price.resample("1T")
df.price.resample("1T").agg({
    "open": "first",
    "high": "max",
    "low": "min",
    "close": "last"
})

參考:

Youtube: https://www.youtube.com/watch?v=ApdjC9aylnw

TradingView Pine Script 入門

金叉死叉與均線區域填充

重點說明:

  • 金叉:短期移動平均線由下方穿越長期移動平均線,表示要漲了 – ta.crossover()
  • 死叉:短期移動平均線由上方穿越長期移動平均線,表示要跌了- ta.corssunder()

在 Pine Script 編輯區,將滑鼠移到指令上方,按下 Ctrl + click 就會叫出 Pine Script 的說明文件。必須要會。
indicator(“… title …”, overlay=true) 將圖與 K 線圖畫在一起
plotchar() 畫文字或符號
color.new(#ff0000, 20) 前面的 #ff0000 表示 RGB 顏色,後面的 20 表示透明度 (0~100),數字越高越透明。

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/ 注释
// © blockplus

//@version=5
indicator("PINE教学2:金叉死叉与均线区域填充", overlay=true) // strategy/library

// 求均线
ma12 = ta.ema(close, 12)// Simple Moving Average
ma26 = ta.ema(close, 26)// Simple Moving Average

// 画均线
line1 = plot(ma12, color=color.red)
line2 = plot(ma26, color=color.blue)

// 金叉死叉信号
gold = ta.crossover(ma12, ma26) // ma12 > ma26 and ma12[1] < ma26[1] 等价于 ta.crossover(ma12, ma26)
dead = ta.crossunder(ma12, ma26) // ma12 < ma26 and ma12[1] > ma26[1] 等价于 ta.crossunder(ma12, ma26)

// 金叉死叉信号绘制
plotchar(gold, char="买", location=location.belowbar, color=color.green, size=size.tiny)
plotchar(dead, char="卖", location=location.abovebar, color=color.red, size=size.tiny)

// 填充均线背景
_color = ma12>ma26 ? color.new(#ff0000, 20) : color.new(#00ff00, 20) // 如果ma12>ma26 那么_color=color.green,否则_color=color.red
fill(line1, line2, color=_color)

編寫自己的函數

指標的分類

  1. 趨勢跟隨指標:適用於大盤上漲或下跌的情形,在區間震盪行情效果差
    均線、MACD線、DM1趨向系統、OBV能量潮等
  2. 震盪指標:在震盪區間市場能判斷出拐點,趨勢形成時給出的信號往往不可靠
    MACD柱、RSI、 CCI、 KD、 KDJ、 William %R等

plot( , , style=plot.style_histogram)

//@version=5
indicator("PINE教學4:常用技術指標MACD、RSI、CCI、BB、ATR", overlay=false)

// Plot MACD
// [macdLine, signalLine, histLine] = ta.macd(close, 12, 26, 9)
// plot(macdLine, color=color.blue)
// plot(signalLine, color=color.orange)
// plot(histLine, color=color.red, style=plot.style_histogram)

// Your MACD

macd(src, fastLen, slowLen, signalLen) =>
    fastMA = ta.ema(src, 12)
    slowMA = ta.ema(src, 26)
    macd = fastMA - slowMA
    signal = ta.ema(macd, 9)
    hist = macd - signal
    [macd, signal, hist]
    // plot(macd, color=color.blue)
    // plot(signal, color=color.orange)
    // plot(hist, color=color.red, style=plot.style_histogram)

[macdLine, signalLine, histLine] = ta.macd(close, 12, 26, 9)

alert("Pric222e (" + str.tostring(close) + ") crossed over MA (" + str.tostring(close) +  ").", alert.freq_once_per_bar)
alertcondition(ta.crossover(macdLine, signalLine), "CO", "CO")

plot(macdLine, color=color.blue)
plot(signalLine, color=color.orange)
plot(histLine, color=histLine>histLine[1]?color.green:color.red, style=plot.style_histogram)

# plot(ta.obv)

進行策略回測與 策略測試器 的使用

pyramid: 1 – number of orders is one

pyramid: 2 – number of orders is two, you can buy, and buy

策略平倉與止盈止損設定

平倉

strategy.close 可以實現部分平倉
strategy.cloase_all 全部平倉
strategy.exit : profit=10, loss=5 單位:最小波動刻度 (例如:ETH 報價 1200.2, 最小波動刻度就是 0.1)

止盈止損

//@version=5
strategy("PINE教學7: 策略出場雨止盈止損", overlay=true, margin_long=100, margin_short=100, default_qty_type=???)

fastLen = input.int(14, "快線長度", minval=2, maxval=200, step=5)
slowLen = input.int(28, "慢線長度", minval=4, maxval=400, step=5)
fastMA = ta.sma(close, fastLen)
slowMA = ta.sma(close, slowLen)

// 做多 
longCondition = ta.crossover(fastMA, slowMA)
if (longCondition)
    strategy.entry("Long", strategy.long)
    //strategy.close("Long", comment="部分平倉", qty_percent=50)
    //strategy.close_all("comment="全部平倉")

    //strategy.exit("exit", "Long", profit=10, loss=5)
    // 相等於
    //strategy.exit("exit", "Long", profit=1/syminfo.mintick, loss=0.5/syminfo.mintick)

    // 10%止盈, 5%止損
    strategy.exit("exit", "Long", limit=strategy.position_avg_price*1.1, stop=strategy.position_avg_price*0.95)

// 做空
shortCondition = ta.crossunder(fastMA, slowMA)
if (shortCondition)
    strategy.entry("Short", strategy.short)

Pine Script Indexing

出處:https://backtest-rookies.com/2018/03/23/tradingview-pine-script-indexing/

I

n pine script, every “variable” is actually a long list of stored values. After each new bar appears, the whole script is run again and a new value is added to each list. That means the “built-in” variablesopenhighlow and close are also just really long lists.

If we just reference close in our code, the most recent value in the list will be returned. So how do we access other items on the list? We Index! To index the list, we add square brackets [] at the end of the list name and give it a number like close[2]. Each number represents a different position in the list starting from zero. Zero is the current value and thus the following two statements return the same value: close and close[0].

參考:

[TradingView教学] 3. PINE语言入门:绘制均线、了解执行模型、变色均线PINE编程教程
策略平倉與止盈止損設定

ccxt 與 針對不同交易所的注意事項

create_exchange()

fetch_balance_unified()

When I try to fetch ftx margin, futures, and default balance it gives me the same results:
ccxt_ftx.fetch_balance(params={‘type’: ‘margin’})
ccxt_ftx.fetch_balance(params={‘type’: ‘future’})
ccxt_ftx.fetch_balance()

def fetch_balance_unified():
    if self.is_spot:
        balance = self.exch.fetch_balance()
    elif self.is_margin or self.is_future:
        balance = exchange.private_get_positions())

    return balance

針對不同交易所的注意事項

Freqtrade Exchange-specific Notes

Sample exchange configuration

A exchange configuration for “binance” would look as follows:

"exchange": {
    "name": "binance",
    "key": "your_exchange_key",
    "secret": "your_exchange_secret",
    "ccxt_config": {},
    "ccxt_async_config": {},
    // ... 
}

Setting rate limits

Usually, rate limits set by CCXT are reliable and work well. In case of problems related to rate-limits (usually DDOS Exceptions in your logs), it’s easy to change rateLimit settings to other values.

"exchange": {
    "name": "kraken",
    "key": "your_exchange_key",
    "secret": "your_exchange_secret",
    "ccxt_config": {"enableRateLimit": true},
    "ccxt_async_config": {
        "enableRateLimit": true,
        "rateLimit": 3100
    },

This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange. "rateLimit": 3100 defines a wait-event of 3.1s between each call. This can also be completely disabled by setting "enableRateLimit" to false.

Note

Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that "enableRateLimit" is enabled and increase the "rateLimit" parameter step by step.

Binance

Binance supports time_in_force.

Stoploss on Exchange

Binance supports stoploss_on_exchange and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. On futures, Binance supports both stop-limit as well as stop-market orders. You can use either "limit" or "market" in the order_types.stoploss configuration setting to decide which type to use.

Binance Blacklist recommendation

For Binance, it is suggested to add "BNB/<STAKE>" to your blacklist to avoid issues, unless you are willing to maintain enough extra BNB on the account or unless you’re willing to disable using BNB for fees. Binance accounts may use BNB for fees, and if a trade happens to be on BNB, further trades may consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.

Binance sites

Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized.

  • binance.com – International users. Use exchange id: binance.
  • binance.us – US based users. Use exchange id: binanceus.

Binance Futures

Binance has specific (unfortunately complex) Futures Trading Quantitative Rules which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders. Violating these rules will result in a trading restriction.

When trading on Binance Futures market, orderbook must be used because there is no price ticker data for futures.

  "entry_pricing": {
      "use_order_book": true,
      "order_book_top": 1,
      "check_depth_of_market": {
          "enabled": false,
          "bids_to_ask_delta": 1
      }
  },
  "exit_pricing": {
      "use_order_book": true,
      "order_book_top": 1
  },

Binance futures settings

Users will also have to have the futures-setting “Position Mode” set to “One-way Mode“, and “Asset Mode” set to “

“. These settings will be checked on startup, and freqtrade will show an error if this setting is wrong.

Binance futures settings

Freqtrade will not attempt to change these settings.

tmux – screen sessions remain active after ssh disconnect

tmux is a terminal multiplexer: it enables a number of terminals to be created, accessed, and controlled from a single screen. tmux may be detached from a screen and continue running in the background, then later reattached.

This release runs on OpenBSD, FreeBSD, NetBSD, Linux, macOS and Solaris. (Ubuntu)

Github: https://github.com/tmux/tmux

tmux is superior to screen for many reasons, here are just some examples:

  • Windows can be moved between session and even linked to multiple sessions
  • Windows can be split horizontally and vertically into panes
  • Support for UTF-8 and 256 colour terminals
  • Sessions can be controlled from the shell without the need to enter a session

Basic Functionality

Basic usage:

  • ssh into the remote machine
  • start tmux by typing tmux into the shell
  • start the process you want inside the started tmux session
  • leave/detach the tmux session by typing Ctrl+b and then d

You can now safely log off from the remote machine, your process will keep running inside tmux. When you come back again and want to check the status of your process you can use tmux attach to attach to your tmux session.

If you want to have multiple sessions running side-by-side, you should name each session using Ctrl+b and $.
You can get a list of the currently running sessions using tmux list-sessions or simply tmux ls.
Attach to a running session with command tmux attach-session -t <session-name>.

# session / window / pane

$ tmux new -s mysession
Ctrl+b %  split pane with horizontal layout
Ctrl+b "  split pane with vertical layout
Ctrl+b o  switch between panes
Ctrl+b c  create new window
Ctrl+b p  go to previous window
Ctrl+b n  go to next window
Ctrl+b d  detach

$ tmux ls
$ tmux attach-session -t mysession       # attach to mysession
$ tmux a -t mysession                    # attach to mysession


$ man tmux     # man page

Example:

$ tmux ls
no server running on /tmp/tmux-1001/default

$ tmux new -s APT                                     建立名稱為 APT 的 session
... Ctrl+b d (detach)                                 退出 APT session
[detached (from session APT)]
$ tmux ls
APT: 1 windows (created Sun Jan 29 15:40:10 2023)
$ tmux a                                              直接 attach 到第一個 session
[detached (from session APT)]

$ tmux new -s OP                                      建立名稱為 OP 的 session
... Ctrl+b d (detach)                                 退出 OP session
[detached (from session OP)]

$ tmux ls                                             ls 看看,目前有兩個 session
APT: 1 windows (created Sun Jan 29 15:40:10 2023)
OP: 1 windows (created Sun Jan 29 15:47:53 2023)

$ tmux a -t OP                                        attach 到 OP session
... Ctrl+b )                                          switch to next client        <--- 快速切換 session / window
... Ctrl+b (                                          switch to previous client    <--- 快速切換 session / window
[detached (from session APT)]

tmux Cheat Sheet & Quick Reference

參考:https://tmuxcheatsheet.com/

經典基本幣價數據分析

需要高中教統計學知識的最基本部分:mean, variance, standard deviation, correlation

import yfinance as yf
import matplotlib.pyplot as plt

assets = ['AAPL','MSFT','TSLA','MMM']
df = yf.download(assets, start='2020-01-01']
prices = df['Adj Close']

不應直接比較不同幣種的幣價

直接比較會產生無法理解或誤導的圖表。

prices.plot()

return = day 2 close / day 1 close – 1

returns = prices.pct_change()

這樣就可以比較了:在這段時間內,TSLA 遙遙領先 APPL。實際上 TSLA 成長了 8 倍。

day 3 cumulative return = (day 2 return + 1) * (day 3 return + 1) – 1 = day 3 close / day 1 close – 1

cum_ret = (returns + 1).cumprod() - 1
cum_ret.plot()
returns.std()

TSLA 的 volatility 大於 AAPL

TSLA 的 volatility 大於 BTC?

把 BTC 加進去一起比較吧!

注意:BTC 一年 365 天,天天都有交易資料。而 TSLA 一年大約有 252 天的交易資料。所以直接寄算出來的 standard deviation 不應該直接比較的。如果要將 BTC, TSLA 一齊比較,那應該要將 TSLA 的 standard deviation * sqrt (365/252) 才能比較。

assets = ['AAPL','MSFT','TSLA','MMM', 'BTC-USD']

將 BTC-USD 由 assets 中移出,只比較股票的部分

returns['AAPL'].hist(bins=50)
returns['AAPL'].plot()
fig, ax = plt.subplots(2,2, fig=(10,5))
ax[0,1].plot(returns['AAPL'])
ax[0,0]ax[0,1]
ax[1,0]ax[1,1]
counter = 0
for i in range(2):
    for j in range(2):
        ax[i, j].plot(returns[returns.columns[counter]])
        ax[i, j].set_title(returns.columns[counter])
        counter += 1

counter = 0
for i in range(2):
    for j in range(2):
        ax[i, j].hist(returns[returns.columns[counter]], bins=50)
        ax[i, j].set_title(returns.columns[counter])
        counter += 1

correlation

returns.corr()

import seaborn as sns
sns.headmap(returns.corr())

AAPL MSFT 高度相關: 0.8095

TSLA MMM 低度相關:0.1834

只比較加密貨幣

assets = ['BTC-USD','ETH-USD','BNB-USD','ADA-USD']

參考:

Youtube – Algovibes: Stock Market & Cryptocurrency Data Analysis with Python

正向合約和反向合約 (Linear contracts & Inverse contracts)

Linear Contracts are settled in quoted asset -> BIT/USDT : USDT
Inverse Contracts are settled in base Asset -> BIT/USD : BIT

在永續合約市場中,合約一般分為正向合約和反向合約。正向合約在加密市場中也稱為USDT本位合約、穩定幣合約,它以USDT為定價單位。而反向合約也稱為幣本位合約,與正向合約的最大區別在於,正向合約是以定價貨幣USDT來作為保證金,而反向合約則是以交易貨幣(如BTC等)來作為保證金。  

Inverse contracts

參考:如何用币本位合约进行套保

什么是套保

以下内容摘录自网络的百科信息:

“套期保值 (hedging),又称对冲贸易,是指交易人在买进(或卖出)实际货物的同时,在期货交易所卖出(或买进)同等数量的期货交易合同作为保值。它是一种为避免或减少价格发生不利变动的损失,而以期货交易临时替代实物交易的一种行为。”

在本文的语境中,套保将特指在币圈衍生品交易所内,用币本位合约/反向合约锁定保证金法币价值的操作。

什么是币本位合约/反向合约

币本位合约的学名叫做反向合约,inverse contracts。是由BitMEX交易所在2014年推出的革命性产品。传统世界的期货合约需要用法币作为保证金对标的进行交易。而反向合约创造性地用币作为保证金对标的即币本身进行交易。

反向合约的合约张数是以美元为单位的。

如果T(美元)是合约张数,P(美元/比特币)是标的价格,比特币合约的币本位价值的数学表达就是:V = – T / P

(1)(单位:美元 除以 美元/比特币 = 比特币)

考虑一个假设的情境对比1和2。价格分别为P1, P2,则价格发生位移之后,合约张数T(美元)是不变的,买入反向合约的币本位盈亏为:PNL_ LONG_BTC = V2-V1 = -T/P2 – (-T/P1) = T/P1 – T/P2 = (P2-P1) x T / P1 / P2

(2)美元本位盈亏为:PNL_ LONG_USD = PNL_BTC x P2 = (P2/P1-1) T = (P2-P1) x T / P1

(3)数学推导方便得出结论,要获得感性认知得换一个叙事性的角度去考虑这个问题。

买入T张反向合约,实质上的动作是卖出和做空T美元,换取标的T/P1数量的币。定义这个动作为“买入”反向合约,是为了和标的 即 币价上涨会让合约持有者获利这个角度去匹配一致。币价上涨会造成美元贬值,从而卖出和做空美元的合约持有者获利。

当价格从P1变为P2时,此前的标的可以买回美元P2 x T/P1

美元上的盈亏为买回的美元减去卖出的美元的数量,即P2 x T/P1 – T = (P2-P1) x T / P1

这部分美元的差价,用比特币结算给玩家,就是 (P2-P1) x T / P1 / P2

分别和上述公式(3)和(2)一致

如何进行套保

Binance – Position Limit (持倉限制)

什麼是 持倉限制?

為了維護系統的安全性和穩定性,我們的系統為不同的槓桿設置了最大倉位。
例如,當您交易BNBUSDT並選擇20倍時,您的最大持倉量為250,000 USDT。 更高的槓桿率導致更低的最高頭寸,反之亦然。

如何調整持倉限制

合約交易規則 – 槓桿與保證金 (leverage & margin)

參考:Trading Rules – Leverage & Margin
以 MATICBUSD 為例

TierPosition Bracket (Notional Value in BUSD)Max LeverageMaintenance Margin RateMaintenance Amount (BUSD)
10 – 25,00020x2.50%0
2225,000 – 100,00010x5.00%625
33100,000 – 250,0005x10.00%5,625
44250,000 – 1,000,0002x12.50%11,875
551,000,000 – 5,000,0001x50.00%386,875

幣安會根據你的交易紀錄,每月一號計算用戶的可調整大小。所以每月一號請到網站上看看是否可以調整。

參考:

Your Guide to Position Limit Adjustment
Trading Rules – Leverage & Margin

關於 爆倉 / 清算 / Liquidation 必須知道的事

A. 如何降低爆倉機率?

1. 關注 Margin Ratio (保證金比率) 數據

為了避免清算,您需要密切關注您的期貨保證金比率。 當您的保證金比率達到 100% 時,您的部分(如果不是全部)頭寸將被清算。

`margin ratio = maintenance margin / margin balance` — 不同於 Maintenance Margin Ratio (mmr) ?

所以當你的保證金餘額低於維持保證金率時,交易所將清算您的頭寸。

如果價格下跌,請確保您的期貨賬戶中有足夠的保證金餘額。 您的保證金餘額越高,強平價格越低。

您可以使用幣安合約清算價格計算器來計算增加錢包餘額將如何降低清算價格。

2. 使用止損功能(stop-loss)來限制和控制可能的損失

止損訂單是在達到給定止損價格後以指定價格執行的條件訂單。 一旦達到止損價,它將根據您的訂單參數以市場/限價買入或賣出。止損旨在限制投資者在不利走勢的頭寸上的損失。

例如,您將止損設置為入場價的 20%。假設您的掛單以 40,000 美元執行。 當價格從 40,000 美元下跌 -20% 時,將觸發止損訂單。

通過設置止損功能,您可以提前退出虧損頭寸,避免被強平。

3. 避免在虧損頭寸中積累更多合約

舉例說明。 假設您的錢包餘額為 500 USDT。 您以 50,000 美元的 20 倍槓桿建立了價值 1,000 USDT 的多頭 BTCUSDT 頭寸。 在此示例中,您的強平價格將為 25,100.40 美元。

現在假設 BTCUSDT 的價格下跌 10% 至 45,000 美元。 此時,您決定增加您的虧損頭寸,並以 45,000 美元的 20 倍槓桿建立另一個價值 1000 USDT 的 BTCUSDT 多頭頭寸。 調整到您的最新頭寸,強平價現在為 35,857.67 美元。

也就是說,向虧損頭寸添加更多合約將提高您整個頭寸的強平價格由 25,100.40 提高到 35,857.67美元,這樣就更容易被交易所清算了。

B. 清算價 和 破產價的秘密

破產價格是交易者的損失恰好等於他們存入的抵押品的價格。

清算價格是交易所開始自動關閉交易者頭寸的價格。 在還沒到破產價格之前,清算價格就會先達到。

1. 如何計算破產價格

破產價格是交易者的抵押品因虧損而被完全清空的價格。

讓我們來分析一下這樣一種情況:Alice 和 Bob 在分別存入 10,000 美元和 20,000 美元的抵押品後,希望通過槓桿工具以 10,000 美元的價格獲得比特幣倉位。 愛麗絲購買 4 份永續合約,鮑勃通過出售 4 份合約作為另一方。 假設 BTC/USDT 現在的交易價格為 11,000 美元。

#### Alice long ####

# Position details
collateral = 10_000
position_side = 1 # 1 = long, -1 = short
position_size = 4
avg_entry_px = 10_000
mark_px = 11_000
# Compute unrealized PNL for position
unrealized_pnl = position_size * position_side * (mark_px - avg_entry_px) = 4 * 1 * (11_000 - 10_000) = 4_000
# Compute current account value
total_account_value = collateral + unrealized_pnl = 10_000 + 4_000 = 14_000
# Compute bankruptcy price
bankruptcy_px = mark_px - position_side * (total_account_value / position_size) = 11_000 - 1 * (14_000 / 4) = 7_500

#### Bob short ####
# Position details
collateral = 20_000
position_side = -1 # 1 = long, -1 = short
position_size = 4
avg_entry_px = 10_000
mark_px = 11_000
# Compute unrealized PNL for position
unrealized_pnl = position_size * position_side * (mark_px - avg_entry_px) = 4 * -1 * (11_000 - 10_000) = -4_000
# Compute current account value
total_account_value = collateral + unrealized_pnl = 20_000 - 4_000 = 16_000
# Compute bankruptcy price
bankruptcy_px = mark_px - position_side * (total_account_value / position_size) = 11_000 + 1 * (16_000 / 4) = 15_000


2. 如何計算清算價格

清算價格是交易所因抵押品不足而觸發清算(即自動平倉交易者頭寸)的價格。 這是事情變得有點模糊的地方,大多數交易所都無助於您。

第一個要理解的概念是維持保證金率。 維持保證金率 (mmr) 是指交易者在被強行平倉之前必須維持多少抵押品,作為交易所在其頭寸歸零之前的緩衝。 為了保持倉位,它必須滿足以下條件:
total_account_value / position_notional >= mmr

那麼我們如何從這裡開始計算強平價格(即觸發強平的標記價格)?

先來導公式:

position_notional = position_size * mark_px
total_account_value = collateral + unrealized_pnl
unrealized_pnl = position_size * position_side * (mark_px - avg_entry_px)
collateral + position_size * position_side * (mark_px - avg_entry_px) / (position_size * mark_px) >= mmr
collateral + position_size * position_side * (mark_px - avg_entry_px) >= mmr * position_size * mark_px
collateral + (position_size * position_side * mark_px) - (position_size * position_side * avg_entry_px) >= mmr * position_size * mark_px
collateral - (position_size * position_side * avg_entry_px) >= (mmr * position_size * mark_px) - (position_size * position_side * mark_px)
collateral - (position_size * position_side * avg_entry_px) >= mark_px * position_size * (mmr - position_side)

mark_px = liquidation_px = (collateral - position_size * position_side * avg_entry_px) / (position_size * (mmr - position_side))

以下以維持保證金率為 3% 為例。

2.1 Alice 作多

延續之前的例子,Alice 已經用 10,000 美元的抵押品購買了價值 40,000 美元的 BTC/USDT(當時 BTC/USDT 的價格為 10,000 美元),現在是 4 倍槓桿。

### Alice long ####
# Position details
collateral = 10_000
position_side = 1 # 1 = long, -1 = short
position_size = 4
avg_entry_px = 10_000
mmr = 0.03
# Compute liquidation price
liquidation_px = (collateral - position_size * position_side * avg_entry_px) / (position_size * (mmr - position_side)) = (10_000 - 4 * 1 * 10_000) / (4 * (0.03 - 1)) = 7_731.96

如您所見,Alice 的清算價格為 7,731.96 美元,早於她的 7,500 美元破產價格(因為她做多)。

2.2 Bob 做空

如果您還記得的話,Bob 已經出售了價值 40,000 美元的 BTC/USDT(當時 BTC/USDT 的價格為 10,000 美元)和 20,000 美元的抵押品,現在是 2 倍槓桿。

#### Bot short ####
# Position details
collateral = 20_000
position_side = -1 # 1 = long, -1 = short
position_size = 4
avg_entry_px = 10_000
mmr = 0.03
# Compute liquidation price
liquidation_px = (collateral - position_size * position_side * avg_entry_px) / (position_size * (mmr - position_side)) = (20_000 - 4 * -1 * 10_000) / (4 * (0.03 + 1)) = 14_563.11

如您所見,鮑勃的清算價格為 14,563.11 美元,早於他的破產價格 15,000 美元(因為他做空)。


3. 關於高槓桿的隱藏成本

破產和清算價格是交易者在交易永續合約時需要注意的兩個最重要的價格水平。 如上所示,您的抵押品、頭寸規模、頭寸方向、平均入場價格和維持保證金比率會影響這些價格相對於產品當前標記價格的位置。

這些價格的一個關鍵要點是它們揭示了您為高槓桿頭寸支付的隱性成本。 如果您使用上面的公式,您可能會注意到,隨著您提高槓桿率,維持保證金在您擁有的初始保證金緩衝中所佔的比例越來越大。

參考:

How to Reduce Your Chances of Getting Liquidated
Medium – Liquidation and bankruptcy prices: under the hood
JupyterLab – Liquidation and bankruptcy prices: under the hood