From 6fd52cc0aed66619d6be827b8ef8b9e31546ea1a Mon Sep 17 00:00:00 2001 From: Plotnikov Andrey Date: Sat, 9 Oct 2021 16:06:21 +0300 Subject: [PATCH] Number of lots + hovered line + score calculation update --- README.md | 1 + pom.xml | 2 +- src/main/java/ru/shemplo/tbs/Bond.java | 9 +++-- .../ru/shemplo/tbs/RunTinkoffBondScanner.java | 28 ++++++++++++-- src/main/java/ru/shemplo/tbs/TBSUtils.java | 27 ++++++++++++++ .../shemplo/tbs/gfx/TBSExploreTableCell.java | 9 +++-- .../shemplo/tbs/gfx/TBSInspectTableCell.java | 21 ++++++----- .../ru/shemplo/tbs/gfx/TBSMetaWrapper.java | 15 ++++++++ .../java/ru/shemplo/tbs/gfx/TBSTableCell.java | 37 +++++++++++++++---- .../ru/shemplo/tbs/gfx/TBSUIApplication.java | 26 ++++++++----- 10 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 src/main/java/ru/shemplo/tbs/TBSUtils.java create mode 100644 src/main/java/ru/shemplo/tbs/gfx/TBSMetaWrapper.java diff --git a/README.md b/README.md index 0cbd8f8..e0c7376 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ _score value is not a ground truth but we try to make it as more objective as po ### How to use * Link `🌐` allows to open bonds page in Tinkoff investment (T) and MOEX (M) * Link `🔍` allows to inspect known coupons of corresponding bond +* Column `👝` shows number of lots in your portfolio (sum by all your accounts) * Symbol `➥` marks next coupon, symbol `⭿` mark coupon after offer is committed * Credit values are calculated with consideration to the inflation with the equation: let `S` some sum, let `D` number of a days and `I` is inflation in percents then diff --git a/pom.xml b/pom.xml index a7d0096..cd4a545 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.shemplo tinkoff-bond-scanner - 1.0 + 1.1 UTF-8 diff --git a/src/main/java/ru/shemplo/tbs/Bond.java b/src/main/java/ru/shemplo/tbs/Bond.java index 897b74e..2195683 100644 --- a/src/main/java/ru/shemplo/tbs/Bond.java +++ b/src/main/java/ru/shemplo/tbs/Bond.java @@ -27,6 +27,7 @@ public class Bond implements Serializable { private String name, code; private Currency currency; + private int lots; private LocalDate start, end, nextCoupon; private long couponsPerYear; @@ -40,8 +41,9 @@ public class Bond implements Serializable { private String primaryBoard; - public Bond (MarketInstrument instrument) { + public Bond (MarketInstrument instrument, int lots) { currency = instrument.getCurrency (); + this.lots = lots; final var MOEX_DESCRIPION_URL = MOEXRequests.makeBondDescriptionURLForMOEX (instrument.getTicker ()); final var MOEX_COUPONS_URL = MOEXRequests.makeBondCouponsURLForMOEX (instrument.getTicker ()); @@ -54,7 +56,7 @@ public Bond (MarketInstrument instrument) { couponsPerYear = description.getBondCouponsPerYear ().orElse (1); nextCoupon = description.getBondNextCouponDate ().orElse (null); - nominalValue = description.getBondNominalValue ().orElse (0.0); + nominalValue = description.getBondNominalValue ().orElse (1.0); percentage = description.getBondPercentage ().orElse (0.0); emitterId = description.getBondEmitterID ().orElse (-1L); start = description.getBondStartDate ().orElse (null); @@ -134,7 +136,8 @@ public void updateScore (ITBSProfile profile) { final var priceBalance = profile.getSafeMaxPrice (lastPrice) - lastPrice; final var monthsBalance = months - profile.getSafeMinMonths (); - score = pureCredit + monthsBalance * 1.13 + priceBalance * 1.35; + score = pureCredit + monthsBalance * 1.13 + priceBalance * 1.35 - lots * 0.25 + couponsPerYear * 0.25 + percentage * 1.4 - 100.0; + score *= nominalValue != 0.0 ? 1000.0 / nominalValue : 1.0; // align to 1k nominal } } diff --git a/src/main/java/ru/shemplo/tbs/RunTinkoffBondScanner.java b/src/main/java/ru/shemplo/tbs/RunTinkoffBondScanner.java index 2bd9ae7..be43f1b 100644 --- a/src/main/java/ru/shemplo/tbs/RunTinkoffBondScanner.java +++ b/src/main/java/ru/shemplo/tbs/RunTinkoffBondScanner.java @@ -12,6 +12,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Scanner; import java.util.stream.Collectors; @@ -19,6 +20,8 @@ import lombok.extern.slf4j.Slf4j; import ru.shemplo.tbs.gfx.TBSUIApplication; import ru.tinkoff.invest.openapi.OpenApi; +import ru.tinkoff.invest.openapi.model.rest.InstrumentType; +import ru.tinkoff.invest.openapi.model.rest.PortfolioPosition; import ru.tinkoff.invest.openapi.model.rest.SandboxRegisterRequest; import ru.tinkoff.invest.openapi.okhttp.OkHttpOpenApi; @@ -82,15 +85,34 @@ private static void openConnection (ITBSProfile profile, String token) { } private static void searchForBonds (ITBSProfile profile, OpenApi client) { + final var ticker2lots = searchForPortfolio (profile, client); + //searchForFavorites (profile, client); + log.info ("Loading data abount bonds from Tinkoff and MOEX..."); final var bonds = client.getMarketContext ().getMarketBonds ().join ().getInstruments ().stream () - . filter (instrument -> profile.getCurrencies ().contains (instrument.getCurrency ())) - . parallel () - . map (Bond::new).filter (profile::testBond).limit (profile.getMaxResults ()) + . filter (instrument -> profile.getCurrencies ().contains (instrument.getCurrency ())).parallel () + . map (ins -> { + final var lots = ticker2lots.getOrDefault (ins.getTicker (), 0); + return new Bond (ins, lots); + }) + . filter (profile::testBond).limit (profile.getMaxResults ()) . collect (Collectors.toList ()); analizeBonds (profile, bonds); } + private static Map searchForPortfolio (ITBSProfile profile, OpenApi client) { + return client.getUserContext ().getAccounts ().join ().getAccounts ().parallelStream ().flatMap (acc -> { + return client.getPortfolioContext ().getPortfolio (acc.getBrokerAccountId ()).join ().getPositions ().stream (); + }).filter (pos -> pos.getInstrumentType () == InstrumentType.BOND).collect (Collectors.toMap ( + PortfolioPosition::getTicker, PortfolioPosition::getLots, Integer::sum + )); + } + + @SuppressWarnings ("unused") // Not supported by Tinkoff Open API yet + private static void searchForFavorites (ITBSProfile profile, OpenApi client) { + + } + private static void analizeBonds (ITBSProfile profile, List bonds) { log.info ("Analizing loaded bonds (total: {})...", bonds.size ()); bonds.forEach (bond -> bond.updateScore (profile)); diff --git a/src/main/java/ru/shemplo/tbs/TBSUtils.java b/src/main/java/ru/shemplo/tbs/TBSUtils.java new file mode 100644 index 0000000..3c0d642 --- /dev/null +++ b/src/main/java/ru/shemplo/tbs/TBSUtils.java @@ -0,0 +1,27 @@ +package ru.shemplo.tbs; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class TBSUtils { + + public static List mapToList (Collection values, Function converter) { + return values.stream ().map (converter).collect (Collectors.toList ()); + } + + public static F aOrB (F a, F b) { + return a == null ? b : a; + } + + public static S mapIfNN (F value, Function converter, S defaultValue) { + return aOrB (value == null ? null : converter.apply (value), defaultValue); + } + + public static void doIfNN (F value, Consumer action) { + if (value != null) { action.accept (value); } + } + +} diff --git a/src/main/java/ru/shemplo/tbs/gfx/TBSExploreTableCell.java b/src/main/java/ru/shemplo/tbs/gfx/TBSExploreTableCell.java index ae29dd8..1424b39 100644 --- a/src/main/java/ru/shemplo/tbs/gfx/TBSExploreTableCell.java +++ b/src/main/java/ru/shemplo/tbs/gfx/TBSExploreTableCell.java @@ -7,7 +7,7 @@ import javafx.scene.text.Text; import ru.shemplo.tbs.Bond; -public class TBSExploreTableCell extends TBSTableCell { +public class TBSExploreTableCell extends TBSTableCell { private boolean openInTinkoff; @@ -19,7 +19,7 @@ public TBSExploreTableCell (boolean openInTinkoff) { } @Override - protected void updateItem (Bond item, boolean empty) { + protected void updateItem (TBSMetaWrapper item, boolean empty) { super.updateItem (item, empty); setText (null); @@ -27,13 +27,14 @@ protected void updateItem (Bond item, boolean empty) { final var link = new Text ("🌐"); link.setOnMouseClicked (me -> { if (me.getButton () == MouseButton.PRIMARY) { + final var code = item.getObject ().getCode (); if (openInTinkoff) { TBSUIApplication.getInstance ().openLinkInBrowser (String.format ( - "https://www.tinkoff.ru/invest/bonds/%s/", item.getCode () + "https://www.tinkoff.ru/invest/bonds/%s/", code )); } else { TBSUIApplication.getInstance ().openLinkInBrowser (String.format ( - "https://www.moex.com/ru/issue.aspx?code=%s&utm_source=www.moex.com", item.getCode () + "https://www.moex.com/ru/issue.aspx?code=%s&utm_source=www.moex.com", code )); } } diff --git a/src/main/java/ru/shemplo/tbs/gfx/TBSInspectTableCell.java b/src/main/java/ru/shemplo/tbs/gfx/TBSInspectTableCell.java index 46236a9..a5287cc 100644 --- a/src/main/java/ru/shemplo/tbs/gfx/TBSInspectTableCell.java +++ b/src/main/java/ru/shemplo/tbs/gfx/TBSInspectTableCell.java @@ -27,6 +27,7 @@ import javafx.stage.Window; import ru.shemplo.tbs.Bond; import ru.shemplo.tbs.Coupon; +import ru.shemplo.tbs.TBSUtils; public class TBSInspectTableCell extends TBSTableCell { @@ -38,7 +39,7 @@ public TBSInspectTableCell () { } @Override - protected void updateItem (Bond item, boolean empty) { + protected void updateItem (TBSMetaWrapper item, boolean empty) { super.updateItem (item, empty); setText (null); @@ -47,7 +48,7 @@ protected void updateItem (Bond item, boolean empty) { link.setOnMouseClicked (me -> { if (me.getButton () == MouseButton.PRIMARY) { final var scene = ((Node) me.getSource ()).getScene (); - showCouponsWindow (scene.getWindow (), item); + showCouponsWindow (scene.getWindow (), item.getObject ()); } }); link.setCursor (Cursor.HAND); @@ -72,17 +73,19 @@ private void showCouponsWindow (Window window, Bond bond) { final var table = initializeTable (bond); root.getChildren ().add (table); - table.setItems (FXCollections.observableArrayList (bond.getCoupons ())); + table.setItems (FXCollections.observableArrayList (TBSUtils.mapToList ( + bond.getCoupons (), TBSMetaWrapper::new + ))); } - private TableView initializeTable (Bond bond) { - final var table = new TableView (); + private TableView > initializeTable (Bond bond) { + final var table = new TableView > (); table.setBackground (new Background (new BackgroundFill (Color.LIGHTGRAY, CornerRadii.EMPTY, Insets.EMPTY))); VBox.setVgrow (table, Priority.ALWAYS); table.setSelectionModel (null); table.setBorder (Border.EMPTY); - final var symbolColumn = makeTBSTableColumn ("", Coupon::getSymbol, false, false, Pos.BASELINE_CENTER, 40.0); + final var symbolColumn = makeTBSTableColumn ("", Coupon::getSymbol, false, false, Pos.BASELINE_CENTER, 50.0); table.getColumns ().add (symbolColumn); final var dateColumn = makeTBSTableColumn ("Date", Coupon::getDate, false, false, 100.0); @@ -103,17 +106,17 @@ private TableView initializeTable (Bond bond) { return table; } - public static TableColumn makeTBSTableColumn ( + public static TableColumn , TBSMetaWrapper > makeTBSTableColumn ( String name, Function converter, boolean sortable, boolean colorized, double minWidth ) { return makeTBSTableColumn (name, converter, sortable, colorized, Pos.BASELINE_LEFT, minWidth); } - public static TableColumn makeTBSTableColumn ( + public static TableColumn , TBSMetaWrapper > makeTBSTableColumn ( String name, Function converter, boolean sortable, boolean colorized, Pos textAlignment, double minWidth ) { - final var column = new TableColumn (name); + final var column = new TableColumn , TBSMetaWrapper > (name); column.setCellFactory (__ -> new TBSTableCell <> (converter, colorized, textAlignment)); column.setCellValueFactory (cell -> { return new SimpleObjectProperty <> (cell.getValue ()); diff --git a/src/main/java/ru/shemplo/tbs/gfx/TBSMetaWrapper.java b/src/main/java/ru/shemplo/tbs/gfx/TBSMetaWrapper.java new file mode 100644 index 0000000..f7c7299 --- /dev/null +++ b/src/main/java/ru/shemplo/tbs/gfx/TBSMetaWrapper.java @@ -0,0 +1,15 @@ +package ru.shemplo.tbs.gfx; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter @Setter +@RequiredArgsConstructor +public class TBSMetaWrapper { + + private final T object; + + private boolean hovered; + +} diff --git a/src/main/java/ru/shemplo/tbs/gfx/TBSTableCell.java b/src/main/java/ru/shemplo/tbs/gfx/TBSTableCell.java index 85555f4..9553eac 100644 --- a/src/main/java/ru/shemplo/tbs/gfx/TBSTableCell.java +++ b/src/main/java/ru/shemplo/tbs/gfx/TBSTableCell.java @@ -4,26 +4,49 @@ import javafx.geometry.Pos; import javafx.scene.control.TableCell; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; -import lombok.RequiredArgsConstructor; +import ru.shemplo.tbs.TBSUtils; -@RequiredArgsConstructor -public class TBSTableCell extends TableCell { +public class TBSTableCell extends TableCell , TBSMetaWrapper > { private final Function converter; private final boolean colorizeNumbers; + private static final Background HOVER_BG = new Background (new BackgroundFill (Color.rgb (220, 240, 245, 1.0), null, null)); private static final Font COLOR_FONT = Font.font ("Consolas", FontWeight.NORMAL, 12.0); - public TBSTableCell (Function converter, boolean colorizeNumbers, Pos textAlignment) { + private Background defaultBackground; + + public TBSTableCell (Function converter, boolean colorizeNumbers) { this.converter = converter; this.colorizeNumbers = colorizeNumbers; + + hoverProperty ().addListener ((__, ___, hovered) -> { + if (getItem () == null) { return; } + + if (hovered) { + defaultBackground = getTableRow ().getBackground (); + getTableRow ().setBackground (HOVER_BG); + } else { + getTableRow ().setBackground (defaultBackground); + } + }); + } + + public TBSTableCell (Function converter, boolean colorizeNumbers, Pos textAlignment) { + this (converter, colorizeNumbers); setAlignment (textAlignment); } @Override - protected void updateItem (F item, boolean empty) { + protected void updateItem (TBSMetaWrapper item, boolean empty) { + if (TBSUtils.mapIfNN (item, TBSMetaWrapper::isHovered, false)) { + setBackground (new Background (new BackgroundFill (Color.YELLOW, null, null))); + } + if (item == getItem ()) { return; } super.updateItem (item, empty); @@ -34,12 +57,12 @@ protected void updateItem (F item, boolean empty) { return; } - final var value = converter.apply (item); + final var value = converter.apply (item.getObject ()); if (value instanceof Number n) { if (colorizeNumbers) { setFont (COLOR_FONT); - if (n.doubleValue () > 1e-6) { + if (n.doubleValue () > 1e-6) { setTextFill (Color.GREEN); } else if (n.doubleValue () < -1e-6) { setTextFill (Color.RED); diff --git a/src/main/java/ru/shemplo/tbs/gfx/TBSUIApplication.java b/src/main/java/ru/shemplo/tbs/gfx/TBSUIApplication.java index 695c326..7e8a0a4 100644 --- a/src/main/java/ru/shemplo/tbs/gfx/TBSUIApplication.java +++ b/src/main/java/ru/shemplo/tbs/gfx/TBSUIApplication.java @@ -23,13 +23,14 @@ import lombok.Getter; import ru.shemplo.tbs.Bond; import ru.shemplo.tbs.ITBSProfile; +import ru.shemplo.tbs.TBSUtils; public class TBSUIApplication extends Application { @Getter private static volatile TBSUIApplication instance; - private TableView table; + private TableView > table; private Text profileDetails; @Getter @@ -59,32 +60,32 @@ public void start (Stage stage) throws Exception { instance = this; } - private TableView initializeTable () { - final var table = new TableView (); + private TableView > initializeTable () { + final var table = new TableView > (); table.setBackground (new Background (new BackgroundFill (Color.LIGHTGRAY, CornerRadii.EMPTY, Insets.EMPTY))); VBox.setVgrow (table, Priority.ALWAYS); table.setSelectionModel (null); table.setBorder (Border.EMPTY); - final var exploreTinkoffColumn = new TableColumn ("T"); + final var exploreTinkoffColumn = new TableColumn , TBSMetaWrapper > ("T"); exploreTinkoffColumn.setCellValueFactory (cell -> new SimpleObjectProperty <> (cell.getValue ())); exploreTinkoffColumn.setCellFactory (__ -> new TBSExploreTableCell (true)); exploreTinkoffColumn.setMinWidth (30); table.getColumns ().add (exploreTinkoffColumn); - final var exploreMOEXColumn = new TableColumn ("M"); + final var exploreMOEXColumn = new TableColumn , TBSMetaWrapper > ("M"); exploreMOEXColumn.setCellValueFactory (cell -> new SimpleObjectProperty <> (cell.getValue ())); exploreMOEXColumn.setCellFactory (__ -> new TBSExploreTableCell (false)); exploreMOEXColumn.setMinWidth (30); table.getColumns ().add (exploreMOEXColumn); - final var ispectButtonColumn = new TableColumn ("C"); + final var ispectButtonColumn = new TableColumn , TBSMetaWrapper > ("C"); ispectButtonColumn.setCellValueFactory (cell -> new SimpleObjectProperty <> (cell.getValue ())); ispectButtonColumn.setCellFactory (__ -> new TBSInspectTableCell ()); ispectButtonColumn.setMinWidth (30); table.getColumns ().add (ispectButtonColumn); - final var shortNameColumn = makeTBSTableColumn ("Bond name", Bond::getName, false, false, 300.0); + final var shortNameColumn = makeTBSTableColumn ("Bond name", Bond::getName, false, false, 250.0); table.getColumns ().add (shortNameColumn); final var codeColumn = makeTBSTableColumn ("Code", Bond::getCode, false, false, 125.0); @@ -93,6 +94,9 @@ private TableView initializeTable () { final var currencyColumn = makeTBSTableColumn ("Currency", Bond::getCurrency, false, false, 90.0); table.getColumns ().add (currencyColumn); + final var lotsColumn = makeTBSTableColumn ("👝", Bond::getLots, false, true, 30.0); + table.getColumns ().add (lotsColumn); + final var scoreColumn = makeTBSTableColumn ("Score", Bond::getScore, false, true, 90.0); table.getColumns ().add (scoreColumn); @@ -129,10 +133,10 @@ private TableView initializeTable () { return table; } - public static TableColumn makeTBSTableColumn ( + public static TableColumn , TBSMetaWrapper > makeTBSTableColumn ( String name, Function converter, boolean sortable, boolean colorized, double minWidth ) { - final var column = new TableColumn (name); + final var column = new TableColumn , TBSMetaWrapper > (name); column.setCellFactory (__ -> new TBSTableCell <> (converter, colorized)); column.setCellValueFactory (cell -> { return new SimpleObjectProperty <> (cell.getValue ()); @@ -145,7 +149,9 @@ public static TableColumn makeTBSTableColumn ( public void applyData (ITBSProfile profile, List bonds) { profileDetails.setText (profile.getProfileDescription ()); - table.setItems (FXCollections.observableArrayList (bonds)); + table.setItems (FXCollections.observableArrayList ( + TBSUtils.mapToList (bonds, TBSMetaWrapper::new) + )); this.profile = profile; }