/* GNU Lesser General Public License Copyright (c) 2017 Wimmer, Simon-Justus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package org.sjwimmer.tacharting.implementation.controller; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.MapChangeListener; import javafx.collections.ObservableMap; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.geometry.Orientation; import javafx.scene.control.*; import javafx.scene.control.Button; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import org.sjwimmer.tacharting.chart.api.*; import org.sjwimmer.tacharting.chart.model.*; import org.sjwimmer.tacharting.chart.model.key.Key; import org.sjwimmer.tacharting.chart.model.types.GeneralTimePeriod; import org.sjwimmer.tacharting.chart.model.types.IndicatorCategory; import org.sjwimmer.tacharting.chart.parameters.Parameter; import org.sjwimmer.tacharting.chart.view.IndicatorPopUpWindow; import org.sjwimmer.tacharting.chart.view.TaChart; import org.sjwimmer.tacharting.implementation.model.BaseIndicatorBox; import org.sjwimmer.tacharting.implementation.model.ChartIndicator; import org.sjwimmer.tacharting.implementation.model.api.CSVConnector; import org.sjwimmer.tacharting.implementation.model.api.CsvSettingsManager; import org.sjwimmer.tacharting.implementation.model.api.ExcelConnector; import org.sjwimmer.tacharting.implementation.model.api.SqlLiteConnector; import org.sjwimmer.tacharting.implementation.model.api.YahooSettingsManager; import org.sjwimmer.tacharting.implementation.model.api.key.CSVKey; import org.sjwimmer.tacharting.implementation.model.api.key.ExcelKey; import org.sjwimmer.tacharting.implementation.model.api.key.IEXKey; import org.sjwimmer.tacharting.implementation.model.api.key.SQLKey; import org.sjwimmer.tacharting.implementation.service.IEXDataSource; import org.sjwimmer.tacharting.implementation.service.YahooService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.ta4j.core.Strategy; import org.ta4j.core.BarSeriesManager; import org.ta4j.core.TradingRecord; import javax.xml.xpath.XPathException; import javax.xml.xpath.XPathExpressionException; import java.awt.*; import java.io.File; import java.sql.SQLException; import java.util.*; import java.util.List; import static org.sjwimmer.tacharting.chart.parameters.Parameter.EXTENSION_FILTER_CSV; import static org.sjwimmer.tacharting.chart.parameters.Parameter.EXTENSION_FILTER_EXCEL; public class ChartController implements MapChangeListener<String, ChartIndicator>{ private final Logger log = LoggerFactory.getLogger(ChartController.class); private TaChart chart; private final Map<String, CheckMenuItem> itemMap = new HashMap<>(); private final ObservableMap<GeneralTimePeriod, List<SQLKey>> tableKey = FXCollections.observableHashMap(); private SQLConnector sqlConnector; @FXML private VBox vbxChart; @FXML private Menu indicatorsMenu; @FXML private Menu candles; @FXML private Menu def; @FXML private Menu custom; @FXML private Menu bollinger; @FXML private Menu statistics; @FXML private Menu volume; @FXML private Menu ichimoku; @FXML private Menu helpers; @FXML private Menu keltner; @FXML private Menu strategy; @FXML private Menu strategyMenu; @FXML private ToolBar toolBarIndicators; @FXML private ComboBox<Parameter.ApiProvider> choiceBoxAPI; @FXML private TextField fieldSearch; @FXML private Button btnSearch; @FXML private ProgressIndicator priProgress; @FXML private ToggleButton tbnStoreData; @FXML private TreeView<Key> tvWatchlist; public ChartController(){ } @FXML public void initialize(){ try{ ImageView indicatorImage = new ImageView(new Image(getClass().getClassLoader().getResourceAsStream("icons/indicator.png"))); indicatorsMenu.setGraphic(indicatorImage); ImageView strategyImage = new ImageView(new Image(getClass().getClassLoader().getResourceAsStream("icons/strategy.png"))); strategyMenu.setGraphic(strategyImage); } catch (Exception e){ log.error(e.getMessage()); } fieldSearch.textProperty().addListener((ov, oldValue, newValue) -> fieldSearch.setText(newValue.toUpperCase())); fieldSearch.setOnKeyPressed(event->{ if(event.getCode() == KeyCode.ENTER){ loadDataFromSelectedApi(fieldSearch.getText().split("[;,]")); fieldSearch.clear(); } }); btnSearch.setOnAction(event ->{ loadDataFromSelectedApi(fieldSearch.getText().split("[;,]")); fieldSearch.clear(); }); btnSearch.disableProperty().bind(fieldSearch.textProperty().isEmpty()); priProgress.setVisible(false); //colSymbol.setCellFactory(column -> new SymbolTableCell()); buildWatchlist(); choiceBoxAPI.setItems(FXCollections.observableArrayList(Parameter.ApiProvider.values())); choiceBoxAPI.setValue(Parameter.ApiProvider.Yahoo); // Bind tableView to output of SQLConnector if(this.sqlConnector == null){ log.debug("No SQLConnector set. Create default SqlLiteConnector."); sqlConnector = new SqlLiteConnector(); } DataRequestService requestService = new DataRequestService(); requestService.start(); } /** * Build the treeView for the watchlist and add listener to {@link #tableKey} */ private void buildWatchlist() { //TODO should not fail if ressource not available javafx.scene.image.Image image = new javafx.scene.image.Image(getClass().getClassLoader().getResourceAsStream("icons/watchlistEntry.png")); javafx.scene.image.Image imageNode = new javafx.scene.image.Image(getClass().getClassLoader().getResourceAsStream("icons/watchlistList.png")); final TreeItem<Key> root = new TreeItem<>(new Key("Default Watchlists")); root.setExpanded(true); tvWatchlist.setRoot(root); tvWatchlist.setContextMenu(buildContextMenu()); tvWatchlist.getSelectionModel().selectedItemProperty().addListener((observable, o, n)->{ if(n.getValue() instanceof SQLKey){ // is symbol entry was selected try { TaBarSeries series = sqlConnector.getSymbolData((SQLKey) n.getValue()); chart.getChartIndicatorBox().setBarSeries(series); TableColumn header = new TableColumn("Strategies"); } catch (Exception sql){ sql.printStackTrace(); } } }); for (GeneralTimePeriod table: GeneralTimePeriod.values()){ TreeItem<Key> it = new TreeItem<Key>(new Key(table.toString()),new ImageView(imageNode)); root.getChildren().add(it); //tableKey.put(table,new ArrayList<>()); // init map with empty lists } tableKey.addListener((MapChangeListener<GeneralTimePeriod,List<SQLKey>>) listener -> { if(listener.wasRemoved() || listener.wasAdded()){ for(TreeItem<Key> item: root.getChildren()){ if(GeneralTimePeriod.valueOf(item.getValue().toString()).equals(listener.getKey())){ item.getChildren().clear(); for (SQLKey key: tableKey.get(GeneralTimePeriod.valueOf(item.getValue().toString()))){ item.getChildren().add(new TreeItem<>(key,new ImageView(image))); } } } } }); } private ContextMenu buildContextMenu(){ final MenuItem itemRemove = new MenuItem("remove"); itemRemove.setOnAction(value->{ TreeItem item = tvWatchlist.getSelectionModel().getSelectedItem(); if(item.getValue() instanceof SQLKey){ SQLKey key = (SQLKey) item.getValue(); log.debug("Remove {} from database", key); try{ sqlConnector.removeData(key); }catch (Exception e){ log.error(e.getMessage()); } } }); final MenuItem itemUpdate = new MenuItem("update"); itemUpdate.setOnAction(e->{ updateDataFromSelectedApi(); }); final ContextMenu menu = new ContextMenu(); menu.getItems().addAll(itemUpdate, itemRemove); return menu; } /** * This function has to be called before showing the stage. It allows the user to add a customized <t>ChartIndicatorBox</t> * @param box the {@link BaseIndicatorBox ChartIndicatorBox} with ChartIndicators, BarSeries * and TradingRecords for the org.sjwimmer.tacharting.chart */ public void setIndicatorBox(IndicatorBox box){ Objects.requireNonNull(box, "IndicatorBox cannot be null"); chart = new TaChart(box); VBox.setVgrow(chart, Priority.ALWAYS); vbxChart.getChildren().add(chart); box.getIndicartors().addListener(this); buildMenuEntries(box); TaBarSeries series = box.getBarSeries(); storeSeries(series); } /** * Sets the {@link SQLConnector} for this controller. Allows the user to set up * his own <code>SQLConnector</code> to work with data of his own database * @param sqlConnector the {@link SQLConnector} implementing class */ public void setSqlConnector(SQLConnector sqlConnector){ this.sqlConnector = sqlConnector; } /** * * @param series the TaBarSeries that should be stored in DB */ public synchronized void storeSeries(final TaBarSeries series){ new Thread(()-> { try{ sqlConnector.insertData(series, false); } catch (SQLException sqle){ sqle.printStackTrace(); }}).start(); } /** * Build the menu with entries of all indicators from xml AND add custom indicators from the indicatorBox * @param box the ChartIndicatorBox */ private void buildMenuEntries(IndicatorBox box){ final IndicatorParameterManager propsManager = box.getPropertiesManager(); for (Map.Entry<String, ChartIndicator> entry : chart.getChartIndicatorBox().getTempIndicators().entrySet()) { addToCategory(entry.getKey(), entry.getValue().getCategory()); } final List<String> keys = propsManager.getAllKeys(); for (String key: keys){ try{ IndicatorCategory category = propsManager.getCategory(key); addToCategory(key, category); } catch (XPathExpressionException xpe){ Platform.runLater(() -> new Alert(Alert.AlertType.INFORMATION,xpe.getMessage()).show()); } } for (Map.Entry<String, Strategy> entry : chart.getChartIndicatorBox().getAllStrategies().entrySet()) { addToStrategies(entry.getKey(), entry.getValue()); } } /** * Adds the items with onActions for the strategies to the menu * @param key key/description of the strategy * @param strategy the strategy */ private void addToStrategies(String key, Strategy strategy) { CheckMenuItem item = new CheckMenuItem(key); item.setOnAction(event -> { BarSeriesManager seriesManager = new BarSeriesManager(chart.getChartIndicatorBox().getBarSeries()); TradingRecord value = seriesManager.run(strategy); chart.plotTradingRecord(value, item.isSelected()); }); strategyMenu.getItems().add(item); } private void addToCategory(String key, IndicatorCategory category){ final String[] el = key.split("_"); String name = el[0]; String id = ""; if(el.length > 1){ // custom indicators or indicators that added during runtime may not have an id separator id = el[1]; } CheckMenuItem item = new CheckMenuItem(String.format("%s [%s]", name, id)); item.setId(key); itemMap.put(key,item); item.setOnAction((a)-> { try { if(item.isSelected()){ chart.getChartIndicatorBox().reloadIndicator(key); } } catch (XPathException xpe){ //TODO: handle exception.. xpe.printStackTrace(); } }); switch(category){ case DEFAULT:{ def.getItems().add(item); break; } case HELPERS:{ helpers.getItems().add(item); break; } case VOLUME:{ volume.getItems().add(item); break; } case CANDLES:{ candles.getItems().add(item); break; } case ICHIMOKU:{ candles.getItems().add(item); break; } case STATISTICS:{ statistics.getItems().add(item); break; } case KELTNER:{ keltner.getItems().add(item); break; } case BOLLINGER:{ bollinger.getItems().add(item); break; } case STRATEGY:{ strategy.getItems().add(item); break; } default: custom.getItems().add(item); break; } } //TODO write class to store this information private Map<String, Button> keyButton = new HashMap<>(); private Map<String, Separator> keySeperator = new HashMap<>(); /** * Update the ToolBar * Called every time an ChartIndicator has been added or removed to the * {@link BaseIndicatorBox chartIndicatorBox} colorOf the underlying {@link TaChart org.sjwimmer.tacharting.chart} * * @param change Change<? extends String, ? extends ChartIndicator> */ @Override public void onChanged(Change<? extends String, ? extends ChartIndicator> change) { String key = change.getKey(); if(change.wasRemoved()){ toolBarIndicators.getItems().remove(keyButton.get(key)); toolBarIndicators.getItems().remove(keySeperator.get(key)); if(!change.wasAdded()) { CheckMenuItem item = itemMap.get(key); if(item!=null){ item.setSelected(false); } } } // it is possible that wasRemoved = wasAdded = true, e.g ObservableMap.put(existingKey, indicator) if(change.wasAdded()) { ChartIndicator indicator = change.getValueAdded(); Button btnSetup = new Button(indicator.getGeneralName()); btnSetup.setOnAction((event)->{ IndicatorPopUpWindow in = IndicatorPopUpWindow.getPopUpWindow(key, chart.getChartIndicatorBox()); in.show(btnSetup, MouseInfo.getPointerInfo().getLocation().x, MouseInfo.getPointerInfo().getLocation().y); }); keyButton.put(key,btnSetup); Separator sep1 = new Separator(Orientation.VERTICAL); keySeperator.put(key, sep1); toolBarIndicators.getItems().add(btnSetup); toolBarIndicators.getItems().add(sep1); } } /** * removes all ChartIndicators from the org.sjwimmer.tacharting.chart and toggle bar that are in the toggle bar */ public void clearToggleBar(){ for (Map.Entry<String, Button> stringButtonEntry : keyButton.entrySet()) { chart.getChartIndicatorBox().removeIndicator(stringButtonEntry.getKey()); } } /** * Opens a FileChooser dialog and adds excel or csv ohlc org.sjwimmer.tacharting.data as BarSeries to the current watchlist */ public void openCsvExcelDialog(){ FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Select Csv/Excel File(s)"); fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); fileChooser.getExtensionFilters().addAll(EXTENSION_FILTER_CSV, EXTENSION_FILTER_EXCEL); List<File> files = fileChooser.showOpenMultipleDialog((vbxChart).getScene().getWindow()); if(files == null) { return; } for(File file: files){ if(file==null) { continue; } int extPoint = file.getName().lastIndexOf("."); String extension = file.getName().substring(extPoint+1); if(EXTENSION_FILTER_CSV.getExtensions().contains("*."+extension)){ addCSV(file); continue; } if(EXTENSION_FILTER_EXCEL.getExtensions().contains("*."+extension)){ addExcel(file); } } } private void addCSV(File file){ try{ CSVConnector csvConnector = new CSVConnector(); csvConnector.connect(file); TaBarSeries series = csvConnector.getSymbolData(CSVKey.DEFAULT_KEY); storeSeries(series); this.tableKey.get(series.getTimeFormatType()).add(series.getKey()); } catch (Exception ioe){ ioe.printStackTrace(); //TODO: handle.. } } public void settingCSV(){ new CsvSettingsManager(); } public void settingsYahoo(){ new YahooSettingsManager(); } public void settingsExcel(){ } public void addExcel(File file){ try{ ExcelConnector excelConnector = new ExcelConnector(); if(excelConnector.connect(file)) { TaBarSeries series = excelConnector.getSymbolData(ExcelKey.DEFAULT_KEY); this.tableKey.get(series.getTimeFormatType()).add(series.getKey()); } } catch (Exception e){ e.printStackTrace(); //TODO } } public void loadDataFromSelectedApi(String... symbol){ switch (choiceBoxAPI.valueProperty().get()){ case Yahoo:{ loadYahooData(symbol); break;} case AlphaVantage: { log.error("AlphaVantage connection not implemented"); break; } default: loadIEX(symbol); } } public void updateDataFromSelectedApi(){ new Alert(Alert.AlertType.INFORMATION, "Currently not supported"); } private void loadYahooData(String... symbol){ log.debug("Start Yahoo request..."); String[] cleanSymbols = Arrays.stream(symbol).map(e->e.replaceAll("\\s+","")).toArray(String[]::new); YahooService yahooConnector = new YahooService(cleanSymbols); priProgress.setVisible(true); priProgress.progressProperty().bind(yahooConnector.progressProperty()); yahooConnector.start(); yahooConnector.setOnSucceeded(value->{ for(TaBarSeries series: yahooConnector.getValue()){ this.tableKey.get(series.getTimeFormatType()).add(series.getKey()); if(tbnStoreData.isSelected()){ storeSeries(series); } } priProgress.setVisible(false); }); yahooConnector.setOnFailed(vale->{ yahooConnector.exceptionProperty().get().printStackTrace(); }); } private void loadIEX(String... symbol){ log.debug("Start IEX request..."); String[] cleanSymbols = Arrays.stream(symbol).map(e->e.replaceAll("\\s+","")).toArray(String[]::new); IEXDataSource iexSource = new IEXDataSource(); for(String sym: symbol) { TaBarSeries series; try { series = iexSource.getSymbolData(new IEXKey(sym)); this.tableKey.get(series.getTimeFormatType()).add(series.getKey()); if(tbnStoreData.isSelected()){ storeSeries(series); } } catch (Exception e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } public void addAlphaVantage(){ log.debug("Start AlphaVantage request..."); //TODO: https://www.alphavantage.co/ } /** Table Cells and logic **************************************************************************************/ /** * Symbol table cell (not needed at the moment) * @param <T> */ class SymbolTableCell <T extends String> extends TableCell<TaBarSeries, T>{ @Override protected void updateItem(T item, boolean empty){ super.updateItem(item, empty); if(item == null){ setStyle(""); setText(null); return; } if(item.equals("")){ setText("unnamed"); } setText(item); } } /** * Service to load all SQLKeys from database with help of a {@link SQLConnector} */ class DataRequestService extends Service<Void>{ @Override protected Task<Void> createTask() { return new Task<Void>() { @Override protected Void call() throws Exception { try { for(GeneralTimePeriod table: GeneralTimePeriod.values()){ List<SQLKey> keys = sqlConnector.getKeyList(table); tableKey.put(table, keys); } } catch (SQLException e){ log.error("Error while requesting key list from database: {}"+e.getMessage()); e.printStackTrace(); } return null; } }; } } }