From c1f41cdc21b2b58adb972f74817a706ecd5cfa95 Mon Sep 17 00:00:00 2001 From: ogallagher Date: Sat, 28 Aug 2021 18:13:10 -0400 Subject: [PATCH] Catch api errors #24 and use api key input form #23 --- src/ogallagher/marketsense/MarketSense.java | 125 +++++++++++++++++- .../persistent/TrainingSession.java | 75 ++++++++--- .../marketsense/resources/APIKeyForm.css | 40 ++++++ .../marketsense/resources/APIKeyForm.fxml | 44 ++++++ twelvedata_client_java | 2 +- 5 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 src/ogallagher/marketsense/resources/APIKeyForm.css create mode 100644 src/ogallagher/marketsense/resources/APIKeyForm.fxml diff --git a/src/ogallagher/marketsense/MarketSense.java b/src/ogallagher/marketsense/MarketSense.java index db6bbf8..9a04f3b 100644 --- a/src/ogallagher/marketsense/MarketSense.java +++ b/src/ogallagher/marketsense/MarketSense.java @@ -19,6 +19,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.TreeSet; import javax.persistence.EntityManager; @@ -31,6 +32,7 @@ import com.fxgraph.graph.PannableCanvas; import javafx.application.Application; +import javafx.application.HostServices; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; @@ -46,6 +48,7 @@ import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; @@ -63,11 +66,14 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; +import javafx.scene.text.Text; import javafx.stage.Stage; +import javafx.stage.WindowEvent; import javafx.util.Callback; import ogallagher.twelvedata_client_java.TwelvedataClient; import ogallagher.twelvedata_client_java.TwelvedataInterface.BarInterval; +import ogallagher.twelvedata_client_java.TwelvedataInterface.Failure; import ogallagher.twelvedata_client_java.TwelvedataInterface.SecuritySet; import ogallagher.marketsense.persistent.Person; import ogallagher.marketsense.persistent.Security; @@ -325,6 +331,11 @@ public static class MarketSenseGUI extends Application { private static int MAIN_WINDOW_WIDTH_INIT = 600; private static int MAIN_WINDOW_HEIGHT_INIT = 500; + /** + * Provide static access to the {@code HostServices} instance of the latest {@code MarketSenseGUI} launched. + */ + private static HostServices hostServices; + public static void main(String[] args) { launch(args); } @@ -349,6 +360,17 @@ public void start(Stage primaryStage) throws Exception { ); mainWindow.setScene(mainScene); + // end program on main window close + mainWindow.setOnHidden(new EventHandler() { + @Override + public void handle(WindowEvent event) { + Platform.exit(); + } + }); + + // host services + hostServices = getHostServices(); + // connect db entity manager dbManager = Persistence .createEntityManagerFactory(properties.getProperty(PROP_PERSIST_UNIT)) @@ -363,6 +385,11 @@ public void start(Stage primaryStage) throws Exception { loadPeople(ShowLogin.class, true); } + @Override + public void stop() { + // TODO handle program exit + } + /** * Should be run on javafx thread. * @@ -428,7 +455,7 @@ public void handle(MouseEvent event) { }); } catch (IOException e) { - System.out.println("error showing login: " + e.getMessage()); + System.out.println("ERROR showing login: " + e.getMessage()); } } } @@ -973,6 +1000,92 @@ public List call() { return graph; } } + + public static class ShowApiKeyForm implements Runnable { + private static final String WINDOW_TITLE = "API Key Form"; + private static final int WINDOW_WIDTH = 500; + private static final int WINDOW_HEIGHT = 240; + + private Stage apiKeyFormWindow; + private String keyOld; + + public ShowApiKeyForm(String keyOld) { + apiKeyFormWindow = new Stage(); + apiKeyFormWindow.setTitle(WINDOW_TITLE); + apiKeyFormWindow.setWidth(WINDOW_WIDTH); + apiKeyFormWindow.setHeight(WINDOW_HEIGHT); + + this.keyOld = keyOld; + } + + @Override + public void run() { + try { + Parent root = (Parent) FXMLLoader.load(MarketSense.class.getResource("resources/APIKeyForm.fxml")); + Scene windowScene = new Scene(root); + + apiKeyFormWindow.setScene(windowScene); + + enableHyperlinks(root); + + // show old key + ((Text) root.lookup("#apiKeyOld")).setText(keyOld); + + // handle new key + TextField keyField = (TextField) root.lookup("#apiKeyNew"); + keyField.setOnKeyReleased(new EventHandler() { + @Override + public void handle(KeyEvent event) { + if (event.getCode().equals(KeyCode.ENTER)) { + String keyNew = keyField.getText(); + + if (keyNew.length() != 0) { + // set api key of twelvedata client + tdclient.setKey(keyNew); + System.out.println("INFO set api key to " + keyNew); + + // close window + apiKeyFormWindow.close(); + } + else { + System.out.println("ERROR api key not given"); + keyField.setText(""); + keyField.setPromptText("blank or invalid key given"); + } + } + } + }); + + apiKeyFormWindow.show(); + } + catch (IOException e) { + System.out.println("ERROR showing api key form window: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Enable hyperlinks in the given gui fragment, given that they store their urls in the + * {@link Hyperlink#tooltipProperty() tooltip}. + * + * @param root The gui fragment root node. + */ + private void enableHyperlinks(Node root) { + Set hyperlinks = root.lookupAll("Hyperlink"); + System.out.println("DEBUG found " + hyperlinks.size() + " hyperlinks"); + + for (Node hln : hyperlinks) { + Hyperlink hl = (Hyperlink) hln; + hl.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + // assumes href is in tooltip + hostServices.showDocument(hl.getTooltip().getText()); + } + }); + } + } + } } /** @@ -1201,14 +1314,20 @@ public static boolean newTrainingSession(String symbol, String barWidth, int sam System.out.println("starting a new training session " + session); // prepare the database - if (session.collectMarketUniverse(dbManager,tdclient)) { + Failure failure = session.collectMarketUniverse(dbManager,tdclient); + + if (failure == null) { System.out.println("market data universe acquired for lookback of " + session.getMaxLookbackMonths() + " months"); // show training session interface Platform.runLater(new MarketSenseGUI.ShowTrainingSession(session)); } + else if (failure.code == Failure.ErrorCode.API_KEY) { + // show api key input form + Platform.runLater(new MarketSenseGUI.ShowApiKeyForm(tdclient.getKey())); + } else { - System.out.println("ERROR failed to creake market data universe for training session"); + System.out.println("ERROR failed to creake market data universe for training session: " + failure.message); } return true; diff --git a/src/ogallagher/marketsense/persistent/TrainingSession.java b/src/ogallagher/marketsense/persistent/TrainingSession.java index 69bb8c4..5730da6 100644 --- a/src/ogallagher/marketsense/persistent/TrainingSession.java +++ b/src/ogallagher/marketsense/persistent/TrainingSession.java @@ -35,6 +35,7 @@ import ogallagher.marketsense.MarketSynth; import ogallagher.twelvedata_client_java.TwelvedataClient; import ogallagher.twelvedata_client_java.TwelvedataInterface.BarInterval; +import ogallagher.twelvedata_client_java.TwelvedataInterface.Failure; import ogallagher.twelvedata_client_java.TwelvedataInterface.TimeSeries; /** @@ -206,14 +207,15 @@ public MarketSample nextSample(EntityManager dbManager, MarketSynth marketSynth) * so there are never any holes between the start and end datetimes. * * Note that ideal universe bounds won't necessarily match valid market calendars and market hours, so in cases where this is - * expected, the universe bounds {@link after} .. {@link before} will be updated to match what the database does have. + * expected, the universe bounds {@link #after} .. {@link #before} will be updated to match what the database does have. * - * @return {@code true} if the needed market data is now in the database. + * @return The failure, or {@code null} if the needed market data is now in the database. */ - public boolean collectMarketUniverse(EntityManager dbManager, TwelvedataClient marketClient) { - boolean result = true, - firstUp = false, - lastDown = false; + public Failure collectMarketUniverse(EntityManager dbManager, TwelvedataClient marketClient) { + Failure result = null; + + boolean firstUp = false; + boolean lastDown = false; TradeBar first = new TradeBar(security, after, barWidth); TradeBar last = new TradeBar(security, BarInterval.offsetBars(before, barWidth, sampleSize), barWidth); @@ -222,7 +224,7 @@ public boolean collectMarketUniverse(EntityManager dbManager, TwelvedataClient m TradeBar preLast = null, postFirst = null; dbManager.getTransaction().begin(); - if (!dbManager.contains(first)) { + if (result == null && !dbManager.contains(first)) { // move forward to find earliest bar after first String qstr = String.format( "select t from %5$s t " + @@ -259,7 +261,7 @@ public boolean collectMarketUniverse(EntityManager dbManager, TwelvedataClient m security.getSymbol(), barWidth, first.getDatetime(), BarInterval.offsetBars(preLast.getDatetime(), barWidth, -1) ); - if (timeSeries != null) { + if (!timeSeries.isFailure()) { // convert to db-compat trade bars and persist List bars = TradeBar.convertTimeSeries(timeSeries, Comparator.naturalOrder()); @@ -269,15 +271,29 @@ public boolean collectMarketUniverse(EntityManager dbManager, TwelvedataClient m System.out.println("persisted " + bars.size() + " new bars"); } else { - System.out.println("WARNING failed to fetch first-prelast for universe, perhaps no bars exist"); - // update first to be preLast - firstUp = true; + Failure f = (Failure) timeSeries; + switch (f.code) { + case Failure.ErrorCode.API_KEY: + case Failure.ErrorCode.CALL_LIMIT: + case Failure.ErrorCode.NO_COMMS: + case Failure.ErrorCode.NULL_RESPONSE: + result = f; + break; + + default: + System.out.println( + "WARNING failed to fetch first-prelast for universe, perhaps no bars exist: " + f.toString() + ); + // update first to be preLast + firstUp = true; + break; + } } } dbManager.getTransaction().commit(); dbManager.getTransaction().begin(); - if (!dbManager.contains(last)) { + if (result == null && !dbManager.contains(last)) { // move backward to find latest bar before last Query query = dbManager.createQuery( String.format( @@ -314,7 +330,7 @@ public boolean collectMarketUniverse(EntityManager dbManager, TwelvedataClient m security.getSymbol(), barWidth, BarInterval.offsetBars(postFirst.getDatetime(), barWidth, 1), last.getDatetime() ); - if (timeSeries != null) { + if (!timeSeries.isFailure()) { // convert to db-compat trade bars and persist List bars = TradeBar.convertTimeSeries(timeSeries, Comparator.naturalOrder()); @@ -324,20 +340,35 @@ public boolean collectMarketUniverse(EntityManager dbManager, TwelvedataClient m System.out.println("persisted " + bars.size() + " new bars"); } else { - System.out.println("WARNING failed to fetch postfirst-last for universe, perhaps no bars exist"); - // update last to be postFirst - lastDown = true; + Failure f = (Failure) timeSeries; + switch (f.code) { + case Failure.ErrorCode.API_KEY: + case Failure.ErrorCode.CALL_LIMIT: + case Failure.ErrorCode.NO_COMMS: + case Failure.ErrorCode.NULL_RESPONSE: + result = f; + break; + + default: + System.out.println("WARNING failed to fetch postfirst-last for universe, perhaps no bars exist: " + f); + // update last to be postFirst + lastDown = true; + break; + } + } } dbManager.getTransaction().commit(); - if (firstUp) { - after = preLast.getDatetime(); - } - if (lastDown) { - before = BarInterval.offsetBars(postFirst.getDatetime(), barWidth, -sampleSize); + if (result == null) { + if (firstUp) { + after = preLast.getDatetime(); + } + if (lastDown) { + before = BarInterval.offsetBars(postFirst.getDatetime(), barWidth, -sampleSize); + } + System.out.println("DEBUG universe trimmed to " + first.getDatetime() + " to " + last.getDatetime()); } - System.out.println("DEBUG universe trimmed to " + first.getDatetime() + " to " + last.getDatetime()); return result; } diff --git a/src/ogallagher/marketsense/resources/APIKeyForm.css b/src/ogallagher/marketsense/resources/APIKeyForm.css new file mode 100644 index 0000000..7fe1d43 --- /dev/null +++ b/src/ogallagher/marketsense/resources/APIKeyForm.css @@ -0,0 +1,40 @@ +/* + +Owen Gallagher +2021-08-28 + +Market data API Key input form. + +*/ + +.root { + -fx-font-size: 14px; + -fx-font-family: sans-serif; + -fx-background-color: #fff; + -fx-padding: 4 4 4 4; +} + +.h1 { + -fx-font-size: 24px; + -fx-font-weight: bold; + -fx-padding: 2 0 8 0; +} + +.h2 { + -fx-font-size: 20px; + -fx-padding: 2 0 4 0; +} + +.h3 { + -fx-font-size: 16px; + -fx-padding: 2 0 4 0; +} + +.paragraph { + -fx-padding: 4px 0px 4px 0px; + -fx-text-alignment: left; +} + +.code { + -fx-font-family: "monospace"; +} \ No newline at end of file diff --git a/src/ogallagher/marketsense/resources/APIKeyForm.fxml b/src/ogallagher/marketsense/resources/APIKeyForm.fxml new file mode 100644 index 0000000..cd971f5 --- /dev/null +++ b/src/ogallagher/marketsense/resources/APIKeyForm.fxml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/twelvedata_client_java b/twelvedata_client_java index 47e4d39..07aefae 160000 --- a/twelvedata_client_java +++ b/twelvedata_client_java @@ -1 +1 @@ -Subproject commit 47e4d391d3046e44cbe5405c84f3b2e10d9feb32 +Subproject commit 07aefae426db3a8bde57d6f1844d32cf9cf774fa