diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..8967089
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ sqlite.xerial
+ true
+ org.sqlite.JDBC
+ jdbc:sqlite:identifier.sqlite
+ $ProjectFileDir$
+
+
+ sqlite.xerial
+ true
+ org.sqlite.JDBC
+ jdbc:sqlite:C:\Users\isy02\IdeaProjects\test228\src\main\resources\companies.db
+ $ProjectFileDir$
+
+
+ file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.36.0.3/sqlite-jdbc-3.36.0.3.jar
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..53072d4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+# Торговый бот на Tinkoff Invest API
+Торговый бот реализующий торговлю инструментами (на данном этапе фондами и акциями), основывающий решения о покупке/продаже на основе
+RSI. Есть возможность добавлять/удалять инструменты, которыми ведется торговля,
+ограничивать количество средств, доступных боту для торговли одним инструментом. Управление ботов ведется через ввод команд в консоль. Подробнее о доступных командах ниже.
+
+## Структура проекта
+Торговый бот реализован в качестве интерактивного консольного приложения, есть возможность быстрого расширения функционала в виде подключения к базе данных и графического пользовательского интерфейса.
+Логически приложение разделено на четыре части:
+1. Взаимодействие с пользователем
+Получение команд, обработка запросов и исключительных ситуаций
+2. Вычислительная
+По полученным данным вычисляются коэффициенты (RSI, NVI, PVI; в данной программе реализован только RSI), по ним принимаются решения о купле и продаже
+3. Соединительная
+Создание потоков и унарных запросов к API для получения и отправки данных, на основе принятых решений и вычисленных индексов
+4. Хранение данных
+Хранение истории всех сделок, компании с доступными компаниями для торговли и индексами
+## Установка и запуск
+Установите java 11 и выше
+Далее откройте командную строку (в Windows от имени администратора) и перейдите в директорию с jar файлом
+Введите java -jar НазваниеФайла.jar
+## Начало работы
+При запуске бота будет выведен запрос на введение токена. Токен можно сгенерировать на сайте Тинькофф инвестиции в разделе токен.
+Далее нужно выбрать номер аккаунта, по которому будет вестись торговля. Затем включается интерактивный режим с возможностью ввода команд.
+Первой введите команду help для ознакомления со всеми возможностями бота.
+
+## Доступные команды
+
+- help - список всех команд
+
- add - Добавление инструмента. Нужно ввести FIGI инструмента, деньги, предоставляемые боту для торговли инструментом, максимальный процент просадки, после которого происходит продажа, процент прибыли, после которого бот начинает продавать инструмент
+
- changeCompany - Изменение параметров для торговли (stop loss, take-profit, free money, etc...)
+
- startTrade - Начать торговать инструментом. Команда add лишь добавляет инструмент в программу, для торговли используйте эту команду
+
- stopTrade - Перестать торговать инструментом
+
- delete - Удалить инструмент из программы
+
- printSchedule - Вывести расписание бирж
+
- exit - Выход из программы. Вся торговля останавливается. На данном этапе при следующем запуске программа не будет помнить торги с предыдущей сессии
diff --git a/out/artifacts/TinkoffBot_jar/TinkoffBot.jar b/out/artifacts/TinkoffBot_jar/TinkoffBot.jar
deleted file mode 100644
index 16338d9..0000000
Binary files a/out/artifacts/TinkoffBot_jar/TinkoffBot.jar and /dev/null differ
diff --git a/pom.xml b/pom.xml
index 2547213..56e5d1f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,16 +14,24 @@
java-sdk-core
1.0-M8
+
org.slf4j
slf4j-api
2.0.0-alpha7
+
org.slf4j
slf4j-simple
2.0.0-alpha7
+
+
+ org.xerial
+ sqlite-jdbc
+ 3.36.0.3
+
diff --git a/src/main/java/Connection/CandleStream.java b/src/main/java/Connection/CandleStream.java
index f6dad34..adde0b7 100644
--- a/src/main/java/Connection/CandleStream.java
+++ b/src/main/java/Connection/CandleStream.java
@@ -53,7 +53,6 @@ public void initialize(DataStreamProcessor processor) throws OutNumberOfReconnec
*/
public void updateSubscription() {
if(!companies.getFigisOfTradingCompanies().isEmpty()) {
- System.out.println(companies.getFigisOfTradingCompanies().get(0));
stream.subscribeCandles(companies.getFigisOfTradingCompanies(), SubscriptionInterval.SUBSCRIPTION_INTERVAL_ONE_MINUTE);
}
}
diff --git a/src/main/java/Connection/TradeStream.java b/src/main/java/Connection/TradeStream.java
index be6abb8..02a3a9e 100644
--- a/src/main/java/Connection/TradeStream.java
+++ b/src/main/java/Connection/TradeStream.java
@@ -49,6 +49,7 @@ public void buyStock(long lots, Quotation price, String figi) throws CompanyNotF
System.out.println("going to buy");
orderId = Double.valueOf(Math.random()).hashCode();
+ //todo: verification of params
var orderResponse = tradeServ.postOrderSync(
figi,
lots,
@@ -62,7 +63,7 @@ public void buyStock(long lots, Quotation price, String figi) throws CompanyNotF
if(orderResponse.getExecutionReportStatus().equals(OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_FILL) ||
orderResponse.getExecutionReportStatus().equals(OrderExecutionReportStatus.EXECUTION_REPORT_STATUS_PARTIALLYFILL)){
Company curComp = companies.getByFigi(figi);
- System.out.println(" " + orderResponse.getLotsExecuted() + " " +
+ System.out.println("Buy lot^: " + orderResponse.getLotsExecuted() + " price: " +
orderResponse.getExecutedOrderPrice().toString() + " \n");
curComp.buyShares(
@@ -85,6 +86,7 @@ public void buyStock(long lots, Quotation price, String figi) throws CompanyNotF
*/
public void sellStock(long lots, Quotation price, String figi, Deal deal) throws CompanyNotFoundException {
System.out.println("Going to sell");
+ //todo: verification of params
var orderResponse = tradeServ.postOrderSync(
figi,
lots,
diff --git a/src/main/java/Data/Company.java b/src/main/java/Data/Company.java
index 29a8a40..13f9dae 100644
--- a/src/main/java/Data/Company.java
+++ b/src/main/java/Data/Company.java
@@ -95,7 +95,7 @@ public void buyShares(long lotNumber, BigDecimal price, String id) {
}
/**
- * change company freemoney, openDeals and shareValue after selling some lots
+ * change company free money, openDeals and shareValue after selling some lots
* @param deal
* @param lotNumber
* @param price
diff --git a/src/main/java/Data/OpenDeals.java b/src/main/java/Data/OpenDeals.java
index 23bde4e..4f8232c 100644
--- a/src/main/java/Data/OpenDeals.java
+++ b/src/main/java/Data/OpenDeals.java
@@ -9,7 +9,7 @@
/**
* Class for working with history of openDeals - staff, that bot bought, and should to sell in some cases
* Deal store price of buying, date, price of stopLoss, number of bought lots
- * Open deals stores concurrent List, in wich you can add deal, delete deal, print etc.
+ * Open deals stores concurrent List, in which you can add deal, delete deal, print etc.
* Class-wrapper for collection of deals
*/
public class OpenDeals {
@@ -50,7 +50,9 @@ public void deletePartly(Deal deal, long numberOfSoldLots) {
addDeal(new Deal(deal.getLotNumber() - numberOfSoldLots,
deal.getPrice(),
deal.getStopPrice(),
- deal.getId()));
+ String.valueOf(Double.valueOf(Math.random()).hashCode())
+ )
+ );
}
deleteDeal(deal);
}
@@ -64,7 +66,7 @@ public List getDealsAsList() {
}
/**
- * sort opendeals by price
+ * sort open deals by price
*/
public void sortByPrices() {
this.openDeals.sort(Comparator.comparing(Deal::getPrice));
diff --git a/src/main/java/Database/DatabaseConnector.java b/src/main/java/Database/DatabaseConnector.java
new file mode 100644
index 0000000..9bd2820
--- /dev/null
+++ b/src/main/java/Database/DatabaseConnector.java
@@ -0,0 +1,127 @@
+package Database;
+
+import UI.Console.Console;
+
+import java.sql.*;
+
+public class DatabaseConnector {
+ private Connection connection;
+ private final String JDBC_DRIVER = "org.sqlite.JDBC";
+
+ public void connect() {
+ try {
+ Class.forName(JDBC_DRIVER);
+ connection = DriverManager.getConnection("jdbc:sqlite:src/main/resources/companies.db");
+ Console.println("Successful database connection");
+ } catch (ClassNotFoundException exception) {
+ Console.printError("Driver cannot find!");
+ } catch (SQLException exception) {
+ Console.printError("Database connection error");
+ }
+ }
+
+ /**
+ * @param sqlStatement SQL statement to be prepared.
+ * @param generateKeys Is keys needed to be generated.
+ * @return Prepared statement.
+ * @throws SQLException When there's exception inside.
+ */
+ public PreparedStatement getPreparedStatement(String sqlStatement, boolean generateKeys) throws SQLException {
+ PreparedStatement preparedStatement;
+ try {
+ if (connection == null) throw new SQLException();
+ int autoKey = generateKeys ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS;
+ preparedStatement = connection.prepareStatement(sqlStatement, autoKey);
+ return preparedStatement;
+ } catch (SQLException exception) {
+ throw new SQLException(exception);
+ }
+ }
+
+ /**
+ * Close prepared statement.
+ * @param sqlStatement SQL statement to be closed.
+ */
+ public void closePreparedStatement(PreparedStatement sqlStatement) {
+ if (sqlStatement == null) return;
+
+ try {
+ sqlStatement.close();
+ } catch (SQLException exception) {
+ Console.printError("It's not possible to close prepared statement");
+ }
+ }
+
+ /**
+ * Close connection to database.
+ */
+ public void closeConnection() {
+ if (connection == null) return;
+ try {
+ connection.close();
+ Console.println("Database connection finished.");
+ } catch (SQLException exception) {
+ Console.printError("Database connection error!");
+ }
+ }
+
+ /**
+ * Set commit mode of database.
+ */
+ public void setCommitMode() {
+ try {
+ if (connection == null) throw new SQLException();
+ connection.setAutoCommit(false);
+ } catch (SQLException exception) {
+ Console.printError("An error occurred while setting the database transaction mode!");
+ }
+ }
+
+ /**
+ * Set normal mode of database.
+ */
+ public void setNormalMode() {
+ try {
+ if (connection == null) throw new SQLException();
+ connection.setAutoCommit(true);
+ } catch (SQLException exception) {
+ Console.printError("An error occurred while establishing normal database mode!");
+ }
+ }
+
+ /**
+ * Commit database status.
+ */
+ public void commit() {
+ try {
+ if (connection == null) throw new SQLException();
+ connection.commit();
+ } catch (SQLException exception) {
+ Console.printError("An error occurred while validating the new state of the database!");
+ }
+ }
+
+ /**
+ * Roll back database status.
+ */
+ public void rollback() {
+ try {
+ if (connection == null) throw new SQLException();
+ connection.rollback();
+ } catch (SQLException exception) {
+ Console.printError("An error occurred while reverting the original state of the database!");
+ }
+ }
+
+ /**
+ * Set save point of database.
+ */
+ public void setSavepoint() {
+ try {
+ if (connection == null) throw new SQLException();
+ connection.setSavepoint();
+ } catch (SQLException exception) {
+ Console.printError("Database saving error!");
+ }
+ }
+}
diff --git a/src/main/java/Database/DatabaseManager.java b/src/main/java/Database/DatabaseManager.java
new file mode 100644
index 0000000..1db7199
--- /dev/null
+++ b/src/main/java/Database/DatabaseManager.java
@@ -0,0 +1,145 @@
+package Database;
+
+import Data.Company;
+import UI.Console.Console;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashMap;
+
+public class DatabaseManager {
+ private final DatabaseConnector databaseConnector;
+
+ private static final String COMPANIES_TABLE = "companies";
+ private static final String COMPANIES_TABLE_ID_COLUMN = "id";
+ private static final String COMPANIES_TABLE_NAME_COLUMN = "name";
+
+ private final String SELECT_ALL_COMPANIES = "SELECT * FROM " + COMPANIES_TABLE;
+
+ private final String INSERT_COMPANY = "INSERT INTO " +
+ COMPANIES_TABLE + " (" +
+ COMPANIES_TABLE_ID_COLUMN + ", " +
+ COMPANIES_TABLE_NAME_COLUMN + ") VALUES (?, ?)";
+ private final String DELETE_COMPANY_BY_ID = "DELETE FROM " + COMPANIES_TABLE +
+ " WHERE " + COMPANIES_TABLE_ID_COLUMN + " = ?";
+
+ public DatabaseManager(DatabaseConnector databaseConnector) {
+ this.databaseConnector = databaseConnector;
+ }
+
+ /**
+ * Inserts company to table in database
+ * @param company to add
+ */
+ public void insertCompany(Company company) {
+
+ PreparedStatement preparedInsertCompanyStatement = null;
+
+ try {
+ databaseConnector.setCommitMode();
+ databaseConnector.setSavepoint();
+
+ preparedInsertCompanyStatement = databaseConnector.getPreparedStatement(INSERT_COMPANY, true);
+ preparedInsertCompanyStatement.setString(1, company.getFigi());
+ preparedInsertCompanyStatement.setString(2, "name" + Math.random());
+ if (preparedInsertCompanyStatement.executeUpdate() == 0) throw new SQLException();
+ Console.println("Request completed INSERT_COMPANY.");
+
+ databaseConnector.commit();
+
+ } catch (SQLException exception) {
+ Console.printError("An error occurred while executing a group of requests to add a new object!\n" + exception.getSQLState() + "\n");
+ exception.printStackTrace();
+ databaseConnector.rollback();
+
+ } finally {
+ databaseConnector.closePreparedStatement(preparedInsertCompanyStatement);
+ databaseConnector.setNormalMode();
+ }
+
+ }
+
+ /**
+ * Create Company.
+ * @param resultSet Result set parameters of Band.
+ * @return New Company.
+ * @throws SQLException When there's exception inside.
+ */
+ private Company createCompany(ResultSet resultSet) throws SQLException {
+
+ String figi = resultSet.getString(COMPANIES_TABLE_ID_COLUMN);
+ String name = resultSet.getString(COMPANIES_TABLE_NAME_COLUMN);
+
+ return new Company(
+ figi,
+ 0,
+ 0,
+ 0,
+ 0
+ );
+
+ }
+
+ /**
+ * @return List of Companies.
+ */
+ public HashMap getCollection() {
+
+ HashMap companyCollection = new HashMap<>();
+ PreparedStatement preparedSelectAllStatement = null;
+
+ try {
+
+ preparedSelectAllStatement = databaseConnector.getPreparedStatement(SELECT_ALL_COMPANIES, false);
+ ResultSet resultSet = preparedSelectAllStatement.executeQuery();
+
+ while (resultSet.next()) {
+ Company newCompany = createCompany(resultSet);
+ companyCollection.put(newCompany.getFigi(), newCompany);
+ }
+
+ } catch (SQLException exception) {
+ Console.printError("Request executing error");
+
+ } finally {
+ databaseConnector.closePreparedStatement(preparedSelectAllStatement);
+ }
+
+ return companyCollection;
+ }
+
+ /**
+ * Delete Company by figi.
+ * @param figi figi of Company.
+ */
+ public void deleteCompanyByFigi(String figi){
+
+ PreparedStatement preparedDeleteCompanyByFigiStatement = null;
+
+ try {
+ preparedDeleteCompanyByFigiStatement = databaseConnector.getPreparedStatement(DELETE_COMPANY_BY_ID, false);
+ preparedDeleteCompanyByFigiStatement.setString(1, figi);
+
+ if (preparedDeleteCompanyByFigiStatement.executeUpdate() == 0) Console.println("Updating status 0");
+
+ } catch (SQLException exception) {
+ Console.printError("Executing request error: DELETE_BAND_BY_ID!");
+
+ } finally {
+ databaseConnector.closePreparedStatement(preparedDeleteCompanyByFigiStatement);
+ }
+ }
+
+ /**
+ * Clear the collection.
+ */
+ public void clearCollection() {
+ Collection companyCollection = getCollection().values();
+ for (Company company : companyCollection) {
+ deleteCompanyByFigi(company.getFigi());
+ }
+ }
+
+}
diff --git a/src/main/java/Main.java b/src/main/java/Main.java
index 2fdfdfb..cde1ccd 100644
--- a/src/main/java/Main.java
+++ b/src/main/java/Main.java
@@ -1,6 +1,8 @@
import Commands.*;
import Connection.Connector;
import Data.*;
+import Database.DatabaseConnector;
+import Database.DatabaseManager;
import Exceptions.AccountNotFoundException;
import Exceptions.CommandException;
import Proccesor.DataStreamProcessor;
@@ -19,11 +21,21 @@ public class Main {
public static void main(String[] args) {
+ DatabaseConnector databaseConnector = new DatabaseConnector();
+ databaseConnector.connect();
+ DatabaseManager databaseManager = new DatabaseManager(databaseConnector);
+ Company c = new Company("asd", 1, 1, 1, 1);
+ databaseManager.insertCompany(c);
+
try (Scanner userScanner = new Scanner(System.in)) {
InvestApi api = initializeApi(userScanner);
String accountId = chooseAccount(api, userScanner);
+ //DatabaseConnector databaseConnector = new DatabaseConnector();
+ //databaseConnector.connect();
+ //DatabaseManager databaseManager = new DatabaseManager(databaseConnector);
+
CompanyCollection companyCollection = new CompanyCollection();
Connector connector = new Connector(api, companyCollection, accountId);
Trader trader = new Trader(connector, api);
@@ -107,7 +119,7 @@ private static String chooseAccount(InvestApi api, Scanner userScanner) {
userInput = Integer.parseInt(userScanner.nextLine());
if (userInput < 1 || userInput > tinkoffAccounts.size()) throw new IllegalArgumentException("Invalid number of account, try again");
accountId = tinkoffAccounts.get(userInput - 1).getId();
- if (accountId == "") throw new AccountNotFoundException("Accounts not found");
+ if (accountId.equals("")) throw new AccountNotFoundException("Accounts not found");
break;
} catch (IllegalArgumentException exception) {
Console.printError(exception.getMessage());
diff --git a/src/main/java/Proccesor/DataStreamProcessor.java b/src/main/java/Proccesor/DataStreamProcessor.java
index 3d1743d..ab46890 100644
--- a/src/main/java/Proccesor/DataStreamProcessor.java
+++ b/src/main/java/Proccesor/DataStreamProcessor.java
@@ -56,11 +56,9 @@ public void process(MarketDataResponse marketDataResponse) {
, i.getCalcByIndex(index).calculateIndex(curCandleCompany, curCandle));
}
-
+ System.out.println("Company "+ curCandleCompany.getFigi() + " RSI is: " + curCandleCompany.getIndexByType(IndexType.RSI).getValue());
trader.trade(curCandleCompany, curCandle,
Solver.solution(curCandleCompany)); //solver calculates probability to buy/sell based on indexes
-
-
}
@@ -82,7 +80,7 @@ private boolean checkIfNewCandleForIndex(IndexType type, MarketDataResponse mark
.getTimeOfLastEl() != marketDataResponse.getCandle().getTime().getSeconds();
case NVI:
case PVI:
- return false; //todo: ,
+ return false;
default:
return false;
}
diff --git a/src/main/java/Proccesor/Solver.java b/src/main/java/Proccesor/Solver.java
index abeb62a..15953e4 100644
--- a/src/main/java/Proccesor/Solver.java
+++ b/src/main/java/Proccesor/Solver.java
@@ -16,7 +16,7 @@ public class Solver {
* '+' is to buy and value is probability to do this thing
*/
public static double solution(Company company){
- return RSIOnlyVersion(company); //todo: algorithm for combining indexes. Now algorithm is only for RSI
+ return RSIOnlyVersion(company);
}
@@ -28,8 +28,8 @@ public static double solution(Company company){
private static double RSIOnlyVersion(Company company){
// Up and down limit of rsi, intersection of them are signal fo buying/selling. Standart is 70 up 30 down
// We change it to give bot to trade frequently, but accuracy is bad
- double high = 60;
- double low = 40;
+ double high = 65;
+ double low = 35;
double rsi = company.getIndexByType(IndexType.RSI).getValue();
if(rsi > high){
return (rsi - high) / (100 - high); // if rsi = 100 probability 100%, if 70 probability 0%
diff --git a/src/main/java/Proccesor/Trader.java b/src/main/java/Proccesor/Trader.java
index 4d3aeab..9fbce4f 100644
--- a/src/main/java/Proccesor/Trader.java
+++ b/src/main/java/Proccesor/Trader.java
@@ -39,8 +39,8 @@ public Trader(Connector connector, InvestApi api){
* @throws CompanyNotFoundException - problems with programm storage
*/
public void trade(Company company, Candle candle, double probability) throws NotEnoughMoneyToTradeException, CompanyNotFoundException {
- if(probability > 0) buyLots( probability, candle, company);
- if(probability < 0) sellLots((-1) * probability, candle, company);
+ if(probability > 0) buyLots(Math.abs(probability), candle, company);
+ if(probability < 0) sellLots(Math.abs(probability), candle, company);
}
@@ -125,8 +125,8 @@ public void sellLots(double probability, Candle candle, Company company) throws
// if you have only 1 lot, sell it. If, lots calculates with probability
- if(d.getLotNumber() == 1) System.out.println(1); tradeStream.sellStock(1, candle.getClose(), company.getFigi(), d);
- if (d.getLotNumber() > 1){
+ if(d.getLotNumber() == 1) tradeStream.sellStock(1, candle.getClose(), company.getFigi(), d);
+ if ((d.getLotNumber() * probability) >= 1){
tradeStream.sellStock(
(long)(d.getLotNumber() * probability),
candle.getClose(),
@@ -135,7 +135,6 @@ public void sellLots(double probability, Candle candle, Company company) throws
);
}
-
}
}
}
diff --git a/src/main/resources/companies.db b/src/main/resources/companies.db
new file mode 100644
index 0000000..75288e4
Binary files /dev/null and b/src/main/resources/companies.db differ