"C ++ versión de OKEX estrategia de cobertura de contratos futuros" que le lleva a través de la estrategia cuantitativa hardcore

El autor:La bondad, Creado: 2019-08-29 16:05:07, Actualizado: 2023-11-07 20:53:27

img

Hablando de estrategias de cobertura, hay varios tipos, diversas combinaciones e ideas diversas en varios mercados. Exploramos las ideas y conceptos de diseño de la estrategia de cobertura desde la cobertura intertemporal más clásica. Hoy en día, el mercado de criptomonedas es mucho más activo que al principio, y también hay muchos intercambios de contratos de futuros que ofrecen muchas oportunidades para la cobertura de arbitraje.

Principio de la estrategia

¿Por qué la estrategia es algo hardcore porque la estrategia está escrita en C++ y la lectura de la estrategia es un poco más difícil? Pero no impide que los lectores aprendan la esencia de este diseño e ideas de estrategia. La lógica de la estrategia es relativamente simple, la longitud del código es moderada, solo 500 líneas. En términos de adquisición de datos de mercado, a diferencia de las otras estrategias que utilizan la interfaz rest. Esta estrategia utiliza la interfaz websocket para aceptar cotizaciones del mercado de divisas.

En términos de diseño, la estructura de la estrategia es razonable, el grado de acoplamiento del código es muy bajo, y es conveniente para expandir o optimizar. La lógica es clara, y tal diseño no solo es fácil de entender. Como material de enseñanza, aprender el diseño de esta estrategia también es un buen ejemplo.

  • Esparcimiento positivo, vender contratos a corto plazo, comprar contratos recientes largos.
  • Spread negativo, compra de contratos a largo plazo, venta corta de contratos recientes.

Después de comprender los principios básicos, el resto es cómo la estrategia activa la posición de apertura de la cobertura, cómo cerrar la posición, cómo agregar posiciones, método de control de posición total y otros detalles de procesamiento de la estrategia.

La estrategia de cobertura se refiere principalmente a la fluctuación de la diferencia de precio del objeto (The Spread) y su regresión.

Esto trae incertidumbre sobre las ganancias y pérdidas de cobertura, pero el riesgo sigue siendo mucho menor que la tendencia unilateral. para las diversas optimizaciones de la estrategia intertemporal, podemos elegir comenzar desde el nivel de control de la posición y la condición de activación de apertura y cierre. por ejemplo, podemos usar el clásico Indicador de banda de Bollinger para determinar la fluctuación de precios. Debido al diseño razonable y al bajo grado de acoplamiento, esta estrategia se puede modificar fácilmente en la Estrategia de cobertura intertemporal del índice de Bollinger

Análisis del código de estrategia

Mirando el código en su totalidad, se puede concluir que el código está dividido aproximadamente en cuatro partes.

  1. Enumerar las definiciones de valores, definir algunos valores de estado, y utilizar para marcar estados. Algunas funciones funcionales que no están relacionadas con la estrategia, tales como funciones de codificación de url, funciones de conversión de tiempo, etc., no tienen relación con la lógica de la estrategia, sólo para el procesamiento de datos.

  2. Clase generadora de datos de línea K: la estrategia está impulsada por los datos de línea K generados por el objeto de clase generadora.

  3. Clase de cobertura: los objetos de esta clase pueden realizar una lógica comercial específica, operaciones de cobertura y procesar detalles de la estrategia.

  4. La función principal de la estrategia, que es la función main. La función principal es la función de entrada de la estrategia. El bucle principal se ejecuta dentro de esta función. Además, esta función también realiza una operación importante, es decir, acceder a la interfaz de websocket del intercambio y obtener los datos de mercado de ticks crudos empujados como el generador de datos de línea K.

A través de la comprensión general del código de estrategia, podemos aprender gradualmente los diversos aspectos de la estrategia, y luego estudiar el diseño, las ideas y las habilidades de la estrategia.

  • Definición de valores de enumeración, otras funciones de función
  1. tipo enumeradoStateDeclaración
enum State {                    // Enum type defines some states
    STATE_NA,                   // Abnormal state
    STATE_IDLE,                 // idle
    STATE_HOLD_LONG,            // holding long positions
    STATE_HOLD_SHORT,           // holding short positions
};

Debido a que algunas funciones en el código devuelven un estado, estos estados se definen en el tipo de enumeraciónState.

Viendo queSTATE_NAaparece en el código es anormal, ySTATE_IDLEestá inactivo, es decir, el estado de la operación puede cubrirse.STATE_HOLD_LONGes el estado en el que se mantiene la posición de cobertura positiva.STATE_HOLD_SHORTEs el estado en el que se mantiene la posición de cobertura negativa.

  1. La sustitución de cadenas, no llamada en esta estrategia, es una función de utilidad alternativa, que se ocupa principalmente de cadenas.
string replace(string s, const string from, const string& to)
  1. Una función para convertir a caracteres hexadecimalestoHex
inline unsigned char toHex(unsigned char x)
  1. Manejo de funciones codificadas de url
std::string urlencode(const std::string& str)
  1. Una función de conversión de tiempo que convierte el tiempo en formato de cadena a una marca de tiempo.
uint64_t _Time(string &s)
  • Clase de generador de datos de línea K
class BarFeeder { // K line data generator class
    public:
        BarFeeder(int period) : _period(period) { // constructor with argument "period" period, initialized in initialization list
            _rs.Valid = true; // Initialize the "Valid" property of the K-line data in the constructor body.
        }

        void feed(double price, chart *c=nullptr, int chartIdx=0) { // input data, "nullptr" null pointer type, "chartIdx" index default parameter is 0
            uint64_t epoch = uint64_t(Unix() / _period) * _period * 1000; // The second-level timestamp removes the incomplete time period (incomplete _period seconds) and is converted to a millisecond timestamp.
            bool newBar = false; // mark the tag variable of the new K line Bar
            if (_rs.size() == 0 || _rs[_rs.size()-1].Time < epoch) { // if the K line data is 0 in length. Or the last bar's timestamp is less than epoch (the last bar of the K line is more than the current most recent cycle timestamp)
                record r; // declare a K line bar structure
                r.Time = epoch; // construct the K line bar of the current cycle
                r.Open = r.High = r.Low = r.close = price; // Initialize the property
                _rs.push_back(r); // K line bar is pressed into the K line data structure
                if (_rs.size() > 2000) { // if the K-line data structure length exceeds 2000, the oldest data is removed.
                    _rs.erase(_rs.begin());
                }
                newBar = true; // tag
            } else { // In other cases, it is not the case of a new bar.
                record &r = _rs[_rs.size() - 1]; // Reference the data of the last bar in the data.
                r.High = max(r.High, price); // The highest price update operation for the referenced data.
                r.Low = min(r.Low, price); // The lowest price update operation for the referenced data.
                r.close = price; // Update the closing price of the referenced data.
            }
    
            auto bar = _rs[_rs.size()-1]; // Take the last column data and assign it to the bar variable
            json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.close}; // construct a json type data
            if (c != nullptr) { // The chart object pointer is not equal to the null pointer, do the following.
               if (newBar) { // judge if the new Bar appears
                    c->add(chartIdx, point); // call the chart object member function add to insert data into the chart object (new k line bar)
                    c->reset(1000); // retain only 1000 bar of data
                } else {
                    c->add(chartIdx, point, -1); // Otherwise update (not new bar), this point (update this bar).
                }
            }
        }
        records & get() { // member function, method for getting K line data.
            Return _rs; // Returns the object's private variable _rs . (ie generated K-line data)
        }
    private:
        int _period;
        records _rs;
};

Esta clase es principalmente responsable de procesar los datos de tick adquiridos en una línea K de diferencia para impulsar la lógica de cobertura de la estrategia.

Algunos lectores pueden tener preguntas, ¿por qué usar datos de tick? ¿Por qué construir un generador de datos de línea K como este? ¿No es bueno usar datos de línea K directamente? Este tipo de pregunta se ha emitido en tres ráfagas. Cuando escribí algunas estrategias de cobertura, también hice un alboroto. Encontré la respuesta cuando escribí la estrategia de cobertura de Bollinger.

Los datos de la línea K de la diferencia entre los dos contratos son las estadísticas de cambio de precio de la diferencia en un cierto período. Por lo tanto, no es posible simplemente tomar los datos de la línea K de cada uno de los dos contratos para la resta y calcular la diferencia de cada dato en cada barra de la línea K. El error más obvio es, por ejemplo, el precio más alto y el precio más bajo de dos contratos, no necesariamente al mismo tiempo. Por lo tanto, el valor restado no tiene mucho sentido.

Por lo tanto, necesitamos usar datos de tick en tiempo real para calcular la diferencia en tiempo real y calcular el cambio de precio en un cierto período en tiempo real (es decir, el precio más alto, más bajo, abierto y cerrado en la columna de la línea K).

  • Clase de cobertura
class Hedge { // Hedging class, the main logic of the strategy.
  public:
    Hedge() { // constructor
        ...
    };
    
    State getState(string &symbolA, depth &depthA, string &symbolB, depth &depthB) { // Get state, parameters: contract A name, contract A depth data, contract B name, contract B depth data
        
        ...
    }
    bool Loop(string &symbolA, depth &depthA, string &symbolB, depth &depthB, string extra="") { // Opening and closing position main logic
        
        ...
    }

  private:
    vector<double> _addArr; // Hedging adding position list
    string _state_desc[4] = {"NA", "IDLE", "LONG", "SHORT"}; // Status value Description
    int _countOpen = 0; // number of opening positions
    int _countcover = 0; // number of closing positions
    int _lastcache = 0; //
    int _hedgecount = 0; // number of hedging
    int _loopcount = 0; // loop count (cycle count)
    double _holdPrice = 0; // holding position price
    BarFeeder _feederA = BarFeeder(DPeriod); // A contract Quote K line generator
    BarFeeder _feederB = BarFeeder(DPeriod); // B contract Quote K line generator
    State _st = STATE_NA; // Hedging type Object Hedging position status
    string _cfgStr; // chart configuration string
    double _holdAmount = 0; // holding position amount
    bool _iscover = false; // the tag of whether to close the position
    bool _needcheckOrder = true; // Set whether to check the order
    chart _c = chart(""); // chart object and initialize
};

Debido a que el código es relativamente largo, algunas partes se omiten, esto es principalmente mostrar la estructura de esta clase de cobertura, el constructor de la función de cobertura se omite, principalmente para el propósito de la inicialización del objeto.

- ¿ Qué pasa?

Esta función se ocupa principalmente de la inspección de órdenes, la cancelación de órdenes, la detección de posiciones, el equilibrio de posiciones, etc. Debido a que en el proceso de operaciones de cobertura, es imposible evitar una sola etapa (es decir, un contrato se ejecuta, otro no lo es), si el examen se realiza en la lógica de la orden de colocación, y luego el procesamiento de la operación de reenvío de órdenes u operación de posición de cierre, la lógica de estrategia será caótica.

Así que al diseñar esta parte, tomé otra idea. si la operación de cobertura se activa, siempre y cuando la orden se coloca una vez, independientemente de si hay una cobertura de una sola pierna, el valor predeterminado es que la cobertura es exitosa, y luego el saldo de la posición se detecta en elgetStateLa función y la lógica para procesar el saldo se tratarán de forma independiente.

El bucle

La lógica de negociación de la estrategia se encapsula en esta función, en la quegetStateSe llama, y el objeto generador de datos de línea K se utiliza para generar los datos de línea K de la diferencia (la propagación), y se realiza el juicio de apertura, cierre y adición de la lógica de posición.

  • Función principal de la estrategia
void main() {

    ...
    
    string realSymbolA = exchange.SetcontractType(symbolA)["instrument"]; // Get the A contract (this_week / next_week / quarter ), the real contract ID corresponding to the week, next week, and quarter of the OKEX futures contract.
    string realSymbolB = exchange.SetcontractType(symbolB)["instrument"]; // ...
    
    string qs = urlencode(json({{"op", "subscribe"}, {"args", {"futures/depth5:" + realSymbolA, "futures/depth5:" + realSymbolB}}}).dump()) ; // jSON encoding, url encoding for the parameters to be passed on the ws interface
    Log("try connect to websocket"); // Print the information of the connection WS interface.
    auto ws = Dial("wss://real.okex.com:10442/ws/v3|compress=gzip_raw&mode=recv&reconnect=true&payload="+qs); // call the FMZ API "Dial" function to acess the WS interface of OKEX Futures
    Log("connect to websocket sucess");
    
    depth depthA, depthB; // Declare two variables of the depth data structure to store the depth data of the A contract and the B contract
    auto filldepth = [](json &data, depth &d) { // construct the code for the depth data with the json data returned by the interface.
        d.Valid = true;
        d.Asks.clear();
        d.Asks.push_back({atof(string(data["asks"][0][0]).c_str()), atof(string(data["asks"][0][1]).c_str( ))});
        d.Bids.clear();
        d.Bids.push_back({atof(string(data["bids"][0][0]).c_str()), atof(string(data["bids"][0][1]).c_str( ))});
    };
    string timeA; // time string A
    string timeB; // time string B
    while (true) {
        auto buf = ws.read(); // Read the data pushed by the WS interface
        
        ...
        
}

Después de que la estrategia se inicia, se ejecuta desde la función principal. En la inicialización de la función principal, la estrategia se suscribe al mercado de ticks de la interfaz del websocket. El trabajo principal de la función principal es construir un bucle principal que reciba continuamente las cotizaciones de ticks empujadas por la interfaz del websocket de la bolsa, y luego llama a la función miembro del objeto de la clase de cobertura: función de bucle.

Un punto a tener en cuenta es que el mercado de ticks mencionado anteriormente es en realidad la interfaz de datos de profundidad fina de los pedidos de suscripción, que es los datos del libro de pedidos para cada archivo. Sin embargo, la estrategia solo utiliza el primer archivo de datos, de hecho, es casi igual a los datos del mercado de ticks. La estrategia no utiliza los datos de otros archivos, ni utiliza el valor del pedido del primer archivo.

Echa un vistazo más de cerca a cómo la estrategia se suscribe a los datos de la interfaz websocket y cómo está configurado.

string qs = urlencode(json({{"op", "subscribe"}, {"args", {"futures/depth5:" + realSymbolA, "futures/depth5:" + realSymbolB}}}).dump());    
Log("try connect to websocket");                                                                                                            
auto ws = Dial("wss://real.okex.com:10442/ws/v3|compress=gzip_raw&mode=recv&reconnect=true&payload="+qs);     
Log("connect to websocket sucess");

En primer lugar, la codificación de url del mensaje de suscripción json parámetro pasado por la interfaz suscrita, es decir, el valor de lapayloadEntonces un paso importante es llamar a la función de interfaz API de la plataforma FMZ QuantDialLa funciónDialAquí hacemos algunas configuraciones, dejemos que el objeto de control de conexión de websocket ws que se va a crear tenga reconexión automática de desconexión (el mensaje de suscripción todavía utiliza el valorqsla cadena de lapayloadParámetro), para lograr esta función, es necesario añadir configuración en la cadena de parámetros de laDial function.

El comienzo de laDialel parámetro de función es el siguiente:

wss://real.okex.com:10442/ws/v3

esta es la dirección de la interfaz de websocket a la que se necesita acceder, y está separada por you.

compress=gzip_raw&mode=recv&reconnect=true&payload="+qsson todos los parámetros de configuración.

Nombre del parámetro Descripción
comprimir comprimir es el modo de compresión, la interfaz de OKEX websocket utiliza gzip_raw de esta manera, por lo que está configurado para gzip_raw
el modo El modo es el modo, opcional dual, enviar y recv de tres tipos. Dual es bidireccional, enviando datos comprimidos y recibiendo datos comprimidos. Enviar es enviar datos comprimidos. Recv recibe los datos comprimidos y los descomprime localmente.
vuelve a conectar Reconectar está configurado para reconectar, reconnect=true para habilitar la reconexión, no se vuelve a conectar por defecto.
Carga útil La carga útil es un mensaje de suscripción que debe ser enviado cuando ws se vuelve a conectar.

Después de esta configuración, incluso si la conexión websocket se desconecta, el sistema subyacente de la plataforma de negociación FMZ Quant del sistema docker se volverá a conectar automáticamente y obtendrá los últimos datos del mercado a tiempo.

Captura cada fluctuación de precios y rápidamente captura la cobertura adecuada.

  • Control de posición

El control de posiciones se controla utilizando una relación de posiciones de cobertura similar a la serie Fibonaci.

for (int i = 0; i < AddMax + 1; i++) { // construct a data structure that controls the number of scalping, similar to the ratio of the Bofinac sequence to the number of hedges.
     if (_addArr.size() < 2) { // The first two added positions are changed as: double the number of hedges
         _addArr.push_back((i+1)*OpenAmount);
     }
     _addArr.push_back(_addArr[_addArr.size()-1] + _addArr[_addArr.size()-2]); // The last two adding positions are added together, and the current position quantity is calculated and stored in the "_addArr" data structure.
}

Se puede ver que el número de posiciones adicionales añadidas cada vez es la suma de las dos últimas posiciones.

Dicho control de posición puede realizar la mayor diferencia, el aumento relativo de la cobertura de arbitraje y la dispersión de la posición, de modo que se capte la pequeña posición de la pequeña fluctuación de precios, y la gran posición de fluctuación de precios se incremente adecuadamente.

  • Posición de cierre: stop loss y take profit

Diferencia de pérdida fija y diferencia de ganancia fija.

Cuando la diferencia de posición alcanza la posición take profit y la posición stop loss, se realizan la posición take profit y la posición stop loss.

  • El diseño de la entrada y salida del mercado

El período del parámetroNPeriodel control proporciona cierto control dinámico sobre la posición de apertura y cierre de la estrategia.

  • Diagrama de la estrategia

La estrategia genera automáticamente un gráfico de líneas K para marcar la información relevante de la transacción.

C ++ estrategia de dibujo de gráficos personalizados operación también es muy simple._cfgStrpara configurar el objeto del gráfico_c, _cCuando el miembro privado se inicializa, elchartobjeto construido por la plataforma FMZ Quant función de interfaz API de gráfico personalizado se llama.

_cfgStr = R"EOF(
[{
"extension": { "layout": "single", "col": 6, "height": "500px"},
"rangeSelector": {"enabled": false},
"tooltip": {"xDateformat": "%Y-%m-%d %H:%M:%S, %A"},
"plotOptions": {"candlestick": {"color": "#d75442", "upcolor": "#6ba583"}},
"chart":{"type":"line"},
"title":{"text":"Spread Long"},
"xAxis":{"title":{"text":"Date"}},
"series":[
    {"type":"candlestick", "name":"Long Spread","data":[], "id":"dataseriesA"},
    {"type":"flags","data":[], "onSeries": "dataseriesA"}
    ]
},
{
"extension": { "layout": "single", "col": 6, "height": "500px"},
"rangeSelector": {"enabled": false},
"tooltip": {"xDateformat": "%Y-%m-%d %H:%M:%S, %A"},
"plotOptions": {"candlestick": {"color": "#d75442", "upcolor": "#6ba583"}},
"chart":{"type":"line"},
"title":{"text":"Spread Short"},
"xAxis":{"title":{"text":"Date"}},
"series":[
    {"type":"candlestick", "name":"Long Spread","data":[], "id":"dataseriesA"},
    {"type":"flags","data":[], "onSeries": "dataseriesA"}
    ]
}
]
)EOF";
_c.update(_cfgStr);                 // Update chart objects with chart configuration
_c.reset();                         // Reset chart data。
call _c.update(_cfgStr); Use _cfgStr to configure to the chart object.

call _c.reset(); to reset the chart data.

Cuando el código de estrategia necesita insertar datos en el gráfico, también llama a la función miembro del gráfico._cObjeto directamente, o pasa la referencia de_ccomo un parámetro, y luego llama a la función miembro objeto (método) de_cpara actualizar los datos del gráfico y insertar la operación.

Por ejemplo:

_c.add(chartIdx, {{"x", UnixNano()/1000000}, {"title", action},  {"text", format("diff: %f", opPrice)}, {"color", color}});

Después de hacer el pedido, marque el gráfico de la línea K.

Como sigue, al dibujar una línea K, una referencia al objeto del gráfico_cse pasa como un parámetro cuando se llama a la función miembrofeedde lasBarFeeder class.

void feed(double price, chart *c=nullptr, int chartIdx=0)

Es decir, el parámetro formalcde lasfeed function.

json point = {bar.Time, bar.Open, bar.High, bar.Low, bar.close}; // construct a json type data
if (c != nullptr) { // The chart object pointer is not equal to the null pointer, do the following.
    if (newBar) { // judge if the new Bar appears
         c->add(chartIdx, point); // call the chart object member function "add" to insert data into the chart object (new k line bar)
         c->reset(1000); // only keep 1000 bar data
     } else {
         c->add(chartIdx, point, -1); // Otherwise update (not new bar), this point (update this bar).
     }
}

Insertar un nuevo K-line Bar datos en el gráfico llamando eladdfunción miembro del objeto del gráfico_c.

c->add(chartIdx, point);

Prueba de retroceso

img img img

Esta estrategia es sólo para fines de aprendizaje y comunicación. Cuando se aplique en el mercado real, modifique y optimice según la situación real del mercado.

Dirección estratégica:https://www.fmz.com/strategy/163447

Las estrategias más interesantes están en la plataforma FMZ Quant":https://www.fmz.com


Relacionados

Más.