Skip to content

Commit

Permalink
Number of lots + hovered line + score calculation update
Browse files Browse the repository at this point in the history
  • Loading branch information
Shemplo committed Oct 9, 2021
1 parent 8b955d4 commit 6fd52cc
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 37 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>ru.shemplo</groupId>
<artifactId>tinkoff-bond-scanner</artifactId>
<version>1.0</version>
<version>1.1</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/ru/shemplo/tbs/Bond.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ());
Expand All @@ -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);
Expand Down Expand Up @@ -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
}

}
28 changes: 25 additions & 3 deletions src/main/java/ru/shemplo/tbs/RunTinkoffBondScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
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;

import javafx.application.Application;
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;

Expand Down Expand Up @@ -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 <String, Integer> 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 <Bond> bonds) {
log.info ("Analizing loaded bonds (total: {})...", bonds.size ());
bonds.forEach (bond -> bond.updateScore (profile));
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/ru/shemplo/tbs/TBSUtils.java
Original file line number Diff line number Diff line change
@@ -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 <F, S> List <S> mapToList (Collection <F> values, Function <F, S> converter) {
return values.stream ().map (converter).collect (Collectors.toList ());
}

public static <F> F aOrB (F a, F b) {
return a == null ? b : a;
}

public static <F, S> S mapIfNN (F value, Function <F, S> converter, S defaultValue) {
return aOrB (value == null ? null : converter.apply (value), defaultValue);
}

public static <F> void doIfNN (F value, Consumer <F> action) {
if (value != null) { action.accept (value); }
}

}
9 changes: 5 additions & 4 deletions src/main/java/ru/shemplo/tbs/gfx/TBSExploreTableCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import javafx.scene.text.Text;
import ru.shemplo.tbs.Bond;

public class TBSExploreTableCell extends TBSTableCell <Bond, Void> {
public class TBSExploreTableCell extends TBSTableCell <Bond, Bond> {

private boolean openInTinkoff;

Expand All @@ -19,21 +19,22 @@ public TBSExploreTableCell (boolean openInTinkoff) {
}

@Override
protected void updateItem (Bond item, boolean empty) {
protected void updateItem (TBSMetaWrapper <Bond> item, boolean empty) {
super.updateItem (item, empty);
setText (null);

if (item != null) {
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
));
}
}
Expand Down
21 changes: 12 additions & 9 deletions src/main/java/ru/shemplo/tbs/gfx/TBSInspectTableCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Bond, Void> {

Expand All @@ -38,7 +39,7 @@ public TBSInspectTableCell () {
}

@Override
protected void updateItem (Bond item, boolean empty) {
protected void updateItem (TBSMetaWrapper <Bond> item, boolean empty) {
super.updateItem (item, empty);
setText (null);

Expand All @@ -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);
Expand All @@ -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 <Coupon> initializeTable (Bond bond) {
final var table = new TableView <Coupon> ();
private TableView <TBSMetaWrapper <Coupon>> initializeTable (Bond bond) {
final var table = new TableView <TBSMetaWrapper <Coupon>> ();
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);
Expand All @@ -103,17 +106,17 @@ private TableView <Coupon> initializeTable (Bond bond) {
return table;
}

public static <T> TableColumn <Coupon, Coupon> makeTBSTableColumn (
public static <T> TableColumn <TBSMetaWrapper <Coupon>, TBSMetaWrapper <Coupon>> makeTBSTableColumn (
String name, Function <Coupon, T> converter, boolean sortable, boolean colorized, double minWidth
) {
return makeTBSTableColumn (name, converter, sortable, colorized, Pos.BASELINE_LEFT, minWidth);
}

public static <T> TableColumn <Coupon, Coupon> makeTBSTableColumn (
public static <T> TableColumn <TBSMetaWrapper <Coupon>, TBSMetaWrapper <Coupon>> makeTBSTableColumn (
String name, Function <Coupon, T> converter, boolean sortable, boolean colorized,
Pos textAlignment, double minWidth
) {
final var column = new TableColumn <Coupon, Coupon> (name);
final var column = new TableColumn <TBSMetaWrapper <Coupon>, TBSMetaWrapper <Coupon>> (name);
column.setCellFactory (__ -> new TBSTableCell <> (converter, colorized, textAlignment));
column.setCellValueFactory (cell -> {
return new SimpleObjectProperty <> (cell.getValue ());
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/ru/shemplo/tbs/gfx/TBSMetaWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.shemplo.tbs.gfx;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

@Getter @Setter
@RequiredArgsConstructor
public class TBSMetaWrapper <T> {

private final T object;

private boolean hovered;

}
37 changes: 30 additions & 7 deletions src/main/java/ru/shemplo/tbs/gfx/TBSTableCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <F, S> extends TableCell <F, F> {
public class TBSTableCell <F, S> extends TableCell <TBSMetaWrapper <F>, TBSMetaWrapper <F>> {

private final Function <F, S> 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 <F, S> converter, boolean colorizeNumbers, Pos textAlignment) {
private Background defaultBackground;

public TBSTableCell (Function <F, S> 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 <F, S> converter, boolean colorizeNumbers, Pos textAlignment) {
this (converter, colorizeNumbers);
setAlignment (textAlignment);
}

@Override
protected void updateItem (F item, boolean empty) {
protected void updateItem (TBSMetaWrapper <F> 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);

Expand All @@ -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);
Expand Down
Loading

0 comments on commit 6fd52cc

Please sign in to comment.