Files
UoL/CM2005 Object Oriented Programming/Midterm/Merkelrex-TradingBot/TradingBot.cpp

556 lines
16 KiB
C++

#include "TradingBot.h"
TradingBot::TradingBot(MerkelMain *merkel)
{
this->merkel = merkel;
products = merkel->getKnownProducts();
// We create the log files
logAssets(merkel->returnWallet(), merkel->getCurrentTime(), "start");
logBidsAsks(OrderBookEntry{1.0, 1.0, "", "", OrderBookType::ask, "simuser"}, "start");
logUserSales(lastSales, "start");
}
void TradingBot::startBot()
{
int input;
while (input != 8)
{
printMenu();
input = getUserOption();
processUserOption(input);
}
std::cout << "Exiting bot..." << std::endl;
}
void TradingBot::printMenu()
{
std::string spacer = "================================================================================";
std::cout << spacer << std::endl;
std::cout << " Welcome to the Trading Bot | Current time: " + merkel->getCurrentTime() << std::endl;
std::cout << spacer << std::endl;
// 1 print help
std::cout << "1: Retrieve current orders" << std::endl;
std::cout << "2: Predict ask and bid rates" << std::endl;
std::cout << "3: Print predictions" << std::endl;
std::cout << "4: Make automatic bids and asks for the current time" << std::endl;
std::cout << "5: Process bids and asks and go to next time slot" << std::endl;
std::cout << "6: Fully automate this time slot" << std::endl;
std::cout << "7: Fully automate all remaining time slots" << std::endl;
std::cout << "8: Exit bot" << std::endl;
std::cout << spacer << std::endl;
std::cout << "Enter an option: ";
return;
}
int TradingBot::getUserOption()
{
int userOption = 0;
std::string line;
std::getline(std::cin, line);
try
{
userOption = std::stoi(line);
}
catch (const std::exception &e)
{
std::cout << "Invalid option" << std::endl;
}
return userOption;
}
void TradingBot::processUserOption(int userOption)
{
if (userOption == 0)
return;
// Retrieve bids and asks for current time slot
if (userOption == 1)
{
retrieveOrders();
if (bookAsks.size() > 0 && bookBids.size() > 0)
std::cout << "Bids and Asks loaded successfully for the current time." << std::endl;
}
// User linear regression to predict future rates
if (userOption == 2)
{
predictRates();
if (predictedAsks.size() > 0 && predictedBids.size() > 0)
{
std::cout << "New predicitons have been created" << std::endl;
}
}
// Create new bids or asks if they are predicted to make a profit
if (userOption == 3)
{
if(predictedAsks.size() == 0 || predictedBids.size() == 0)
{
std::cout << "No predictions to print" << std::endl;
return;
}
printPredictions();
}
// Based on predicted rates, make new bids or asks
if (userOption == 4)
{
makeAutoBids();
makeAutoAsks();
}
// Have Merkel process bids, asks and go to the new time slot
if (userOption == 5)
{
processBidsAsks();
}
// Automate by pulling orders, predicting rates, making new asks/bids, withdrawing bids/asks
if (userOption == 6)
{
retrieveOrders();
predictRates();
makeAutoBids();
makeAutoAsks();
processBidsAsks();
}
// Automate all remaining time slots
if (userOption == 7)
{
do
{
retrieveOrders();
predictRates();
makeAutoBids();
makeAutoAsks();
processBidsAsks();
}
while (merkel->getCurrentTime() != "END");
std::cout << "Finished all time slots and made " << salesHistory.size() << " sales" << std::endl;
}
}
// R1A
// Retrieve orders for the current time slot
void TradingBot::retrieveOrders()
{
bookAsks.clear();
bookAsks = merkel->getCurrentAsks();
bookBids.clear();
bookBids = merkel->getCurrentBids();
// We'll go through all products and get the mean
// Get the mean ask and bid price and store in history
std::map<std::string, double> askMap;
std::map<std::string, double> bidMap;
for (const std::string &product : products)
{
askMap[product] = getMean(bookAsks, product);
bidMap[product] = getMean(bookBids, product);
}
askHistory.push_back(askMap);
bidHistory.push_back(bidMap);
return;
}
// R1B
// Predict the next rates for all products
// Also calculates the mean ask and bid prices per product and saves them to history
void TradingBot::predictRates()
{
if (bookAsks.size() == 0 || bookBids.size() == 0)
{
std::cout << "No orders have been imported. Import orders and try again" << std::endl;
return;
}
// If the history vectors only have 1 element, then we're at the first time slot
// We can only predict a future price thorugh regression when there are at least 2 entries in the history vectors
if (bidHistory.size() >= 2 && askHistory.size() >= 2)
{
for (const std::string &product : products)
{
// We want to find an equation of type Y = a + bX where
// Y is the predicted value
// X is the point in time index
// We need to calculate a and b
// The regression needs to be calculated every time slot because it may change due to the new data
// ASKS //
std::vector<double> askRegression = regressionFromHistory(askHistory, product);
double &askIntercept = askRegression[0];
double &askSlope = askRegression[1];
// We calculate the current askRegression value (1-indexed)
// The formula is Y = Y-intercept + Slope * X or regressionValue = askntercept + askSlope * timeIndex
currentAsks[product] = askIntercept + askSlope * bidHistory.size();
predictedAsks[product] = askIntercept + askSlope * (bidHistory.size() + 1);
// BIDS //
std::vector<double> bidRegression = regressionFromHistory(bidHistory, product);
double &bidIntercept = bidRegression[0];
double &bidSlope = bidRegression[1];
// We calculate the current bidRegression value (1-indexed)
// The formula is Y = Y-intercept + Slope * X or regressionValue = bidIntercept + bidSlope * timeIndex
currentBids[product] = bidIntercept + bidSlope * bidHistory.size();
predictedBids[product] = bidIntercept + bidSlope * (bidHistory.size() + 1);
}
}
return;
}
// R2A
// R2B
// R2D
// Make bids automatically
void TradingBot::makeAutoBids()
{
// The bid history has one or no entries, auto bids are skipped
if (bidHistory.size() < 2)
return;
// We use the regression values to determine if we should make bids for the different products
for (std::string &product : products)
{
// We'll make a bid (buy) the product if the predicted price is higher than the current mean stored in bidHistory
if (bidHistory.back()[product] < predictedBids[product])
{
// The price we'll offer to buy will be half way between the current and predicted regression values
double price = (currentBids[product] + predictedBids[product]) / 2.0;
// To determine how much to offer to buy, we'll use the last saved list of sales to get an idea of the traded volume
double amount = 0.0;
if (lastSales.size() > 0)
{
for(int i=0; i<lastSales.size(); ++i)
{
if(lastSales[i].product == product)
amount += lastSales[i].amount;
}
}
// We will trade up to 20 percent of the last traded volume
amount = amount * 0.20;
// If amount still 0, we use the current orders' volume
if(amount == 0.0)
{
for (const OrderBookEntry &bid : bookBids)
{
if (bid.product == product)
amount += bid.amount;
}
amount = amount * 0.20;
}
// R2D
// Check our bids and withdraw them if the price is higher than our calculated price
for (const OrderBookEntry &bid : bookBids)
{
if(bid.username == "simuser" && bid.product == product && bid.price > price)
{
merkel->withdrawOrder(bid);
}
}
// We now make a bid
// R2B
merkel->enterBid(price, amount, product);
// We log the bid
// R4B
logBidsAsks(OrderBookEntry{price, amount, merkel->getCurrentTime(), product, OrderBookType::bid, "simuser"}, "append");
}
}
}
// R3A
// R3B
// R3D
// Make asks automatically
void TradingBot::makeAutoAsks()
{
// The ask history has one or no entries, auto asks are skipped
if (askHistory.size() < 2)
return;
// We use the regression values to determine if we should make asks for the different products
for (std::string &product : products)
{
// We'll make an ask (sell) the product if the predicted price is lower than the current mean stored in askHistory
if (askHistory.back()[product] > predictedAsks[product])
{
// The price we'll offer to sell will be half way between the current and predicted regression values
double price = (currentAsks[product] + predictedAsks[product]) / 2.0;
// To determine how much to offer to sell, we'll use the last saved list of sales to get an idea of the traded volume
double amount = 0.0;
if (lastSales.size() > 0)
{
for(int i=0; i<lastSales.size(); ++i)
{
if(lastSales[i].product == product)
amount += lastSales[i].amount;
}
}
// We will trade up to 20 percent of the last traded volume
amount = amount * 0.20;
// If amount still 0, we use the current orders' volume
if(amount == 0.0)
{
for (const OrderBookEntry &ask : bookAsks)
{
if (ask.product == product)
amount += ask.amount;
}
amount = amount * 0.20;
}
// R3D
// Check our asks and withdraw them if the price is higher than our calculated price
for (const OrderBookEntry &ask : bookAsks)
{
if(ask.username == "simuser" && ask.product == product && ask.price < price)
{
merkel->withdrawOrder(ask);
}
}
// We now make an ask
// R3B
merkel->enterAsk(price, amount, product);
// We log the ask
// R4B
logBidsAsks(OrderBookEntry{price, amount, merkel->getCurrentTime(), product, OrderBookType::ask, "simuser"}, "append");
}
}
}
// R2C & R3C
// Ask Merkel to process all bids and asks for the current time slot
void TradingBot::processBidsAsks()
{
lastSales.clear();
// R2C & R3C
lastSales = merkel->gotoNextTimeframe(true);
salesHistory.insert(salesHistory.end(), lastSales.begin(), lastSales.end());
logAssets(merkel->returnWallet(), merkel->getCurrentTime(), "append");
logUserSales(lastSales, "append");
return;
}
// Calculate and Return the Y intercept and slope from a vector of map objects
std::vector<double> TradingBot::regressionFromHistory(std::vector<std::map<std::string, double>> history, std::string product)
{
// Check if enough data points are present
if (history.size() < 2)
return std::vector<double>{0.0, 0.0};
// number of history entries
int hisSize = history.size();
// Build a vector of history prices for this product
std::vector<double> priceHistory;
for (const std::map<std::string, double> &historyEntry : history)
{
priceHistory.push_back(historyEntry.at(product));
}
// Find the mean of the independent variables
// 1-indexed sum
double sumX = (hisSize * (hisSize + 1)) / 2;
double meanX = sumX / ((double)hisSize);
// Find the mean of the dependent variables
double sumY = 0.0;
double meanY = 0.0;
for (const double &price : priceHistory)
{
sumY += price;
}
meanY = sumY / ((double)hisSize);
// We calculate the distances to the mean lines
// X distances
std::vector<double> distanceX;
for (int i = 1; i <= hisSize; ++i)
{
distanceX.push_back(((double)i) - meanX);
}
// Y distances
std::vector<double> distanceY;
for (const double &price : priceHistory)
{
distanceY.push_back(price - meanY);
}
// We square X distances
std::vector<double> distanceSquareX;
for (int i = 0; i < distanceX.size(); ++i)
{
distanceSquareX.push_back(distanceX[i] * distanceX[i]);
}
// We find the product of the distances
std::vector<double> distanceProducts;
for (int i = 0; i < distanceX.size(); ++i)
{
distanceProducts.push_back(distanceX[i] * distanceY[i]);
}
// We add up the X distance squares
double squareSum = 0.0;
for (const double &square : distanceSquareX)
{
squareSum += square;
}
// We add up the distance products
double productSum = 0.0;
for (const double &distanceP : distanceProducts)
{
productSum += distanceP;
}
// We calculate the slope of the regression line
double slope = productSum / squareSum;
// We calculate interception with the Y axis
// We know the regression line will go through the (meanX, meanY) point
// We have: meanY = yIntercept + slope * meanX
// So we solve for Y
double yIntercept = meanY - slope * meanX;
return std::vector<double>{yIntercept, slope};
}
// return the mean price from the vector
double TradingBot::getMean(std::vector<OrderBookEntry> orders, std::string product)
{
double sum = 0;
int count = 0;
for (const OrderBookEntry &order : orders)
{
if (order.product == product)
{
sum += order.price;
++count;
}
}
if (count == 0)
return 0.0;
return sum / count;
}
// Print predictions if they have been made
void TradingBot::printPredictions()
{
std::cout.precision(8);
std::cout << "Asks" << std::endl;
if (predictedAsks.size() == 0)
std::cout << "No ask predictions have been made yet" << std::endl;
for (auto const &ask : predictedAsks)
{
std::cout << ask.first << " - " << ask.second << std::endl;
}
std::cout << "Bids" << std::endl;
if (predictedBids.size() == 0)
std::cout << "No bid predictions have been made yet" << std::endl;
for (auto const &bid : predictedBids)
{
std::cout << bid.first << " - " << bid.second << std::endl;
}
}
// R4A
// Print assets from wallet
void TradingBot::logAssets(std::string Wallet, std::string timestamp, std::string mode)
{
std::fstream assetStream;
if(mode == "start")
assetStream.open("assets.txt", std::ios::out);
else
assetStream.open("assets.txt", std::ios_base::app);
// Check the file was created properly
if(!assetStream)
{
// We couldn't open/create the file
return;
}
else
{
assetStream << "Time: " << timestamp << std::endl;
assetStream << Wallet << std::endl;
assetStream.close();
}
}
// R4B
// Print bids and offers that have been created
void TradingBot::logBidsAsks(OrderBookEntry order, std::string mode)
{
std::fstream askStream;
std::fstream bidStream;
if(mode == "start")
{
// create the files overwriting anything there
askStream.open("asks.txt", std::ios::out);
bidStream.open("bids.txt", std::ios::out);
askStream.close();
bidStream.close();
return;
}
else if(order.orderType == OrderBookType::ask)
{
askStream.open("asks.txt", std::ios_base::app);
// Check the file was created properly
if(!askStream)
{
// We couldn't open/create the file
return;
}
else
{
askStream << order.timestamp << ","
<< order.product << ","
<< OrderBookEntry::OrderBookTypeToString(order.orderType) << ","
<< order.price << ","
<< order.amount << std::endl;
askStream.close();
}
}
else if(order.orderType == OrderBookType::bid)
{
bidStream.open("bids.txt", std::ios_base::app);
// Check the file was created properly
if(!bidStream)
{
// We couldn't open/create the file
return;
}
else
{
bidStream << order.timestamp << ","
<< order.product << ","
<< OrderBookEntry::OrderBookTypeToString(order.orderType) << ","
<< order.price << ","
<< order.amount << std::endl;
bidStream.close();
}
}
}
// R4C
// Print successful asks and bids
void TradingBot::logUserSales(std::vector<OrderBookEntry> sales, std::string mode)
{
std::fstream saleStream;
if(mode == "start")
saleStream.open("user-sales.txt", std::ios::out);
else
saleStream.open("user-sales.txt", std::ios_base::app);
// Check the file was created properly
if(!saleStream)
{
// We couldn't open/create the file
return;
}
else
{
// We loop though the sales vector and check the type username
for(const OrderBookEntry &sale : sales)
{
// this means the sale was created from a user ask or sale.
if(sale.username == "simuser")
{
saleStream << sale.timestamp << ","
<< sale.product << ","
<< OrderBookEntry::OrderBookTypeToString(sale.orderType) << ","
<< sale.price << ","
<< sale.amount << std::endl;
}
}
saleStream.close();
}
}