Skip to content

Commit fefb230

Browse files
committed
Load total RUB balance from accounts + refactoring
1 parent 8d62a9f commit fefb230

14 files changed

+331
-146
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>ru.shemplo</groupId>
66
<artifactId>tinkoff-bond-scanner</artifactId>
7-
<version>1.3</version>
7+
<version>1.4</version>
88

99
<properties>
1010
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

src/main/java/ru/shemplo/tbs/RunTinkoffBondScanner.java

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package ru.shemplo.tbs;
22

33
import java.io.File;
4-
import java.io.IOException;
5-
import java.nio.file.Files;
6-
import java.nio.file.Paths;
74
import java.util.Date;
85
import java.util.Locale;
96
import java.util.Scanner;
@@ -15,9 +12,6 @@
1512
import ru.shemplo.tbs.entity.ITBSProfile;
1613
import ru.shemplo.tbs.entity.PlanningDump;
1714
import ru.shemplo.tbs.gfx.TBSUIApplication;
18-
import ru.tinkoff.invest.openapi.OpenApi;
19-
import ru.tinkoff.invest.openapi.model.rest.SandboxRegisterRequest;
20-
import ru.tinkoff.invest.openapi.okhttp.OkHttpOpenApi;
2115

2216
@Slf4j
2317
public class RunTinkoffBondScanner {
@@ -48,50 +42,25 @@ public static void main (String ... args) {
4842
} else if ("q".equals (decision)) {
4943
return;
5044
} else {
51-
initializeProfile (profile);
45+
loadCurrencyQuotes (profile);
5246
}
5347
} else {
54-
initializeProfile (profile);
48+
loadCurrencyQuotes (profile);
5549
}
5650
}
5751

58-
private static void initializeProfile (ITBSProfile profile) {
59-
try {
60-
log.info ("Reading token from file...");
61-
final var token = Files.readString (Paths.get (profile.getTokenFilename ()));
62-
openConnection (profile, token);
63-
} catch (IOException ioe) {
64-
log.error ("Failed to read token: " + ioe, ioe);
65-
}
66-
}
67-
68-
private static void openConnection (ITBSProfile profile, String token) {
69-
log.info ("Connecting to Tinkoff API...");
70-
log.info ("Profile: {}", profile);
71-
try (final var client = new OkHttpOpenApi (token, !profile.isHighResponsible ())) {
72-
log.info ("Perform registration in Tinkoff API...");
73-
if (client.isSandboxMode ()) {
74-
client.getSandboxContext ().performRegistration (new SandboxRegisterRequest ()).join ();
75-
}
76-
77-
loadCurrencyQuotes (profile, client);
78-
} catch (Exception e) {
79-
log.error ("Unexpected exception in client: " + e, e);
80-
}
81-
}
82-
83-
private static void loadCurrencyQuotes (ITBSProfile profile, OpenApi client) {
52+
private static void loadCurrencyQuotes (ITBSProfile profile) {
8453
final var currencyManager = TBSCurrencyManager.getInstance ();
85-
currencyManager.initialize (profile, client, log);
54+
currencyManager.initialize (profile);
8655

8756
log.info ("Quotes: {}", currencyManager.getStringQuotes ());
8857

89-
searchForBonds (profile, client);
58+
searchForBonds (profile);
9059
}
9160

92-
private static void searchForBonds (ITBSProfile profile, OpenApi client) {
61+
private static void searchForBonds (ITBSProfile profile) {
9362
final var bondManager = TBSBondManager.getInstance ();
94-
bondManager.initialize (profile, client, log);
63+
bondManager.initialize (profile);
9564

9665
analizeBonds (profile);
9766
}
@@ -151,9 +120,7 @@ private static void restorePlanningBonds () {
151120

152121
private static void showResults (ITBSProfile profile) {
153122
log.info ("Starting UI...");
154-
new Thread (() -> {
155-
Application.launch (TBSUIApplication.class);
156-
}, "Primary-Window-Thread").start ();
123+
new Thread (() -> Application.launch (TBSUIApplication.class)).start ();
157124

158125
while (TBSUIApplication.getInstance () == null) {}
159126

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package ru.shemplo.tbs;
2+
3+
import java.util.concurrent.ExecutorService;
4+
import java.util.concurrent.Executors;
5+
6+
import lombok.AccessLevel;
7+
import lombok.NoArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
@Slf4j
11+
@NoArgsConstructor (access = AccessLevel.PRIVATE)
12+
public class TBSBackgroundExecutor implements AutoCloseable {
13+
14+
private static volatile TBSBackgroundExecutor instance;
15+
16+
public static TBSBackgroundExecutor getInstance () {
17+
if (instance == null) {
18+
synchronized (TBSClient.class) {
19+
if (instance == null) {
20+
instance = new TBSBackgroundExecutor ();
21+
}
22+
}
23+
}
24+
25+
return instance;
26+
}
27+
28+
private final ExecutorService executors = Executors.newSingleThreadExecutor ();
29+
30+
public void runInBackground (Runnable task) {
31+
executors.execute (task);
32+
}
33+
34+
@Override
35+
public void close () throws Exception {
36+
log.info ("Stopping background executors...");
37+
executors.shutdown ();
38+
}
39+
40+
}

src/main/java/ru/shemplo/tbs/TBSBondManager.java

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111
import java.util.Map;
1212
import java.util.stream.Collectors;
1313

14-
import org.slf4j.Logger;
15-
14+
import lombok.AccessLevel;
15+
import lombok.NoArgsConstructor;
16+
import lombok.extern.slf4j.Slf4j;
1617
import ru.shemplo.tbs.entity.Bond;
1718
import ru.shemplo.tbs.entity.ITBSProfile;
18-
import ru.tinkoff.invest.openapi.OpenApi;
1919
import ru.tinkoff.invest.openapi.model.rest.Currency;
2020
import ru.tinkoff.invest.openapi.model.rest.InstrumentType;
2121

22+
@Slf4j
23+
@NoArgsConstructor (access = AccessLevel.PRIVATE)
2224
public class TBSBondManager implements Serializable {
2325

2426
private static final long serialVersionUID = 1L;
@@ -74,23 +76,31 @@ public static Bond getBondByTicker (String ticker, boolean scannedPreferred) {
7476
private transient Map <String, Bond> ticker2portfolio = new HashMap <> ();
7577
private transient Map <String, Bond> ticker2scanned = new HashMap <> ();
7678

77-
public void initialize (ITBSProfile profile, OpenApi client, Logger log) {
78-
log.info ("Loading bonds from portfolio (with data from Tinkoff and MOEX)...");
79-
portfolio = client.getUserContext ().getAccounts ().join ().getAccounts ().parallelStream ()
80-
. flatMap (acc -> {
81-
return client.getPortfolioContext ().getPortfolio (acc.getBrokerAccountId ()).join ()
82-
. getPositions ().stream ();
83-
})
84-
. filter (pos -> pos.getInstrumentType () == InstrumentType.BOND)
85-
. map (Bond::new).collect (Collectors.toList ());
86-
87-
log.info ("Loading data abount bonds from Tinkoff and MOEX...");
88-
scanned = client.getMarketContext ().getMarketBonds ().join ().getInstruments ().stream ()
89-
. filter (instrument -> profile.getCurrencies ().contains (instrument.getCurrency ())).parallel ()
90-
. map (Bond::new).filter (profile::testBond)//.limit (profile.getMaxResults ())
91-
. collect (Collectors.toList ());
92-
93-
updateMapping ();
79+
public void initialize (ITBSProfile profile) {
80+
try {
81+
final var client = TBSClient.getInstance ().getConnection (profile);
82+
83+
log.info ("Loading bonds from portfolio (with data from Tinkoff and MOEX)...");
84+
portfolio = client.getUserContext ().getAccounts ().join ().getAccounts ().parallelStream ()
85+
. flatMap (acc -> {
86+
return client.getPortfolioContext ().getPortfolio (acc.getBrokerAccountId ()).join ()
87+
. getPositions ().stream ();
88+
})
89+
. filter (pos -> pos.getInstrumentType () == InstrumentType.BOND)
90+
. map (Bond::new).collect (Collectors.toList ());
91+
92+
log.info ("Loading data abount bonds from Tinkoff and MOEX...");
93+
scanned = client.getMarketContext ().getMarketBonds ().join ().getInstruments ().stream ()
94+
. filter (instrument -> profile.getCurrencies ().contains (instrument.getCurrency ())).parallel ()
95+
. map (Bond::new).filter (profile::testBond)//.limit (profile.getMaxResults ())
96+
. collect (Collectors.toList ());
97+
98+
updateMapping ();
99+
} catch (IOException ioe) {
100+
log.error ("Failed to scan bonds", ioe);
101+
portfolio = List.of ();
102+
scanned = List.of ();
103+
}
94104
}
95105

96106
public void analize (ITBSProfile profile) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package ru.shemplo.tbs;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Paths;
6+
7+
import lombok.AccessLevel;
8+
import lombok.NoArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import ru.shemplo.tbs.entity.ITBSProfile;
11+
import ru.tinkoff.invest.openapi.OpenApi;
12+
import ru.tinkoff.invest.openapi.model.rest.SandboxRegisterRequest;
13+
import ru.tinkoff.invest.openapi.okhttp.OkHttpOpenApi;
14+
15+
@Slf4j
16+
@NoArgsConstructor (access = AccessLevel.PRIVATE)
17+
public class TBSClient implements AutoCloseable {
18+
19+
private static volatile TBSClient instance;
20+
21+
public static TBSClient getInstance () {
22+
if (instance == null) {
23+
synchronized (TBSClient.class) {
24+
if (instance == null) {
25+
instance = new TBSClient ();
26+
}
27+
}
28+
}
29+
30+
return instance;
31+
}
32+
33+
private volatile OpenApi connection;
34+
35+
private String readToken (ITBSProfile profile) throws IOException {
36+
log.info ("Reading token from file...");
37+
return Files.readString (Paths.get (profile.getTokenFilename ()));
38+
}
39+
40+
public OpenApi getConnection (ITBSProfile profile) throws IOException {
41+
if (connection == null) {
42+
synchronized (this) {
43+
if (connection == null) {
44+
final var token = readToken (profile);
45+
if (token == null) { return null; }
46+
47+
log.info ("Connecting to Tinkoff API...");
48+
log.info ("Profile: {}", profile);
49+
connection = new OkHttpOpenApi (token, !profile.isHighResponsible ());
50+
log.info ("Perform registration in Tinkoff API...");
51+
if (connection.isSandboxMode ()) {
52+
connection.getSandboxContext ().performRegistration (
53+
new SandboxRegisterRequest ()
54+
).join ();
55+
}
56+
}
57+
}
58+
}
59+
60+
return connection;
61+
}
62+
63+
public void close () throws IOException {
64+
if (connection != null) {
65+
log.info ("Closing connection to Tinkoff API...");
66+
connection.close ();
67+
}
68+
}
69+
70+
}

src/main/java/ru/shemplo/tbs/TBSCurrencyManager.java

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@
1010
import java.util.Objects;
1111
import java.util.stream.Collectors;
1212

13-
import org.slf4j.Logger;
14-
13+
import lombok.AccessLevel;
14+
import lombok.NoArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
1516
import ru.shemplo.tbs.entity.ITBSProfile;
16-
import ru.tinkoff.invest.openapi.OpenApi;
1717
import ru.tinkoff.invest.openapi.model.rest.Candle;
1818
import ru.tinkoff.invest.openapi.model.rest.CandleResolution;
1919
import ru.tinkoff.invest.openapi.model.rest.Currency;
2020

21+
@Slf4j
22+
@NoArgsConstructor (access = AccessLevel.PRIVATE)
2123
public class TBSCurrencyManager implements Serializable {
2224

2325
private static final long serialVersionUID = 1L;
@@ -38,24 +40,31 @@ public static TBSCurrencyManager getInstance () {
3840

3941
private Map <Currency, Double> currency2coefficient;
4042

41-
public void initialize (ITBSProfile profile, OpenApi client, Logger log) {
42-
log.info ("Loading current currency quotes from Tinkoff...");
43-
currency2coefficient = client.getMarketContext ().getMarketCurrencies ().join ().getInstruments ().stream ()
44-
. map (cur -> {
45-
final var currency = TBSUtils.getCurrencyByTicker (cur.getTicker ());
46-
if (currency.isEmpty ()) { return null; }
47-
48-
final var now = OffsetDateTime.now ();
49-
final var coeff = client.getMarketContext ().getMarketCandles (
50-
cur.getFigi (), now.minusDays (3), now, CandleResolution.DAY
51-
).join ().flatMap (res -> res.getCandles ().stream ().reduce ((acc, candle) -> {
52-
return candle.getTime ().isAfter (acc.getTime ()) ? candle : acc;
53-
})).map (Candle::getC).orElse (BigDecimal.ONE).doubleValue ();
54-
55-
return Map.entry (currency.get (), coeff);
56-
})
57-
. filter (Objects::nonNull)
58-
. collect (Collectors.toMap (Entry::getKey, Entry::getValue));
43+
public void initialize (ITBSProfile profile) {
44+
try {
45+
final var client = TBSClient.getInstance ().getConnection (profile);
46+
47+
log.info ("Loading current currency quotes from Tinkoff...");
48+
currency2coefficient = client.getMarketContext ().getMarketCurrencies ().join ().getInstruments ().stream ()
49+
. map (cur -> {
50+
final var currency = TBSUtils.getCurrencyByTicker (cur.getTicker ());
51+
if (currency.isEmpty ()) { return null; }
52+
53+
final var now = OffsetDateTime.now ();
54+
final var coeff = client.getMarketContext ().getMarketCandles (
55+
cur.getFigi (), now.minusDays (3), now, CandleResolution.DAY
56+
).join ().flatMap (res -> res.getCandles ().stream ().reduce ((acc, candle) -> {
57+
return candle.getTime ().isAfter (acc.getTime ()) ? candle : acc;
58+
})).map (Candle::getC).orElse (BigDecimal.ONE).doubleValue ();
59+
60+
return Map.entry (currency.get (), coeff);
61+
})
62+
. filter (Objects::nonNull)
63+
. collect (Collectors.toMap (Entry::getKey, Entry::getValue));
64+
} catch (IOException ioe) {
65+
log.error ("Failed to load currencies", ioe);
66+
currency2coefficient = Map.of ();
67+
}
5968
}
6069

6170
public String getStringQuotes () {

src/main/java/ru/shemplo/tbs/TBSDumpService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
import java.io.ObjectOutputStream;
88
import java.io.Serializable;
99

10+
import lombok.AccessLevel;
11+
import lombok.NoArgsConstructor;
1012
import lombok.extern.slf4j.Slf4j;
1113

1214
@Slf4j
15+
@NoArgsConstructor (access = AccessLevel.PRIVATE)
1316
public class TBSDumpService {
1417

1518
private static volatile TBSDumpService instance;

src/main/java/ru/shemplo/tbs/TBSPlanner.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
import javafx.beans.property.SimpleIntegerProperty;
1717
import javafx.collections.FXCollections;
1818
import javafx.collections.ObservableList;
19+
import lombok.AccessLevel;
1920
import lombok.Getter;
21+
import lombok.NoArgsConstructor;
2022
import ru.shemplo.tbs.entity.IPlanningBond;
2123
import ru.shemplo.tbs.entity.PlanningBond;
2224
import ru.shemplo.tbs.entity.PlanningDump;
2325

26+
@NoArgsConstructor (access = AccessLevel.PRIVATE)
2427
public class TBSPlanner implements Serializable {
2528

2629
private static final long serialVersionUID = 1L;

0 commit comments

Comments
 (0)