Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .bazelrc
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
build --cxxopt='-std=c++17'
# Use this if MSVC
# build --cxxopt=/std:c++20

build --cxxopt=-std=c++20

19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# Bazel
bazel-*
MODULE.bazel.lock

# Build folders, libs, includes
include/
lib/
x64/
yfinance-cpp/

# Visual Studio
.vs/
*.sln
*.vcxproj
*.vcxproj.user
*.vcxproj.filters

# Other extensions
*.dll
*.pdb
*.exp
2 changes: 2 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_binary", "cc_test")

cc_library(
name = "yfinance",
srcs = glob(["cpp/*.cpp"]),
Expand Down
3 changes: 2 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module(
version = "0.0.1",
)

bazel_dep(name = "nlohmann_json", version = "3.11.3")
bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1")
bazel_dep(name = "cpr", version = "1.14.1")
bazel_dep(name = "curl", version = "8.8.0")

35 changes: 35 additions & 0 deletions cpp/base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,39 @@ namespace yfinance {
throw std::runtime_error(error_message);
}
}
std::vector<std::string> Symbol::search(const std::string& query, int quote_counts) {
cpr::Response r = Session::getInstance().Get(
cpr::Url{Utils::Statics::Search::v11},
cpr::Parameters{
{"q", query},
{"quotesCount", std::to_string(quote_counts)},
{"newsCount", "0"},
{"listsCount", "0"},
{"recommendedCount", "0"},
{"quotesQueryId", "tss_match_phrase_query"}
}
);

std::vector<std::string> quotes;

if ((r.status_code == 200) && (!r.text.empty())) {
json json_quotes = json::parse(r.text)["quotes"];

quotes.reserve(quote_counts);

for (auto& quote : json_quotes) {
// Check if quote is a symbol first
if (quote.contains("symbol")) {
quotes.push_back(quote["symbol"]);
}
}
}
else {
std::string error_message =
"Request failed with status code: " + std::to_string(r.status_code);
throw std::runtime_error(error_message);
}

return quotes;
}
}
37 changes: 30 additions & 7 deletions cpp/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ namespace yfinance {
refreshCredentials();
}

// Create a copy constructor
Session::Session(const Session& session) {
m_user_agent = USER_AGENT;
m_crumb = session.m_crumb;
m_cookie = session.m_cookie;
}

void Session::refreshCredentials() {
// 1. Get cookie from fc.yahoo.com
m_session.SetUrl(cpr::Url{"https://fc.yahoo.com"});
m_session.Get();
cpr::Response r1 = m_session.Get();

// Save the cookie to std::string
for (const auto& cookie : r1.cookies) {
if (!m_cookie.empty()) m_cookie += "; ";
m_cookie += cookie.GetName() + "=" + cookie.GetValue();
}

// 2. Get crumb from getcrumb
m_session.SetUrl(cpr::Url{"https://query2.finance.yahoo.com/v1/test/getcrumb"});
Expand All @@ -38,14 +51,24 @@ namespace yfinance {
parameters.Add({"crumb", m_crumb});
}
m_session.SetParameters(parameters);

// Merge headers if needed, but for now just append/set
// Note: SetHeader replaces all headers. We might want to preserve User-Agent.
cpr::Header session_header;
session_header.insert({"User-Agent", m_user_agent});

// Add cookie via headers (cpr::Session::SetCookies did not work)
if (!m_cookie.empty()) {
session_header.insert({"Cookie", m_cookie});
}

// Merge other headers
if (!headers.empty()) {
// Merge headers if needed, but for now just append/set
// Note: SetHeader replaces all headers. We might want to preserve User-Agent.
headers.insert({"User-Agent", m_user_agent});
m_session.SetHeader(headers);
} else {
m_session.SetHeader(cpr::Header{{"User-Agent", m_user_agent}});
session_header.merge(headers);
}

m_session.SetHeader(session_header);

return m_session.Get();
}

Expand Down
43 changes: 43 additions & 0 deletions cpp/session_pool.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "../hpp/session_pool.h"

namespace yfinance {
SessionPool::SessionPool(size_t size) : sem(size) {
// Make sure size is bigger than 0
assert(size > 0);

// Generate the first session object who will be copied
auto first_session = std::make_unique<Session>();

// Copy the first session into multiple sessions
for (size_t i = 0; i < (size - 1); i++) {
pool.emplace(std::make_unique<Session>(*first_session));
}

pool.push(std::move(first_session));
}

// Fetches a free session object, waits if no one is free
std::unique_ptr<Session> SessionPool::acquire() {
// Make sure there is an empty session
sem.acquire();

// Lock the mutex for queue
std::unique_lock<std::mutex> lock(m_mutex);

auto session = std::move(pool.front());
pool.pop();

return session;
}

void SessionPool::release(std::unique_ptr<Session>&& session) {
{
// Lock the mutex for queue
std::unique_lock<std::mutex> lock(m_mutex);

pool.push(std::move(session));
}

sem.release();
}
}
76 changes: 76 additions & 0 deletions cpp/symbols.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include "../hpp/symbols.h"

#include "../hpp/session.h"
#include "../hpp/utils.h"

#include <cpr/cpr.h>
#include <future>

using json = nlohmann::json;

namespace yfinance {
// Uses 8 threads maximum by default
Symbols::Symbols(const std::vector<std::string>& symbols, int max_threads)
: sem{max_threads}, m_symbols(symbols), session_pool(max_threads)
{}

std::vector<nlohmann::json> Symbols::get_summaries(const std::string& module) {

// Create an array to store the futures
std::vector<std::future<json>> futures;
futures.reserve(m_symbols.size());

// Launch async tasks for API calls
for (const auto& symbol : m_symbols) {
futures.push_back(
std::async(std::launch::async, [this, &symbol, &module]() -> json {
// Make sure to cap the threads used with a semaphore
// So we won't end up with 300 threads for 300 API calls
sem.acquire();

json data{};

// Acquire a free session from session pool
auto session = session_pool.acquire();

cpr::Response r = session->Get(cpr::Url{
Utils::Statics::Summary::v10 + symbol},
cpr::Parameters{{"modules", module}});

// TODO: Add exception handling
if ((r.status_code == 200) && (!r.text.empty())) {
nlohmann::json quoteSummary = nlohmann::json::parse(r.text);
data = quoteSummary["quoteSummary"]
["result"][0][module];
}
/*
else {
std::string error_message =
"Request failed with status code: " + std::to_string(r.status_code);

throw std::runtime_error(error_message);
}
*/

// Release the session
session_pool.release(std::move(session));
// Release the semaphore
sem.release();

return data;
})
);
}

// Collect results in order
std::vector<json> data;
data.reserve(futures.size());

// Join the results
for (auto& fut : futures) {
data.push_back(fut.get());
}

return data;
}
}
4 changes: 4 additions & 0 deletions cpp/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,9 @@ namespace Utils {
"#N/A", "NaN", "nan", "<NA>", "N/A", "n/a",
"NA", "NULL", "null" };
}

namespace Search {
const std::string v11 = "https://query1.finance.yahoo.com/v1/finance/search";
}
}
}
1 change: 1 addition & 0 deletions demo/multi_threading_demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x64/
31 changes: 31 additions & 0 deletions demo/multi_threading_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Multi Threaded Symbol Loading Demo
This demo compares multi thread approach to single threaded approach calling 10 symbols 10 times
Multi threaded approach takes slightly more time pre-initializing (which is NOT included in the benchmark) however, on multiple API calls, it can save more than 5x the time compared to the single threaded approach

Currently only get_summary function is implemented

## Results
This is the result that I have got on my machine

### Multi Threaded
=======================================================================
=========================== SHOW TIMEIT RESULTS =======================
=======================================================================

Iterations completed : 10
Total milliseconds :1574
Average milliseconds :157
Maxima milliseconds :320
Minima milliseconds :102


### Single Threaded
=======================================================================
=========================== SHOW TIMEIT RESULTS =======================
=======================================================================

Iterations completed : 10
Total milliseconds :8327
Average milliseconds :832
Maxima milliseconds :1686
Minima milliseconds :496
48 changes: 48 additions & 0 deletions demo/multi_threading_demo/multi_threading.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "../../hpp/symbols.h"

#include "../../hpp/benchmark.h"
#include "../../hpp/base.h"

#include <nlohmann/json.hpp>

#include <iostream>
#include <vector>
#include <string>

void benchmarkTestMT(yfinance::Symbols* symbols) {
auto data = symbols->get_summaries("price");

for (auto& quoteSummary : data) {
std::cout << quoteSummary.dump() << std::endl;
}
}

void benchmarkTestST(const std::vector<std::string>& symbol_names) {
for (auto& symbol_name : symbol_names) {
yfinance::Symbol symbol(symbol_name);
auto quoteSummary = symbol.get_summary("price");

std::cout << quoteSummary.dump() << std::endl;
}
}

int main() {
// Load 10 different symbols
std::vector<std::string> symbol_names = {
"NVDA", "TSLA", "HOOD", "PLTR", "BTC-USD",
"EURUSD=X", "^SPX", "^DJI", "^TYX", "SPY"
};

// Initialize Symbols object pre-API calls
yfinance::Symbols symbols(symbol_names);

// Benchmark both multi thread and single thread version
auto f_mt = std::bind(&benchmarkTestMT, &symbols);
auto f_st = std::bind(&benchmarkTestST, symbol_names);

auto mt_result = Benchmarking::Timeit(10, f_mt);
auto st_result = Benchmarking::Timeit(10, f_st);

std::cout << mt_result << std::endl;
std::cout << st_result << std::endl;
}
1 change: 1 addition & 0 deletions demo/search_demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x64/
10 changes: 10 additions & 0 deletions demo/search_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Search Demo
This demo demonstrates how to use search functionality.
Symbol::search returns a vector of strings that auto-completes the given query string using yfinance API.
quote_counts is by default 8, however it is NOT guaranteed that result will have 'quote_counts' elements

## Results
```
Search result for TS :
TS TSLA TSM TSEM ^GSPTSE TSHA TSN
```
20 changes: 20 additions & 0 deletions demo/search_demo/search_demo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "../../hpp/base.h"

#include <vector>
#include <iostream>

int main() {
const std::string query = "TS";

auto quotes = yfinance::Symbol::search(query, 10);

std::cout << "Search result for " << query << " :" << std::endl;

for (auto& symbol : quotes) {
std::cout << symbol << " ";
}

std::cout << std::endl;

return 0;
}
Loading