From 945a77c64b28dbf77095c78dabac2a397c14ab91 Mon Sep 17 00:00:00 2001 From: Max Glass Date: Mon, 30 Sep 2024 02:34:56 -0500 Subject: [PATCH] add cpp submission --- linter/CMakeLists.txt | 8 +- linter/Taskfile.yml | 3 +- linter/src/config.h.in | 8 +- linter/src/crow/crow.cpp | 43 +++- linter/src/fetching/fetching.cpp | 168 +++++++++++++ linter/src/fetching/fetching.hpp | 45 ++++ linter/src/firebase/fetching.cpp | 236 ------------------ linter/src/firebase/fetching.hpp | 40 --- linter/src/lint/lint.cpp | 100 +++++--- linter/src/lint/lint.hpp | 5 +- linter/src/pywrapper/runtime.cpp | 166 ------------ linter/src/pywrapper/runtime.hpp | 22 -- linter/src/runtime/cpp/cpp_runtime.cpp | 148 +++++++++++ linter/src/runtime/cpp/cpp_runtime.hpp | 56 +++++ linter/src/runtime/python/python_runtime.cpp | 146 +++++++++++ linter/src/runtime/python/python_runtime.hpp | 53 ++++ linter/src/runtime/runtime.hpp | 65 +++++ linter/src/spawner/main.cpp | 63 ++++- linter/src/spawning/spawning.cpp | 22 +- linter/src/spawning/spawning.hpp | 7 +- linter/test/src/NUTC-linter_test.cpp | 29 ++- .../api/protected/db/user/createAlgo/route.ts | 2 + .../protected/db/user/uploadAlgoFile/route.ts | 2 +- web/app/dash/submit/form.tsx | 136 +++++++++- .../migration.sql | 8 + web/prisma/schema.prisma | 1 + webserver/src/main.rs | 63 ++++- 27 files changed, 1080 insertions(+), 565 deletions(-) create mode 100644 linter/src/fetching/fetching.cpp create mode 100644 linter/src/fetching/fetching.hpp delete mode 100644 linter/src/firebase/fetching.cpp delete mode 100644 linter/src/firebase/fetching.hpp delete mode 100644 linter/src/pywrapper/runtime.cpp delete mode 100644 linter/src/pywrapper/runtime.hpp create mode 100644 linter/src/runtime/cpp/cpp_runtime.cpp create mode 100644 linter/src/runtime/cpp/cpp_runtime.hpp create mode 100644 linter/src/runtime/python/python_runtime.cpp create mode 100644 linter/src/runtime/python/python_runtime.hpp create mode 100644 linter/src/runtime/runtime.hpp create mode 100644 web/prisma/migrations/20240926234323_algo_language/migration.sql diff --git a/linter/CMakeLists.txt b/linter/CMakeLists.txt index ed2f06b1..370015ed 100644 --- a/linter/CMakeLists.txt +++ b/linter/CMakeLists.txt @@ -41,8 +41,9 @@ endif() add_library( NUTC-linter_lib OBJECT - src/firebase/fetching.cpp - src/pywrapper/runtime.cpp + src/fetching/fetching.cpp + src/runtime/cpp/cpp_runtime.cpp + src/runtime/python/python_runtime.cpp src/spawning/spawning.cpp src/crow/crow.cpp # Utils @@ -52,7 +53,8 @@ add_library( add_library( NUTC-linter-spawner_lib OBJECT src/lint/lint.cpp - src/pywrapper/runtime.cpp + src/runtime/cpp/cpp_runtime.cpp + src/runtime/python/python_runtime.cpp ) # --- Main executable libs ---- diff --git a/linter/Taskfile.yml b/linter/Taskfile.yml index 8694a5b7..57b19ccc 100644 --- a/linter/Taskfile.yml +++ b/linter/Taskfile.yml @@ -46,14 +46,13 @@ tasks: - test -f CMakeUserPresets.json cmds: - cmake --build {{.CMAKE_SUFFIX}} -j - run: env: NUTC_SPAWNER_BINARY_PATH: '{{if eq .RELEASE_BUILD "true"}}./build/NUTC-linter-spawner{{else}}./build/dev/NUTC-linter-spawner{{end}}' vars: LINTER_PATH: '{{if eq .RELEASE_BUILD "true"}}build/{{.NAME}}{{else}}build/dev/{{.NAME}}{{end}}' cmds: - - task: built + - task: build - ./{{ .LINTER_PATH }} run-v: diff --git a/linter/src/config.h.in b/linter/src/config.h.in index 6c5c149e..10018cfb 100644 --- a/linter/src/config.h.in +++ b/linter/src/config.h.in @@ -23,11 +23,15 @@ #define LOG_BACKUP_COUNT 5 #ifdef NUTC_LOCAL_DEV -# define FIREBASE_URL "http://firebase:9000/" +# define S3_URL "http://localhost:4566" +# define WEBSERVER_URL "http://localhost:16124" #else -# define FIREBASE_URL "https://nutc-web-default-rtdb.firebaseio.com/" +# define S3_URL "https://nutc.s3.us-east-2.amazonaws.com" +# define WEBSERVER_URL "http://localhost:16124" #endif +#define S3_BUCKET "nutc" + // Linting #define LINT_AUTO_TIMEOUT_SECONDS 10 diff --git a/linter/src/crow/crow.cpp b/linter/src/crow/crow.cpp index 13ab12d5..b1afa0d2 100644 --- a/linter/src/crow/crow.cpp +++ b/linter/src/crow/crow.cpp @@ -1,6 +1,6 @@ #include "crow.hpp" -#include "firebase/fetching.hpp" +#include "fetching/fetching.hpp" #include "logging.hpp" #include "spawning/spawning.hpp" @@ -41,14 +41,32 @@ get_server_thread() log_e(main, "No algo_id provided"); return crow::response(400); } + if (!req.url_params.get("language")) { + log_e(main, "No language provided"); + return crow::response(400); + } std::string uid = req.url_params.get("uid"); std::string algo_id = req.url_params.get("algo_id"); + std::string language = req.url_params.get("language"); - auto algo_code = nutc::client::get_algo(uid, algo_id); + spawning::AlgoLanguage algo_language; + if (language == "python") { + algo_language = spawning::AlgoLanguage::Python; + } + else if (language == "cpp") { + algo_language = spawning::AlgoLanguage::Cpp; + } + else { + log_e(main, "Invalid language provided: {}", language); + return crow::response(400); + } + + auto algo_code = nutc::client::get_algo(algo_id); if (!algo_code.has_value()) { - nutc::client::set_lint_failure( + nutc::client::set_lint_result( uid, algo_id, + false, fmt::format( "[linter] FAILURE - could not find algo {} for id {}\n", algo_id, @@ -66,17 +84,16 @@ get_server_thread() return res; } - client::LintingResultOption algo_status_code; - auto lint_res = spawner_manager.spawn_client(algo_code.value()); - if (lint_res.success) { - nutc::client::set_lint_success(uid, algo_id, lint_res.message); - algo_status_code = client::LintingResultOption::SUCCESS; - } - else { - nutc::client::set_lint_failure(uid, algo_id, lint_res.message); - algo_status_code = client::LintingResultOption::FAILURE; - } + auto lint_res = + spawner_manager.spawn_client(algo_code.value(), algo_language); + + nutc::client::set_lint_result( + uid, algo_id, lint_res.success, lint_res.message + ); + client::LintingResultOption algo_status_code = + lint_res.success ? client::LintingResultOption::SUCCESS + : client::LintingResultOption::FAILURE; crow::json::wvalue response({ {"linting_status", static_cast(algo_status_code)} }); diff --git a/linter/src/fetching/fetching.cpp b/linter/src/fetching/fetching.cpp new file mode 100644 index 00000000..dc5524d9 --- /dev/null +++ b/linter/src/fetching/fetching.cpp @@ -0,0 +1,168 @@ +#include "fetching.hpp" + +#include "config.h" +#include "config.h.in" + +#include +#include + +#include +#include +#include + +namespace { +std::string +replaceDisallowedValues(const std::string& input) +{ + std::regex newlinePattern("\\n"); + std::string input2 = std::regex_replace(input, newlinePattern, "\\n"); + std::regex disallowedPattern("[.$#\\[\\]]"); + + return std::regex_replace(input2, disallowedPattern, ""); +} +} // namespace + +namespace nutc { +namespace client { + +void +set_lint_result( + const std::string& uid, + const std::string& algo_id, + bool succeeded, + const std::string& message +) +{ + std::string endpoint = + fmt::format("{}/lint-result/{}/{}", WEBSERVER_URL, uid, algo_id); + + SetLintBody body{succeeded, replaceDisallowedValues(message)}; + + database_request("POST", endpoint, glz::write_json(body), true); +} + +static size_t +write_callback(void* contents, size_t size, size_t nmemb, void* userp) +{ + auto* str = reinterpret_cast(userp); + auto* data = static_cast(contents); + + str->append(data, size * nmemb); + return size * nmemb; +} + +std::optional +storage_request(const std::string& url) +{ + std::string readBuffer; + + CURL* curl = curl_easy_init(); + if (curl) { + CURLcode res = curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + if (res != CURLE_OK) { + return std::nullopt; + } + res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + if (res != CURLE_OK) { + return std::nullopt; + } + res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + if (res != CURLE_OK) { + return std::nullopt; + } + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + return std::nullopt; + } + curl_easy_cleanup(curl); + } + + return readBuffer; +} + +std::optional +get_algo(const std::string& algo_id) +{ + std::string url = fmt::format("{}/{}/{}", S3_URL, S3_BUCKET, algo_id); + std::cout << "requesting file from [" << url << "]\n"; + auto algo_file = storage_request(url); + return algo_file; +} + +std::optional +database_request( + const std::string& method, + const std::string& url, + const std::string& data, + bool json_body +) +{ + std::string readBuffer; + + CURL* curl = curl_easy_init(); + if (curl) { + CURLcode res = curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + if (res != CURLE_OK) { + return std::nullopt; + } + res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + if (res != CURLE_OK) { + return std::nullopt; + } + res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + if (res != CURLE_OK) { + return std::nullopt; + } + + if (json_body) { + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/json"); + res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + if (res != CURLE_OK) { + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + return std::nullopt; + } + } + + if (method == "POST") { + res = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + return std::nullopt; + } + } + else if (method == "PUT") { + res = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + if (res != CURLE_OK) { + return std::nullopt; + } + res = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); + if (res != CURLE_OK) { + return std::nullopt; + } + } + else if (method == "DELETE") { + res = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + if (res != CURLE_OK) { + return std::nullopt; + } + } + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + return std::nullopt; + } + + curl_easy_cleanup(curl); + } + glz::json_t json{}; + auto error = glz::read_json(json, readBuffer); + if (error) { + return std::nullopt; + } + return json; +} +} // namespace client +} // namespace nutc diff --git a/linter/src/fetching/fetching.hpp b/linter/src/fetching/fetching.hpp new file mode 100644 index 00000000..a4c641e4 --- /dev/null +++ b/linter/src/fetching/fetching.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include +#include + +namespace nutc { +namespace client { + +struct SetLintBody { + bool success; + std::string message; +}; + +enum class LintingResultOption { UNKNOWN = -1, FAILURE, SUCCESS, PENDING }; + +std::optional database_request( + const std::string& method, + const std::string& url, + const std::string& data = "", + bool json_body = false +); + +std::optional storage_request(const std::string& url); + +void set_lint_result( + const std::string& uid, + const std::string& algo_id, + bool succeeded, + const std::string& message +); + +[[nodiscard]] std::optional get_algo(const std::string& algo_id); + +} // namespace client +} // namespace nutc + +template <> +struct glz::meta { + using t = nutc::client::SetLintBody; + static constexpr auto value = + object("success", &t::success, "message", &t::message); +}; diff --git a/linter/src/firebase/fetching.cpp b/linter/src/firebase/fetching.cpp deleted file mode 100644 index 013b9ab7..00000000 --- a/linter/src/firebase/fetching.cpp +++ /dev/null @@ -1,236 +0,0 @@ -#include "fetching.hpp" - -#include "config.h" - -#include -#include - -#include -#include - -namespace nutc { -namespace client { - -static std::string -get_firebase_endpoint(const std::string& params) -{ -#ifdef NUTC_LOCAL_DEV - return FIREBASE_URL + params + "?ns=nutc-web-default-rtdb"; -#else - return FIREBASE_URL + params; -#endif -} - -void -set_lint_result(const std::string& uid, const std::string& algo_id, bool succeeded) -{ - std::string success = "\"success\""; - std::string failure = "\"failure\""; - std::string params = - fmt::format("users/{}/algos/{}/lintResults.json", uid, algo_id); - - firebase_request( - "PUT", get_firebase_endpoint(params), succeeded ? success : failure - ); -} - -std::string -replaceDisallowedValues(const std::string& input) -{ - std::regex newlinePattern("\\n"); - std::string input2 = std::regex_replace(input, newlinePattern, "\\n"); - std::regex disallowedPattern("[.$#\\[\\]]"); - - return std::regex_replace(input2, disallowedPattern, ""); -} - -void -set_lint_success( - const std::string& uid, const std::string& algo_id, const std::string& success -) -{ - std::string json_success = "\"" + replaceDisallowedValues(success) + "\""; - std::string params1 = - fmt::format("users/{}/algos/{}/lintSuccessMessage.json", uid, algo_id); - std::string params2 = fmt::format("users/{}/latestAlgoId.json", uid); - - firebase_request("PUT", get_firebase_endpoint(params1), json_success); - - firebase_request("PUT", get_firebase_endpoint(params2), "\"" + algo_id + "\""); - set_lint_result(uid, algo_id, true); -} - -void -set_lint_failure( - const std::string& uid, const std::string& algo_id, const std::string& failure -) -{ - std::string json_failure = "\"" + replaceDisallowedValues(failure) + "\""; - std::string params = - fmt::format("users/{}/algos/{}/lintFailureMessage.json", uid, algo_id); - firebase_request("PUT", get_firebase_endpoint(params), json_failure); - set_lint_result(uid, algo_id, false); -} - -std::optional -get_user_info(const std::string& uid) -{ - std::string url = fmt::format("users/{}.json", uid); - return firebase_request("GET", get_firebase_endpoint(url)); -} - -static size_t -write_callback(void* contents, size_t size, size_t nmemb, void* userp) -{ - auto* str = reinterpret_cast(userp); - auto* data = static_cast(contents); - - str->append(data, size * nmemb); - return size * nmemb; -} - -std::optional -storage_request(const std::string& firebase_url) -{ - std::string readBuffer; - - CURL* curl = curl_easy_init(); - if (curl) { - CURLcode res = curl_easy_setopt(curl, CURLOPT_URL, firebase_url.c_str()); - if (res != CURLE_OK) { - return std::nullopt; - } - res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - if (res != CURLE_OK) { - return std::nullopt; - } - res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - if (res != CURLE_OK) { - return std::nullopt; - } - - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - return std::nullopt; - } - curl_easy_cleanup(curl); - } - - return readBuffer; -} - -std::optional -get_algo(const std::string& uid, const std::string& algo_id) -{ - auto maybe_user_info = get_user_info(uid); - if (!maybe_user_info.has_value()) { - return std::nullopt; - } - auto user_info = maybe_user_info.value(); - // if not has "algos" - if (!user_info.contains("algos") || !user_info["algos"].contains(algo_id)) { - return std::nullopt; - } - glz::json_t algo_info = user_info["algos"][algo_id]; - std::string downloadURL = algo_info["downloadURL"].get(); - auto algo_file = storage_request(downloadURL); - return algo_file; -} - -nutc::client::LintingResultOption -get_algo_status(const std::string& uid, const std::string& algo_id) -{ - auto maybe_user_info = get_user_info(uid); - if (!maybe_user_info.has_value()) { - return nutc::client::LintingResultOption::UNKNOWN; - } - auto user_info = maybe_user_info.value(); - // if not has "algos" - if (!user_info.contains("algos")) { - return nutc::client::LintingResultOption::UNKNOWN; - } - - // check if algo id exists - if (!user_info["algos"].contains(algo_id)) { - return nutc::client::LintingResultOption::UNKNOWN; - } - glz::json_t algo_info = user_info["algos"][algo_id]; - - // check if this algo id has lint results - if (!algo_info.contains("lintResults")) { - return nutc::client::LintingResultOption::UNKNOWN; - } - - std::string linting_result = algo_info["lintResults"].get(); - - switch (linting_result[0]) { - case 'f': - return nutc::client::LintingResultOption::FAILURE; - case 's': - return nutc::client::LintingResultOption::SUCCESS; - default: - return nutc::client::LintingResultOption::PENDING; - } -} - -std::optional -firebase_request( - const std::string& method, const std::string& url, const std::string& data -) -{ - std::string readBuffer; - - CURL* curl = curl_easy_init(); - if (curl) { - CURLcode res = curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - if (res != CURLE_OK) { - return std::nullopt; - } - res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - if (res != CURLE_OK) { - return std::nullopt; - } - res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - if (res != CURLE_OK) { - return std::nullopt; - } - - if (method == "POST") { - res = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); - if (res != CURLE_OK) { - return std::nullopt; - } - } - else if (method == "PUT") { - res = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); - if (res != CURLE_OK) { - return std::nullopt; - } - res = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); - if (res != CURLE_OK) { - return std::nullopt; - } - } - else if (method == "DELETE") { - res = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - if (res != CURLE_OK) { - return std::nullopt; - } - } - - res = curl_easy_perform(curl); - if (res != CURLE_OK) { - return std::nullopt; - } - - curl_easy_cleanup(curl); - } - glz::json_t json{}; - auto error = glz::read_json(json, readBuffer); - if (error) { - return std::nullopt; - } - return json; -} -} // namespace client -} // namespace nutc diff --git a/linter/src/firebase/fetching.hpp b/linter/src/firebase/fetching.hpp deleted file mode 100644 index 5297a862..00000000 --- a/linter/src/firebase/fetching.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -namespace nutc { -namespace client { - -enum class LintingResultOption { UNKNOWN = -1, FAILURE, SUCCESS, PENDING }; - -// database request - change name -std::optional firebase_request( - const std::string& method, const std::string& url, const std::string& data = "" -); - -std::optional storage_request(const std::string& url); - -std::optional get_user_info(const std::string& uid); - -void -set_lint_result(const std::string& uid, const std::string& algo_id, bool succeeded); - -void set_lint_failure( - const std::string& uid, const std::string& algo_id, const std::string& failure -); -void set_lint_success( - const std::string& uid, const std::string& algo_id, const std::string& success -); - -[[nodiscard]] std::optional -get_algo(const std::string& uid, const std::string& algo_id); -[[nodiscard]] LintingResultOption get_algo_status( - const std::string& uid, const std::string& algo_id -); // returns a LintingResultOption enum - -} // namespace client -} // namespace nutc diff --git a/linter/src/lint/lint.cpp b/linter/src/lint/lint.cpp index d378da53..d37640d4 100644 --- a/linter/src/lint/lint.cpp +++ b/linter/src/lint/lint.cpp @@ -1,6 +1,5 @@ #include "lint.hpp" - -#include "pywrapper/runtime.hpp" +#include "runtime/runtime.hpp" #include #include @@ -10,62 +9,81 @@ #include namespace nutc { -namespace lint { - -namespace { -bool -mock_limit_func(const std::string& side, const std::string&, float, float) -{ - if (side == "BUY" || side == "SELL") - return true; - throw std::runtime_error( - fmt::format("Side should be BUY or SELL, but called with side: {}", side) - ); -} - -bool -mock_market_func(const std::string& side, const std::string&, float) -{ - if (side == "BUY" || side == "SELL") - return true; - throw std::runtime_error( - fmt::format("Side should be BUY or SELL, but called with side: {}", side) - ); -} -} // namespace +namespace lint { lint_result -lint(const std::string& algo_code) +lint(Runtime& runtime) { std::string out_message = "[linter] starting to lint algorithm\n"; - bool ok = nutc::pywrapper::create_api_module(mock_limit_func, mock_market_func); - if (!ok) { - out_message += "[linter] failed to create API module\n"; + auto init_err = runtime.init(); + if (init_err.has_value()) { + out_message += fmt::format("{}\n", init_err.value()); + return {false, out_message}; + } + + try { + runtime.fire_on_orderbook_update("ETH", "BUY", 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_orderbook_update: {}", e.what()); + return {false, out_message}; + } + + try { + runtime.fire_on_trade_update("ETH", "BUY", 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_trade_update: {}", e.what()); + return {false, out_message}; + } + + try { + runtime.fire_on_orderbook_update("BTC", "BUY", 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_orderbook_update: {}", e.what()); + return {false, out_message}; + } + + try { + runtime.fire_on_trade_update("BTC", "BUY", 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_trade_update: {}", e.what()); + return {false, out_message}; + } + + try { + runtime.fire_on_account_update("BTC", "BUY", 1.0, 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_trade_update: {}", e.what()); return {false, out_message}; } - ok = nutc::pywrapper::supress_stdout(); - if (!ok) { - out_message += "[linter] failed to initialize python environment\n"; + + try { + runtime.fire_on_account_update("BTC", "BUY", 1.0, 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_account_update: {}", e.what()); return {false, out_message}; } - std::optional err = nutc::pywrapper::import_py_code(algo_code); - if (err.has_value()) { - out_message += fmt::format("{}\n", err.value()); + try { + runtime.fire_on_orderbook_update("LTC", "BUY", 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_orderbook_update: {}", e.what()); return {false, out_message}; } - err = nutc::pywrapper::run_initialization(); - if (err.has_value()) { - out_message += fmt::format("{}\n", err.value()); + try { + runtime.fire_on_trade_update("LTC", "BUY", 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_trade_update: {}", e.what()); return {false, out_message}; } - err = nutc::pywrapper::trigger_callbacks(); - if (err.has_value()) { - out_message += fmt::format("{}\n", err.value()); + try { + runtime.fire_on_account_update("LTC", "BUY", 1.0, 1.0, 1.0); + } catch (const std::exception& e) { + out_message += fmt::format("Failed to run on_account_update: {}", e.what()); return {false, out_message}; } + out_message += "\n[linter] linting process succeeded!\n"; return {true, out_message}; } diff --git a/linter/src/lint/lint.hpp b/linter/src/lint/lint.hpp index 3dcb502d..41784326 100644 --- a/linter/src/lint/lint.hpp +++ b/linter/src/lint/lint.hpp @@ -1,13 +1,12 @@ #pragma once #include "lint/lint_result.hpp" - -#include +#include "runtime/runtime.hpp" namespace nutc { namespace lint { -[[nodiscard]] lint_result lint(const std::string& algo_code); +[[nodiscard]] lint_result lint(Runtime& runtime); } // namespace lint } // namespace nutc diff --git a/linter/src/pywrapper/runtime.cpp b/linter/src/pywrapper/runtime.cpp deleted file mode 100644 index 2c2df2f1..00000000 --- a/linter/src/pywrapper/runtime.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "runtime.hpp" - -#include -#include - -namespace py = pybind11; - -namespace nutc { -namespace pywrapper { - -bool -create_api_module( - std::function - publish_limit_order, - std::function - publish_market_order -) -{ - try { - py::module m = py::module::create_extension_module( - "nutc_api", "Official NUTC Exchange API", new py::module::module_def - ); - - m.def("publish_limit_order", publish_limit_order); - m.def("publish_market_order", publish_market_order); - - py::module_ sys = py::module_::import("sys"); - py::dict sys_modules = sys.attr("modules").cast(); - sys_modules["nutc_api"] = m; - - py::exec(R"(import nutc_api)"); - } catch (...) { - return false; - } - return true; -} - -bool -supress_stdout() -{ - try { - py::exec(R"( - import sys - import os - class SuppressOutput(object): - def write(self, txt): - pass # Do nothing on write - def flush(self): - pass # Do nothing on flush - - sys.stdout = SuppressOutput() - )"); - } catch (...) { - return false; - } - return true; -} - -std::optional -import_py_code(const std::string& code) -{ - try { - py::exec(code); - } catch (const std::exception& e) { - return fmt::format("Failed to import code: {}", e.what()); - } - py::exec(R"( - def place_market_order(side, ticker, quantity): - return nutc_api.publish_market_order(side, ticker, quantity))"); - py::exec(R"( - def place_limit_order(side, ticker, quantity, price): - return nutc_api.publish_limit_order(side, ticker, price, quantity))"); - - return std::nullopt; -} - -std::optional -run_initialization() -{ - try { - py::exec("strategy = Strategy()"); - } - - catch (const std::exception& e) { - return fmt::format("Failed to run initialization: {}", e.what()); - } - - try { - py::object main_module = py::module_::import("__main__"); - py::dict main_dict = main_module.attr("__dict__"); - py::object on_trade_update = main_dict["strategy"].attr("on_trade_update"); - py::object on_orderbook_update = - main_dict["strategy"].attr("on_orderbook_update"); - py::object on_account_update = main_dict["strategy"].attr("on_account_update"); - } catch (py::error_already_set& e) { - return fmt::format("Failed to import callback functions: {}", e.what()); - } - - return std::nullopt; -} - -std::optional -trigger_callbacks() -{ - try { - py::exec(R"(place_limit_order("BUY", "ETH", 1.0, 1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run place_limit_order: {}", e.what()); - } - - try { - py::exec(R"(strategy.on_orderbook_update("ETH","BUY",1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_orderbook_update: {}", e.what()); - } - try { - py::exec(R"(strategy.on_trade_update("ETH","BUY",1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_trade_update: {}", e.what()); - } - - try { - py::exec(R"(strategy.on_account_update("ETH","BUY",1.0,1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_account_update: {}", e.what()); - } - - try { - py::exec(R"(strategy.on_orderbook_update("BTC","BUY",1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_orderbook_update: {}", e.what()); - } - try { - py::exec(R"(strategy.on_trade_update("BTC","BUY",1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_trade_update: {}", e.what()); - } - - try { - py::exec(R"(strategy.on_account_update("BTC","BUY",1.0,1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_account_update: {}", e.what()); - } - - try { - py::exec(R"(strategy.on_orderbook_update("LTC","BUY",1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_orderbook_update: {}", e.what()); - } - try { - py::exec(R"(strategy.on_trade_update("LTC","BUY",1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_trade_update: {}", e.what()); - } - - try { - py::exec(R"(strategy.on_account_update("LTC","BUY",1.0,1.0,1.0))"); - } catch (const std::exception& e) { - return fmt::format("Failed to run on_account_update: {}", e.what()); - } - - return std::nullopt; -} - -} // namespace pywrapper -} // namespace nutc diff --git a/linter/src/pywrapper/runtime.hpp b/linter/src/pywrapper/runtime.hpp deleted file mode 100644 index fd3c3d51..00000000 --- a/linter/src/pywrapper/runtime.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace nutc { -namespace pywrapper { -[[nodiscard]] bool create_api_module( - std::function - publish_limit_order, - std::function - publish_market_order -); -[[nodiscard]] bool supress_stdout(); -[[nodiscard]] std::optional import_py_code(const std::string& code); - -[[nodiscard]] std::optional run_initialization(); - -[[nodiscard]] std::optional trigger_callbacks(); -} // namespace pywrapper -} // namespace nutc diff --git a/linter/src/runtime/cpp/cpp_runtime.cpp b/linter/src/runtime/cpp/cpp_runtime.cpp new file mode 100644 index 00000000..4d702f98 --- /dev/null +++ b/linter/src/runtime/cpp/cpp_runtime.cpp @@ -0,0 +1,148 @@ +#include "cpp_runtime.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace { +namespace fs = std::filesystem; + +std::pair +get_temp_file() +{ +#ifdef __APPLE__ + std::string template_path = (fs::temp_directory_path() / "algoXXXXXX").string(); + std::vector writable_template_path( + template_path.begin(), template_path.end() + ); + writable_template_path.push_back('\0'); + int fd = mkstemp(writable_template_path.data()); + if (fd == -1) { + throw std::runtime_error("Failed to get file descriptor for temporary file"); + } + + return {fd, writable_template_path.data()}; + +#else + int memfd = memfd_create("algo", MFD_CLOEXEC); + if (memfd == -1) { + throw std::runtime_error("Failed to create memfd"); + } + + return {memfd, "/proc/self/fd/" + std::to_string(memfd)}; + +#endif +} + +} // namespace + +namespace nutc::lint { + +CppRuntime::CppRuntime( + std::string algo, + LimitOrderFunction limit_order, + MarketOrderFunction market_order, + CancelOrderFunction cancel_order +) : + Runtime(std::move(algo), limit_order, market_order, cancel_order) +{} + +CppRuntime::~CppRuntime() +{ + dlclose(dl_handle_); + close(fd_); +} + +std::optional +CppRuntime::init() +{ + auto [fd, path] = get_temp_file(); + + fd_ = fd; + + std::ofstream algo_file(path); + algo_file << algo_ << std::flush; + algo_file.close(); + + dl_handle_ = dlopen(path.c_str(), RTLD_NOW); + if (dl_handle_ == nullptr) { + std::string err = dlerror(); + close(fd_); + return fmt::format("[linter] failed to dlopen: {}", err); + } + + auto init_func = reinterpret_cast(dlsym(dl_handle_, "init")); + on_trade_update_func_ = + reinterpret_cast(dlsym(dl_handle_, "on_trade_update")); + on_orderbook_update_func_ = + reinterpret_cast(dlsym(dl_handle_, "on_orderbook_update") + ); + on_account_update_func_ = + reinterpret_cast(dlsym(dl_handle_, "on_account_update")); + + if (!init_func || !on_trade_update_func_ || !on_orderbook_update_func_ + || !on_account_update_func_) { + dlclose(dl_handle_); + close(fd_); + return fmt::format("[linter] failed to dynamically load functions"); + } + strategy_object_ = + init_func(m_market_order_func, m_limit_order_func, m_cancel_order_func); + + return std::nullopt; +} + +void +CppRuntime::fire_on_trade_update( + std::string ticker, std::string side, double price, double quantity +) const +{ + on_trade_update_func_( + strategy_object_, + ticker, + side, + static_cast(quantity), + static_cast(price) + ); +} + +void +CppRuntime::fire_on_orderbook_update( + std::string ticker, std::string side, double price, double quantity +) const +{ + on_orderbook_update_func_( + strategy_object_, + ticker, + side, + static_cast(quantity), + static_cast(price) + ); +} + +void +CppRuntime::fire_on_account_update( + std::string ticker, std::string side, double price, double quantity, double capital +) const +{ + on_account_update_func_( + strategy_object_, + ticker, + side, + static_cast(quantity), + static_cast(price), + static_cast(capital) + ); +} + +} // namespace nutc::lint diff --git a/linter/src/runtime/cpp/cpp_runtime.hpp b/linter/src/runtime/cpp/cpp_runtime.hpp new file mode 100644 index 00000000..75a69b11 --- /dev/null +++ b/linter/src/runtime/cpp/cpp_runtime.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "runtime/runtime.hpp" + +namespace nutc::lint { + +class CppRuntime : public Runtime { +public: + CppRuntime( + std::string algo, + LimitOrderFunction limit_order, + MarketOrderFunction market_order, + CancelOrderFunction cancel_order + ); + + ~CppRuntime() override; + + std::optional init() override; + + void fire_on_trade_update( + std::string ticker, std::string side, double price, double quantity + ) const override; + + void fire_on_orderbook_update( + std::string ticker, std::string side, double price, double quantity + ) const override; + + void fire_on_account_update( + std::string ticker, + std::string side, + double price, + double quantity, + double capital + ) const override; +private: + + using Strategy = void; + using InitFunc = Strategy* (*)(MarketOrderFunction, + LimitOrderFunction, + CancelOrderFunction); + using OnTradeUpdateFunc = + void (*)(Strategy*, const std::string&, const std::string&, double, double); + using OnOrderBookUpdateFunc = OnTradeUpdateFunc; + using OnAccountUpdateFunc = void (*)( + Strategy*, const std::string&, const std::string&, double, double, double + ); + + OnTradeUpdateFunc on_trade_update_func_; + OnOrderBookUpdateFunc on_orderbook_update_func_; + OnAccountUpdateFunc on_account_update_func_; + + Strategy* strategy_object_; + void* dl_handle_; + int fd_; +}; +} // namespace nutc::lint diff --git a/linter/src/runtime/python/python_runtime.cpp b/linter/src/runtime/python/python_runtime.cpp new file mode 100644 index 00000000..ce6abedd --- /dev/null +++ b/linter/src/runtime/python/python_runtime.cpp @@ -0,0 +1,146 @@ +#include "python_runtime.hpp" + +#include +#include +#include + +namespace nutc::lint { + +namespace py = pybind11; + +PyRuntime::~PyRuntime() +{ + pybind11::finalize_interpreter(); +} + +std::optional +PyRuntime::init() +{ + auto api_status = + create_api_module(m_limit_order_func, m_market_order_func, m_cancel_order_func); + if (!api_status) { + return "[linter] failed to create API module\n"; + } + auto init_status = run_initialization_code(algo_); + if (init_status.has_value()) { + return init_status.value(); + } + + return std::nullopt; +} + +void +PyRuntime::fire_on_trade_update( + std::string ticker, std::string side, double price, double quantity +) const +{ + py::globals()["strategy"].attr("on_trade_update")( + ticker, side, static_cast(quantity), static_cast(price) + ); +} + +void +PyRuntime::fire_on_orderbook_update( + std::string ticker, std::string side, double price, double quantity +) const +{ + py::globals()["strategy"].attr("on_orderbook_update")( + ticker, side, static_cast(quantity), static_cast(price) + ); +} + +void +PyRuntime::fire_on_account_update( + std::string ticker, std::string side, double price, double quantity, double capital +) const +{ + py::globals()["strategy"].attr("on_account_update")( + ticker, + side, + static_cast(quantity), + static_cast(price), + static_cast(capital) + ); +} + +bool +PyRuntime::create_api_module( + LimitOrderFunction publish_limit_order, + MarketOrderFunction publish_market_order, + CancelOrderFunction cancel_order +) +{ + try { + py::module_ sys = py::module_::import("sys"); + // TODO: disable this for testing + py::exec(R"( + import sys + import os + class SuppressOutput(object): + def write(self, txt): + pass + def flush(self): + pass + + sys.stdout = SuppressOutput() + )"); + py::module module = py::module::create_extension_module( + "nutc_api", "NUTC Exchange API", new py::module::module_def + ); + + module.def("publish_market_order", publish_market_order); + module.def("publish_limit_order", publish_limit_order); + module.def("cancel_order", cancel_order); + + auto sys_modules = sys.attr("modules").cast(); + sys_modules["nutc_api"] = module; + + py::exec(R"(import nutc_api)"); + } catch (...) { + return false; + } + return true; +} + +std::optional +PyRuntime::run_initialization_code(const std::string& py_code) +{ + try { + py::exec(py_code); + } catch (const std::exception& e) { + return fmt::format("Failed to import code: {}", e.what()); + } + py::exec(R"( + def place_market_order(side: str, ticker: str, quantity: float): + return nutc_api.publish_market_order(side, ticker, quantity) + )"); + py::exec(R"( + def place_limit_order(side: str, ticker: str, quantity: float, price: float, ioc: bool = False): + return nutc_api.publish_limit_order(side, ticker, quantity, price, ioc) + )"); + py::exec(R"( + def cancel_order(ticker: str, order_id: int): + return nutc_api.cancel_order(ticker, order_id) + )"); + + try { + py::exec("strategy = Strategy()"); + } catch (const std::exception& e) { + return fmt::format("Failed to run initialization: {}", e.what()); + } + + try { + py::object main_module = py::module_::import("__main__"); + py::dict main_dict = main_module.attr("__dict__"); + py::object on_trade_update = main_dict["strategy"].attr("on_trade_update"); + py::object on_orderbook_update = + main_dict["strategy"].attr("on_orderbook_update"); + py::object on_account_update = main_dict["strategy"].attr("on_account_update"); + } catch (py::error_already_set& e) { + return fmt::format("Failed to import callback functions: {}", e.what()); + } + + return std::nullopt; +} + +} // namespace nutc::lint diff --git a/linter/src/runtime/python/python_runtime.hpp b/linter/src/runtime/python/python_runtime.hpp new file mode 100644 index 00000000..0b0bf175 --- /dev/null +++ b/linter/src/runtime/python/python_runtime.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "runtime/runtime.hpp" + +#include + +namespace nutc::lint { + +class PyRuntime : public Runtime { +public: + PyRuntime( + std::string algo, + LimitOrderFunction place_limit_order, + MarketOrderFunction place_market_order, + CancelOrderFunction cancel_order + ) : + Runtime(std::move(algo), place_limit_order, place_market_order, cancel_order) + { + pybind11::initialize_interpreter(); + } + + ~PyRuntime() override; + + std::optional init() override; + + void fire_on_trade_update( + std::string ticker, std::string side, double price, double quantity + ) const override; + + void fire_on_orderbook_update( + std::string ticker, std::string side, double price, double quantity + ) const override; + + void fire_on_account_update( + std::string ticker, + std::string side, + double price, + double quantity, + double capital + ) const override; + +private: + + static bool create_api_module( + LimitOrderFunction publish_limit_order, + MarketOrderFunction publish_market_order, + CancelOrderFunction cancel_order + ); + static std::optional run_initialization_code(const std::string& py_code + ); +}; + +} // namespace nutc::lint diff --git a/linter/src/runtime/runtime.hpp b/linter/src/runtime/runtime.hpp new file mode 100644 index 00000000..ef8ebdfc --- /dev/null +++ b/linter/src/runtime/runtime.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include +#include + +namespace nutc::lint { + +using LimitOrderFunction = std::function; +using MarketOrderFunction = std::function< + bool(const std::string& side, const std::string& ticker, double quantity)>; +using CancelOrderFunction = + std::function; + +class Runtime { +public: + virtual ~Runtime() = default; + + Runtime(const Runtime&) = default; + Runtime(Runtime&&) noexcept = default; + Runtime& operator=(const Runtime&) = default; + Runtime& operator=(Runtime&&) noexcept = default; + + Runtime( + std::string algo, + LimitOrderFunction limit_order, + MarketOrderFunction market_order, + CancelOrderFunction cancel_order + ) : + algo_(std::move(algo)), + m_limit_order_func(limit_order), + m_market_order_func(market_order), + m_cancel_order_func(cancel_order) + {} + + virtual std::optional init() = 0; + virtual void fire_on_trade_update( + std::string ticker, std::string side, double price, double quantity + ) const = 0; + virtual void fire_on_orderbook_update( + std::string ticker, std::string side, double price, double quantity + ) const = 0; + virtual void fire_on_account_update( + std::string ticker, + std::string side, + double price, + double quantity, + double buyer_capital + ) const = 0; + +protected: + std::string algo_; + LimitOrderFunction m_limit_order_func; + MarketOrderFunction m_market_order_func; + CancelOrderFunction m_cancel_order_func; +}; + +} // namespace nutc::lint diff --git a/linter/src/spawner/main.cpp b/linter/src/spawner/main.cpp index f31a75a7..2a7d3998 100644 --- a/linter/src/spawner/main.cpp +++ b/linter/src/spawner/main.cpp @@ -1,5 +1,10 @@ #include "lint/lint.hpp" +#include "lint/lint_result.hpp" +#include "runtime/cpp/cpp_runtime.hpp" +#include "runtime/python/python_runtime.hpp" +#include +#include #include #include #include @@ -9,21 +14,67 @@ #include #include +namespace { +bool +mock_limit_func(const std::string& side, const std::string&, float, float, bool) +{ + if (side == "BUY" || side == "SELL") + return true; + throw std::runtime_error( + fmt::format("Side should be BUY or SELL, but called with side: {}", side) + ); +} + +bool +mock_market_func(const std::string& side, const std::string&, float) +{ + if (side == "BUY" || side == "SELL") + return true; + throw std::runtime_error( + fmt::format("Side should be BUY or SELL, but called with side: {}", side) + ); +} + +bool +mock_cancel_func(const std::string&, std::int64_t) +{ + return true; +} +} // namespace + int -main() +main(int argc, char* argv[]) { + if (argc < 2) { + std::cout << "[linter] no language provided\n"; + return 1; + } + std::string algo_code; std::string line; while (std::getline(std::cin, line)) { algo_code += line + '\n'; } - // Initialize py - pybind11::initialize_interpreter(); - - auto lint_result = nutc::lint::lint(algo_code); + nutc::lint::lint_result lint_result; + std::string flag = argv[1]; + if (flag == "-python") { + nutc::lint::PyRuntime runtime( + algo_code, mock_limit_func, mock_market_func, mock_cancel_func + ); + lint_result = nutc::lint::lint(runtime); + } + else if (flag == "-cpp") { + nutc::lint::CppRuntime runtime( + algo_code, mock_limit_func, mock_market_func, mock_cancel_func + ); + lint_result = nutc::lint::lint(runtime); + } + else { + std::cout << "[linter] no language provided\n"; + return 1; + } - pybind11::finalize_interpreter(); std::cout << glz::write_json(lint_result) << "\n"; return 0; diff --git a/linter/src/spawning/spawning.cpp b/linter/src/spawning/spawning.cpp index d60e78da..68be3368 100644 --- a/linter/src/spawning/spawning.cpp +++ b/linter/src/spawning/spawning.cpp @@ -28,14 +28,26 @@ LintProcessManager::spawner_binary_path() } nutc::lint::lint_result -LintProcessManager::spawn_client(const std::string& algo_code) +LintProcessManager::spawn_client(const std::string& algo_code, AlgoLanguage language) { static const std::string path{spawner_binary_path()}; auto in_pipe = std::make_shared(io_context); bp::opstream out_pipe; + auto get_language_flag = [](AlgoLanguage language) { + switch (language) { + case nutc::spawning::AlgoLanguage::Python: + return "-python"; + case nutc::spawning::AlgoLanguage::Cpp: + return "-cpp"; + } + }; + auto child = std::make_shared( - bp::exe(path), bp::std_in * in_pipe, io_context + bp::exe(path), + bp::args(get_language_flag(language)), + bp::std_in * in_pipe, + io_context ); out_pipe << algo_code << std::flush; @@ -51,7 +63,8 @@ LintProcessManager::spawn_client(const std::string& algo_code) in_pipe->close(); res.success = false; res.message += fmt::format( - "[linter] FAILED to lint algo\n\nYour code did not execute within {} " + "[linter] FAILED to lint algo\n\nYour code did not execute within " + "{} " "seconds. Check all " "functions to see if you have an infinite loop or infinite " "recursion.\n\nIf you continue to experience this error, " @@ -89,7 +102,8 @@ LintProcessManager::spawn_client(const std::string& algo_code) if (error) { res = { false, - "Internal server error. Reach out to nuft@u.northwesten.edu " + "Internal server error. Reach out to " + "nuft@u.northwesten.edu " "for support" }; }; diff --git a/linter/src/spawning/spawning.hpp b/linter/src/spawning/spawning.hpp index ec4a8d21..36573b9f 100644 --- a/linter/src/spawning/spawning.hpp +++ b/linter/src/spawning/spawning.hpp @@ -11,12 +11,17 @@ namespace nutc { namespace spawning { +enum class AlgoLanguage { + Python, + Cpp +}; + namespace ba = boost::asio; class LintProcessManager { public: const std::filesystem::path& spawner_binary_path(); - nutc::lint::lint_result spawn_client(const std::string&); + nutc::lint::lint_result spawn_client(const std::string&, AlgoLanguage language); private: ba::io_context io_context{}; diff --git a/linter/test/src/NUTC-linter_test.cpp b/linter/test/src/NUTC-linter_test.cpp index 9defcc93..a1520a8e 100644 --- a/linter/test/src/NUTC-linter_test.cpp +++ b/linter/test/src/NUTC-linter_test.cpp @@ -127,23 +127,26 @@ const std::string missing_on_account_update_algo = R"(class Strategy: TEST_F(IntegrationLinterTest, basic) { - auto lint_result = manager.spawn_client(basic_algo); + auto lint_result = + manager.spawn_client(basic_algo, nutc::spawning::AlgoLanguage::Python); ASSERT_TRUE(lint_result.success); } TEST_F(IntegrationLinterTest, invalidSideArg) { - auto lint_result = manager.spawn_client(incorrect_arguments_algo); + auto lint_result = manager.spawn_client( + incorrect_arguments_algo, nutc::spawning::AlgoLanguage::Python + ); ASSERT_FALSE(lint_result.success); ASSERT_TRUE( - lint_result.message.find("Side should be BUY or SELL") - != std::string::npos + lint_result.message.find("Side should be BUY or SELL") != std::string::npos ); } TEST_F(IntegrationLinterTest, timeout) { - auto lint_result = manager.spawn_client(timeout_algo); + auto lint_result = + manager.spawn_client(timeout_algo, nutc::spawning::AlgoLanguage::Python); ASSERT_FALSE(lint_result.success); ASSERT_TRUE( lint_result.message.find("Your code did not execute within") @@ -154,7 +157,7 @@ TEST_F(IntegrationLinterTest, timeout) TEST_F(IntegrationLinterTest, invalidAlgo) { std::string algo = R"(not_valid_python)"; - auto lint_result = manager.spawn_client(algo); + auto lint_result = manager.spawn_client(algo, nutc::spawning::AlgoLanguage::Python); ASSERT_FALSE(lint_result.success); ASSERT_TRUE( lint_result.message.find("Failed to import code:") != std::string::npos @@ -164,7 +167,7 @@ TEST_F(IntegrationLinterTest, invalidAlgo) TEST_F(IntegrationLinterTest, noStrategyClass) { std::string algo = R"(import math)"; - auto lint_result = manager.spawn_client(algo); + auto lint_result = manager.spawn_client(algo, nutc::spawning::AlgoLanguage::Python); ASSERT_FALSE(lint_result.success); ASSERT_TRUE( lint_result.message.find("NameError: name 'Strategy' is not defined") @@ -174,21 +177,27 @@ TEST_F(IntegrationLinterTest, noStrategyClass) TEST_F(IntegrationLinterTest, missingRequiredFunction) { - auto lint_result = manager.spawn_client(missing_on_trade_update_algo); + auto lint_result = manager.spawn_client( + missing_on_trade_update_algo, nutc::spawning::AlgoLanguage::Python + ); ASSERT_FALSE(lint_result.success); ASSERT_TRUE( lint_result.message.find("has no attribute 'on_trade_update'") != std::string::npos ); - lint_result = manager.spawn_client(missing_on_orderbook_update_algo); + lint_result = manager.spawn_client( + missing_on_orderbook_update_algo, nutc::spawning::AlgoLanguage::Python + ); ASSERT_FALSE(lint_result.success); ASSERT_TRUE( lint_result.message.find("has no attribute 'on_orderbook_update'") != std::string::npos ); - lint_result = manager.spawn_client(missing_on_account_update_algo); + lint_result = manager.spawn_client( + missing_on_account_update_algo, nutc::spawning::AlgoLanguage::Python + ); ASSERT_FALSE(lint_result.success); ASSERT_TRUE( lint_result.message.find("has no attribute 'on_account_update'") diff --git a/web/app/api/protected/db/user/createAlgo/route.ts b/web/app/api/protected/db/user/createAlgo/route.ts index d6320e08..fe96e7fe 100644 --- a/web/app/api/protected/db/user/createAlgo/route.ts +++ b/web/app/api/protected/db/user/createAlgo/route.ts @@ -10,6 +10,7 @@ export async function POST(req: Request) { !algo.name || !algo.description || !algo.case || + !algo.language || !algo.algoFileS3Key || !algo.uid ) { @@ -26,6 +27,7 @@ export async function POST(req: Request) { name: algo.name, description: algo.description, case: algo.case, + language: algo.language, lintResults: "pending", algoFile: { connect: { diff --git a/web/app/api/protected/db/user/uploadAlgoFile/route.ts b/web/app/api/protected/db/user/uploadAlgoFile/route.ts index d4b50ff1..cc934179 100644 --- a/web/app/api/protected/db/user/uploadAlgoFile/route.ts +++ b/web/app/api/protected/db/user/uploadAlgoFile/route.ts @@ -26,7 +26,7 @@ export async function POST() { const params = { Bucket: "nutc", Key: algoFile.s3Key, - ContentType: "text/x-python", + ContentType: "text/plain", } satisfies PutObjectCommandInput; try { diff --git a/web/app/dash/submit/form.tsx b/web/app/dash/submit/form.tsx index 4d7794f0..bbb7e541 100644 --- a/web/app/dash/submit/form.tsx +++ b/web/app/dash/submit/form.tsx @@ -1,5 +1,5 @@ "use client"; -import React from "react"; +import React, { useRef } from "react"; import { CheckIcon, PaperClipIcon, @@ -17,12 +17,18 @@ import { } from "@headlessui/react"; import { SubmitHandler, useForm } from "react-hook-form"; import { useRouter } from "next/navigation"; +import { isNull } from "util"; const CASES = [ { id: 1, name: "HFT" }, { id: 2, name: "Crypto Trading" }, ]; +const LANGUAGES = [ + { id: 1, name: "Python" }, + { id: 2, name: "C++" }, +]; + function classNames(...classes: any) { return classes.filter(Boolean).join(" "); } @@ -33,22 +39,26 @@ const CASE_DOCUMENT_URL = export default function SubmissionForm(props: { user: any }) { const router = useRouter(); const [isDragOver, setDragOver] = useState(false); + const fileSubmitRef = useRef(null); type Inputs = { name: string; case: string; + language: string; description: string; algoFileS3Key: string; }; - const { handleSubmit, register, watch, setValue } = useForm({ - defaultValues: { - name: "", - case: "HFT", - description: "", - algoFileS3Key: "", - }, - }); + const { handleSubmit, register, watch, setValue, resetField } = + useForm({ + defaultValues: { + name: "", + case: "HFT", + language: "Python", + description: "", + algoFileS3Key: "", + }, + }); const onSubmit: SubmitHandler = async data => { const response = await fetch("/api/protected/db/user/createAlgo", { @@ -130,13 +140,23 @@ export default function SubmissionForm(props: { user: any }) { } }; - const [caseValue, algoFileS3Key] = watch(["case", "algoFileS3Key"]); + const [caseValue, languageValue, algoFileS3Key] = watch([ + "case", + "language", + "algoFileS3Key", + ]); const handleAlgoChange = async (file: File) => { const fileExtension = file.name.split(".").pop()?.toLowerCase(); - if (fileExtension !== "py") { + + if ( + (languageValue === "Python" && fileExtension !== "py") || + (languageValue === "C++" && + fileExtension !== "h" && + fileExtension !== "hpp") + ) { Swal.fire({ - title: "Please upload a Python file", + title: `Please upload a ${languageValue} file`, icon: "error", toast: true, position: "top-end", @@ -188,6 +208,8 @@ export default function SubmissionForm(props: { user: any }) { handleAlgoChange(files[0]); }; + console.log("key", algoFileS3Key); + return (
@@ -296,6 +318,91 @@ export default function SubmissionForm(props: { user: any }) { )} + { + // clear the file input + if (fileSubmitRef.current) { + fileSubmitRef.current.value = ""; + } + + setValue("algoFileS3Key", ""); + setValue("language", v); + }}> + {({ open }) => ( +
+
+ + + + +
+ +
+ + {languageValue} + + + + + + + {LANGUAGES.map(languageOption => ( + + classNames( + focus + ? "bg-indigo-600 text-white" + : "text-white", + "relative cursor-default select-none py-2 pl-3 pr-9", + ) + }> + {({ selected, focus }) => ( + <> + + {languageOption.name} + + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+ )} +
+

- .py up to 100KB + {languageValue === "Python" + ? ".py up to 100KB" + : ".h up to 100KB"}

diff --git a/web/prisma/migrations/20240926234323_algo_language/migration.sql b/web/prisma/migrations/20240926234323_algo_language/migration.sql new file mode 100644 index 00000000..00ca5efa --- /dev/null +++ b/web/prisma/migrations/20240926234323_algo_language/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `language` to the `algos` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "algos" ADD COLUMN "language" TEXT NOT NULL; diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index b03da192..39b039da 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -41,6 +41,7 @@ model Algo { name String @unique description String case String + language String lintResults String sandboxLogFileURL String? lintFailureMessage String? diff --git a/webserver/src/main.rs b/webserver/src/main.rs index c57cf9f8..2ef1abdd 100644 --- a/webserver/src/main.rs +++ b/webserver/src/main.rs @@ -6,14 +6,72 @@ use serde::Deserialize; use serde_json::json; use tokio_postgres::NoTls; -const LINTER_BASE_URL: &str = "http://linter:18081"; -const SANDBOX_BASE_URL: &str = "http://sandbox:18080"; +// const LINTER_BASE_URL: &str = "http://linter:18081"; +// const SANDBOX_BASE_URL: &str = "http://sandbox:18080"; +const LINTER_BASE_URL: &str = "http://localhost:18081"; #[derive(Deserialize, Debug)] pub struct LinterResponse { pub linting_status: i32, } +#[derive(Deserialize, Debug)] +struct LintResult { + success: bool, + message: String, +} + +#[tracing::instrument] +#[post("/lint-result/{uid}/{algo_id}")] +async fn set_lint_result( + path_info: web::Path<(String, String)>, + lint_result: web::Json, + db_pool: web::Data, +) -> impl Responder { + let (uid, algo_id) = path_info.into_inner(); + + let postgres_client = match db_pool.get().await { + Ok(client) => client, + Err(e) => { + eprintln!("Failed to connect to the database: {}", e); + return HttpResponse::InternalServerError().finish(); + } + }; + + let query = if lint_result.success { + r#" + UPDATE "algos" + SET "lintResults" = $1, + "lintSuccessMessage" = $2 + WHERE "uid" = $3 AND "algoFileS3Key" = $4; + "# + } else { + r#" + UPDATE "algos" + SET "lintResults" = $1, + "lintFailureMessage" = $2 + WHERE "uid" = $3 AND "algoFileS3Key" = $4; + "# + }; + + let result = if lint_result.success { + "success" + } else { + "failure" + }; + + match postgres_client + .execute(query, &[&result, &lint_result.message, &uid, &algo_id]) + .await + { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => { + tracing::error!("Failed to update lint result in the database: {e}"); + HttpResponse::InternalServerError().finish() + } + } +} + #[tracing::instrument] #[post("/submit/{uid}/{algo_id}")] async fn linter(data: web::Path<(String, String)>) -> impl Responder { @@ -214,6 +272,7 @@ async fn main() -> std::io::Result<()> { .service(linter) .service(get_single_user_algorithm) .service(get_all_user_algorithms) + .service(set_lint_result) .wrap(cors) }) .bind(("0.0.0.0", 16124))?