Embedding python in C++ with boost::python

We have a low-latency C++ trading framework which allows us to deploy various trading strategies relatively easily.

Most of our traders, however, don’t know C++, which means they are often waiting on IT resource to develop and deploy a new strategy.

A lot of them do know python though, so I wanted to find a way to allow them to write the core logic of their strategies in python, whilst still leveraging our low-latency trading infrastructure

Here is my embedding python in C++ proof-of-concept.

There is nothing new here, but I found existing documentation and tutorials somewhat lacking, hence this blog post.

Step 1: Define the C++ types which represents the trading framework

Side

An enum to represent buying or selling an order

enum Side
{
    BUY,
    SELL
};

Order

A struct with the details of a single order

struct Order
{
    std::string symbol;
    Side        side;
    int         size;
    double      price;
    int         order_id;
};

StrategyServer

A class which provides the trading functionality, such as sending an order

class StrategyServer
{
public:
    void sendOrder(StrategyInstance&, const std::string& symbol, Side, int size, double price);
private:
    int _next_order_id = 0;
};

StrategyInstance

A pure virtual base class which represents a single strategy

  • Subclasses must override eval(), the entry-point into the strategy. This is where the strategy’s logic is evaluated and orders potentially sent to market.

  • Subclasses must also override onOrder(const Order&) which is the callback for the resulting order sent to market

class StrategyInstance
{
public:
    StrategyInstance(StrategyServer&);
    virtual ~StrategyInstance() = default;

    // evaluates the strategy's logic
    virtual void eval() = 0;
    
    // callback for an acknowledged order
    virtual void onOrder(const Order&) = 0;

    // asks the StrategyServer to send an order
    void sendOrder(const std::string& symbol, Side, int size, double price); 

private:
    StrategyServer& _server;
};

Step 2: Create a python wrapper for our virtual StrategyInstance

Use boost::python::wrapper to allow a python class to subclass StrategyInstance

The virtual overrides look up the python class’s implementation and call those

class PyStrategyInstance final
    : public StrategyInstance
    , public bp::wrapper<StrategyInstance>
{
    using StrategyInstance::StrategyInstance;

    void eval() override
    {
        // call through to the python class's `eval` method
        get_override("eval")();
    }

    void onOrder(const Order& order) override
    {
        // call through to the python class's `on_order` method
        get_override("on_order")(order);
    }
};

Step 3: Expose these types to python

We use BOOST_PYTHON_MODULE to define a python module containing all these types

BOOST_PYTHON_MODULE(StrategyFramework)
{
    bp::enum_<Side>("Side")
        .value("BUY", BUY)
        .value("SELL", SELL)
        ;

    bp::class_<Order>("Order")
        .def_readonly("symbol",   &Order::symbol)
        .def_readonly("side",     &Order::side)
        .def_readwrite("size",    &Order::size)
        .def_readwrite("price",   &Order::price)
        .def_readonly("order_id", &Order::order_id)
        ;

    bp::class_<StrategyServer>("StrategyServer")
        ;

    bp::class_<PyStrategyInstance, boost::noncopyable>("StrategyInstance", bp::init<StrategyServer&>())
        .def("send_order", &StrategyInstance::sendOrder)
        ;
}

Step 4: define a function which imports a python file as a module

This is taken from the python wiki on boost::python, from the tip on loading a module by path.

What is does is allow us to specify a python file and load it as if we called import module

bp::object import(const std::string& module, const std::string& path, bp::object& globals)
{
    bp::dict locals;
    locals["module_name"] = module;
    locals["path"]        = path;

    bp::exec("import imp\n"
             "new_module = imp.load_module(module_name, open(path), path, ('py', 'U', imp.PY_SOURCE))\n",
             globals,
             locals);
    return locals["new_module"];
}

Step 5: load a python file containing our strategy and execute it

This is where we pull it all together.

We initialise the python runtime, register the python module with our C++ code in it, import the python file which contains our strategy, and then run it

int main()
try
{
    Py_Initialize();
    
    // register the python module we created, so our script can import it
    PyImport_AppendInittab("StrategyFramework", &initStrategyFramework);

    StrategyServer server;

    // import the __main__ module and obtain the globals dict
    bp::object main     = bp::import("__main__");
    bp::object globals  = main.attr("__dict__");
    
    // import our strategy.py file
    bp::object module   = import("strategy", "strategy.py", globals);
    
    // obtain the strategy class and instantiate one
    bp::object Strategy = module.attr("Strategy");
    bp::object strategy = Strategy(server);

    // eval the strategy
    strategy.attr("eval")();

    return 0;
}
catch(const bp::error_already_set&)
{
    std::cerr << ">>> Error! Uncaught exception:\n";
    PyErr_Print();
    return 1;
}

Step 6: define a python strategy

This is the sample strategy written in python.

As a proof of concept, all it does is send an order when the strategy is evaluated.

We print the resulting order we receive in our callback

from StrategyFramework import *

class Strategy(StrategyInstance):
    def __init__(self, server):
        StrategyInstance.__init__(self, server)

    def eval(self):
        print "strategy.eval"
        self.send_order("GOOG", Side.BUY, 100, 759.11)

    def on_order(self, o):
        print "order for {} {} {}@{} has order_id={}".format(
            o.symbol, "buy" if o.side == Side.BUY else "sell", o.size, o.price, o.order_id)

Step 7: run it!

$ ./strategy 
strategy.eval
sending order to market
order for GOOG buy 100@759.11 has order_id=1

Complete code listing:

Here is the complete code listing which you can use to test out the code yourself

main.cpp:

#include <iostream>
#include <boost/python.hpp>

enum Side
{
    BUY,
    SELL
};

struct Order
{
    std::string symbol;
    Side        side;
    int         size;
    double      price;
    int         order_id;
};

// forward declaration
class StrategyInstance;

class StrategyServer
{
public:
    void sendOrder(StrategyInstance&, const std::string& symbol, Side, int size, double price);
private:
    int _next_order_id = 0;
};

class StrategyInstance
{
public:
    StrategyInstance(StrategyServer&);
    virtual ~StrategyInstance() = default;

    virtual void eval() = 0;
    virtual void onOrder(const Order&) = 0;

    void sendOrder(const std::string& symbol, Side, int size, double price);

private:
    StrategyServer& _server;
};

///////////////////////////////////

// implementation

void StrategyServer::sendOrder(StrategyInstance& instance, const std::string& symbol, Side side, int size, double price)
{
    // simulate sending an order, receiving an acknowledgement and calling back to the strategy instance

    std::cout << "sending order to market\n";

    Order order { symbol, side, size, price, ++_next_order_id };
    instance.onOrder(order);
}

///////////////////////////////////

StrategyInstance::StrategyInstance(StrategyServer& server)
    : _server(server)
{}

void StrategyInstance::sendOrder(const std::string& symbol, Side side, int size, double price)
{
    _server.sendOrder(*this, symbol, side, size, price);
}

////////////////////////////////////

// export to python 

namespace bp = boost::python;

class PyStrategyInstance final
    : public StrategyInstance
    , public bp::wrapper<StrategyInstance>
{
    using StrategyInstance::StrategyInstance;

    void eval() override
    {
        get_override("eval")();
    }

    void onOrder(const Order& order) override
    {
        get_override("on_order")(order);
    }
};

BOOST_PYTHON_MODULE(StrategyFramework)
{
    bp::enum_<Side>("Side")
        .value("BUY", BUY)
        .value("SELL", SELL)
        ;

    bp::class_<Order>("Order")
        .def_readonly("symbol",   &Order::symbol)
        .def_readonly("side",     &Order::side)
        .def_readwrite("size",    &Order::size)
        .def_readwrite("price",   &Order::price)
        .def_readonly("order_id", &Order::order_id)
        ;

    bp::class_<StrategyServer>("StrategyServer")
        ;

    bp::class_<PyStrategyInstance, boost::noncopyable>("StrategyInstance", bp::init<StrategyServer&>())
        .def("send_order", &StrategyInstance::sendOrder)
        ;
}

bp::object import(const std::string& module, const std::string& path, bp::object& globals)
{
    bp::dict locals;
    locals["module_name"] = module;
    locals["path"]        = path;

    bp::exec("import imp\n"
             "new_module = imp.load_module(module_name, open(path), path, ('py', 'U', imp.PY_SOURCE))\n",
             globals,
             locals);
    return locals["new_module"];
}

//////////////////////////////////

int main()
try
{
    Py_Initialize();
    PyImport_AppendInittab("StrategyFramework", &initStrategyFramework);

    StrategyServer server;

    bp::object main     = bp::import("__main__");
    bp::object globals  = main.attr("__dict__");
    bp::object module   = import("strategy", "strategy.py", globals);
    bp::object Strategy = module.attr("Strategy");
    bp::object strategy = Strategy(server);

    strategy.attr("eval")();

    return 0;
}
catch(const bp::error_already_set&)
{
    std::cerr << ">>> Error! Uncaught exception:\n";
    PyErr_Print();
    return 1;
}

strategy.py:

from StrategyFramework import *

class Strategy(StrategyInstance):
    def __init__(self, server):
        StrategyInstance.__init__(self, server)

    def eval(self):
        print "strategy.eval"
        self.send_order("GOOG", Side.BUY, 100, 759.11)

    def on_order(self, o):
        print "order for {} {} {}@{} has order_id={}".format(
            o.symbol, "buy" if o.side == Side.BUY else "sell", o.size, o.price, o.order_id)

Written on December 6, 2016