diff --git a/inc/data-sources.hpp b/inc/data-sources.hpp index 531e4b9..876b77e 100644 --- a/inc/data-sources.hpp +++ b/inc/data-sources.hpp @@ -2,6 +2,7 @@ #include +#include "data-sources/finnhub.hpp" #include "data-sources/ipo-cal-appspot.hpp" #include "ipo.hpp" #include "mainwindow.hpp" @@ -18,9 +19,12 @@ class DataSources : public QObject public slots: void queryJapaneseIpos(); + void queryUsIpos(); private: - DataSourceIpoCalAppSpot *dataSourceJapan = new DataSourceIpoCalAppSpot(); + DataSourceIpoCalAppSpot *dataSourceJapanIpos; + DataSourceFinnhub *dataSourceUsIpos; MainWindow *parentObject; QTimer *timer; + QTimer *timer2; }; diff --git a/inc/data-sources/finnhub.hpp b/inc/data-sources/finnhub.hpp new file mode 100644 index 0000000..d767e06 --- /dev/null +++ b/inc/data-sources/finnhub.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +#include "ipo.hpp" + +class DataSourceFinnhub : public QObject +{ + Q_OBJECT + +public: + explicit DataSourceFinnhub(QObject *parent = nullptr, QString apiKey = ""); + ~DataSourceFinnhub(); + + QList queryData(); + +private: + QString apiKey = ""; + QString baseUrl = QString("https://finnhub.io/api/v1"); + QNetworkAccessManager manager; + QUrlQuery query; + QNetworkReply *reply; + QUrl url = QUrl(baseUrl + "/calendar/ipo"); +}; diff --git a/inc/data-sources/ipo-cal-appspot.hpp b/inc/data-sources/ipo-cal-appspot.hpp index a2dec11..d20609b 100644 --- a/inc/data-sources/ipo-cal-appspot.hpp +++ b/inc/data-sources/ipo-cal-appspot.hpp @@ -10,10 +10,10 @@ class DataSourceIpoCalAppSpot : public QObject Q_OBJECT public: - explicit DataSourceIpoCalAppSpot(QObject *parent = 0); + explicit DataSourceIpoCalAppSpot(QObject *parent = nullptr); ~DataSourceIpoCalAppSpot(); - QList query(); + QList queryData(); private: QString baseUrl = QString("https://ipo-cal.appspot.com/api"); diff --git a/inc/ipo.hpp b/inc/ipo.hpp index e8fa311..10ded7f 100644 --- a/inc/ipo.hpp +++ b/inc/ipo.hpp @@ -8,6 +8,7 @@ struct Ipo { QUrl company_website; QDateTime expected_date; QString region; + QString status; // "expected", "priced", "withdrawn", "filed" QString stock_exchange; QString ticker; }; diff --git a/inc/mainwindow.hpp b/inc/mainwindow.hpp index b691bbe..c83b9fe 100644 --- a/inc/mainwindow.hpp +++ b/inc/mainwindow.hpp @@ -22,6 +22,7 @@ class MainWindow : public QMainWindow explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); QList ipos; + QSettings *settings; public slots: void toggleHidden(); @@ -41,11 +42,11 @@ private slots: private: DataSources *dataSources; QSystemTrayIcon *trayIcon; - QSettings *settings; TrayMenu *trayMenu; Ui::MainWindow *ui; void bindShortcuts(); + static bool compareDates(const Ipo &ipo1, const Ipo &ipo2); QString formatDateCell(QString expectedDate); QString formatWebsiteCell(QString websiteUrl); void loadSettings(); diff --git a/ipo-calendar.pro b/ipo-calendar.pro index 3777ab5..484be13 100644 --- a/ipo-calendar.pro +++ b/ipo-calendar.pro @@ -30,6 +30,7 @@ SOURCES += src/main.cpp \ src/mainwindow.cpp \ src/traymenu.cpp \ src/runguard.cpp \ + src/data-sources/finnhub.cpp \ src/data-sources/ipo-cal-appspot.cpp \ src/data-sources.cpp \ @@ -37,6 +38,7 @@ HEADERS += inc/mainwindow.hpp \ inc/traymenu.hpp \ inc/runguard.hpp \ inc/ipo.hpp \ + inc/data-sources/finnhub.hpp \ inc/data-sources/ipo-cal-appspot.hpp \ inc/data-sources.hpp \ diff --git a/res/styles/ipo-calendar.qss b/res/styles/ipo-calendar.qss index 842e787..89e585a 100644 --- a/res/styles/ipo-calendar.qss +++ b/res/styles/ipo-calendar.qss @@ -1,7 +1,7 @@ /* List iew */ QTreeWidget { background-color: #000; - color: white; + color: #fff; } /* Header */ @@ -9,9 +9,9 @@ QHeaderView::section { background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #616161, stop: 0.5 #505050, stop: 0.6 #434343, stop: 1 #656565); - color: white; - padding-left: 4px; border: 1px solid #6c6c6c; + color: #fff; + padding-left: 4px; } /* Cells */ @@ -19,9 +19,16 @@ QTreeWidget::item { padding-right: 14px; } QTreeWidget::item::selected { + background-color: #013636; +} +QTreeView::item:selected:hover { + background-color: #024040; +} +QTreeView::item:hover { background-color: #011919; } + /* Scrollbar */ QScrollBar:vertical { border: none; diff --git a/src/data-sources.cpp b/src/data-sources.cpp index c5d40ed..1971175 100644 --- a/src/data-sources.cpp +++ b/src/data-sources.cpp @@ -9,13 +9,24 @@ DataSources::DataSources(QObject *parent) : QObject(parent) { parentObject = (MainWindow *)this->parent(); + dataSourceJapanIpos = new DataSourceIpoCalAppSpot(this); + QString finnhubApiKey; + if (parentObject->settings->contains("Secrets/finnhubApiKey")) { + finnhubApiKey = parentObject->settings->value("Secrets/finnhubApiKey").toString(); + } + dataSourceUsIpos = new DataSourceFinnhub(this, finnhubApiKey); + // Start-time requests queryJapaneseIpos(); + queryUsIpos(); - // Consequitive requests + // Recurring requests timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(queryJapaneseIpos())); timer->start(8 * HOUR_IN_MS); + timer2 = new QTimer(this); + connect(timer2, SIGNAL(timeout()), this, SLOT(queryUsIpos())); + timer->start(2 * HOUR_IN_MS); } DataSources::~DataSources() @@ -24,7 +35,30 @@ DataSources::~DataSources() void DataSources::queryJapaneseIpos() { - QList retrievedIpos = dataSourceJapan->query(); + QList retrievedIpos = dataSourceJapanIpos->queryData(); + + foreach (Ipo retrievedIpo, retrievedIpos) { + Ipo *existingIpo = nullptr; + + foreach (Ipo ipo, parentObject->ipos) { + if (ipo.company_name == retrievedIpo.company_name) { + existingIpo = &ipo; + } + } + + if (existingIpo) { + existingIpo = &retrievedIpo; + } else { + parentObject->ipos.append(retrievedIpo); + } + } + + parentObject->updateList(); +} + +void DataSources::queryUsIpos() +{ + QList retrievedIpos = dataSourceUsIpos->queryData(); foreach (Ipo retrievedIpo, retrievedIpos) { Ipo *existingIpo = nullptr; diff --git a/src/data-sources/finnhub.cpp b/src/data-sources/finnhub.cpp new file mode 100644 index 0000000..25ac6cb --- /dev/null +++ b/src/data-sources/finnhub.cpp @@ -0,0 +1,85 @@ +/* + * IPO Calendar data source: US IPOs + * API Spec: https://finnhub.io/docs/api + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "data-sources/finnhub.hpp" + +DataSourceFinnhub::DataSourceFinnhub(QObject *parent, QString apiKey) : QObject(parent) +{ + query.addQueryItem("from", "2021-01-01"); + query.addQueryItem("to", "2021-06-01"); + url.setQuery(query.query()); + this->apiKey = apiKey; +} + +DataSourceFinnhub::~DataSourceFinnhub() +{ + delete reply; +} + +QList DataSourceFinnhub::queryData() +{ + QNetworkRequest request(url); + QList retrievedIpos; + + if (apiKey.isEmpty()) { + return retrievedIpos; + } + + request.setRawHeader("X-Finnhub-Token", apiKey.toUtf8()); + + reply = manager.get(request); + + while (!reply->isFinished()) { + QCoreApplication::processEvents(); + } + + QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + int status = statusCode.toInt(); + + if (status == 200) { + QJsonParseError jsonParseError; + QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll(), &jsonParseError); + if (jsonParseError.error != QJsonParseError::NoError) { + return retrievedIpos; + } + QJsonObject jsonRoot = jsonDocument.object(); + if (jsonRoot["ipoCalendar"] == QJsonValue::Undefined) { + return retrievedIpos; + } + QJsonArray dataArray = jsonRoot["ipoCalendar"].toArray(); + + foreach (const QJsonValue &item, dataArray) { + Ipo ipo; + QJsonObject ipoObj = item.toObject(); + + if (ipoObj["name"] == QJsonValue::Undefined) { + continue; + } + + ipo.company_name = ipoObj["name"].toString(); + ipo.company_website = QUrl("https://ddg.gg/?q=\\" + ipo.company_name); + ipo.expected_date = QDateTime::fromString(ipoObj["date"].toString(), "yyyy-MM-dd"); + ipo.region = QString("North America (US)"); + ipo.status = ipoObj["status"].toString(); + ipo.stock_exchange = ipoObj["exchange"].toString(); + ipo.ticker = ipoObj["symbol"].toString(); + + retrievedIpos.append(ipo); + } + } + + reply->deleteLater(); + + return retrievedIpos; +} diff --git a/src/data-sources/ipo-cal-appspot.cpp b/src/data-sources/ipo-cal-appspot.cpp index b459437..b5b2199 100644 --- a/src/data-sources/ipo-cal-appspot.cpp +++ b/src/data-sources/ipo-cal-appspot.cpp @@ -1,11 +1,10 @@ /* * IPO Calendar data source: Japanese IPOs - * Spec: https://ipo-cal.appspot.com/apispec.html + * API Spec: https://ipo-cal.appspot.com/apispec.html * */ #include -#include #include #include #include @@ -15,21 +14,20 @@ #include "data-sources/ipo-cal-appspot.hpp" -DataSourceIpoCalAppSpot::DataSourceIpoCalAppSpot(QObject *parent): QObject(parent) +DataSourceIpoCalAppSpot::DataSourceIpoCalAppSpot(QObject *parent) : QObject(parent) { } DataSourceIpoCalAppSpot::~DataSourceIpoCalAppSpot() { delete reply; + // TODO: overwrite apiKey with zeroes before freeing its memory (for security reasons) } -QList DataSourceIpoCalAppSpot::query() +QList DataSourceIpoCalAppSpot::queryData() { QNetworkRequest request(url); - QList retrieved_ipos; - - // request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QList retrievedIpos; reply = manager.get(request); @@ -44,11 +42,11 @@ QList DataSourceIpoCalAppSpot::query() QJsonParseError jsonParseError; QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll(), &jsonParseError); if (jsonParseError.error != QJsonParseError::NoError) { - return retrieved_ipos; + return retrievedIpos; } QJsonObject jsonRoot = jsonDocument.object(); if (jsonRoot["result"] == QJsonValue::Undefined) { - return retrieved_ipos; + return retrievedIpos; } QJsonArray dataArray = jsonRoot["data"].toArray(); @@ -67,11 +65,11 @@ QList DataSourceIpoCalAppSpot::query() ipo.stock_exchange = QString("TSE (%1)").arg(ipoObj["market_key"].toString()); ipo.ticker = ipoObj["code"].toString(); - retrieved_ipos.append(ipo); + retrievedIpos.append(ipo); } } reply->deleteLater(); - return retrieved_ipos; + return retrievedIpos; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 69e866f..cf415ad 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -67,6 +67,11 @@ MainWindow::~MainWindow() delete ui; } +bool MainWindow::compareDates(const Ipo &ipo1, const Ipo &ipo2) +{ + return ipo1.expected_date > ipo2.expected_date; +} + QString MainWindow::formatDateCell(QString timestamp) { QString color = "white"; @@ -94,6 +99,8 @@ void MainWindow::updateList() delete ui->treeWidget->takeTopLevelItem(0); } + qSort(ipos.begin(), ipos.end(), compareDates); + foreach(Ipo ipo, ipos) { QTreeWidgetItem *ipoItem = new QTreeWidgetItem(static_cast(nullptr)); ipoItem->setText(0, ipo.company_name);