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;
}