Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 #1

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

V2 #1

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
# tinvest4s

![Scala CI](https://github.com/a-khakimov/tinvest4s/workflows/Scala%20CI/badge.svg?branch=main)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=a-khakimov_tinvest4s&metric=ncloc)](https://sonarcloud.io/dashboard?id=a-khakimov_tinvest4s)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=a-khakimov_tinvest4s&metric=coverage)](https://sonarcloud.io/dashboard?id=a-khakimov_tinvest4s)
[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=a-khakimov_tinvest4s&metric=sqale_index)](https://sonarcloud.io/dashboard?id=a-khakimov_tinvest4s)

# tinvest4s
<img align="right" src="images/tinvest4s.logo.ico"/>

Библиотека предназначена для взаимодействия с [ОpenAPI Тинькофф Инвестиций](https://tinkoffcreditsystems.github.io/invest-openapi/).

## Начало работы

Для работы с библиотекой потребуется изучить [документацию](https://tinkoffcreditsystems.github.io/invest-openapi/) на ОpenAPI и получить в [личном кабинете](https://www.tinkoff.ru/invest/) токен для авторизации.

## Список реализованных методов

| Сделано? | Метод | |
| :------: |:-----------------------|--------|
| [x] | /portfolio | |
| [ ] | /portfolio/currencies | |
| [x] | /orders/limit-order | |
| [x] | /orders/market-order | |
| [ ] | /orders/cancel | |

## Подключение библиотеки к проекту

В данный момент возможен вариант подключения библиотеки в качестве внешнего проекта.
Expand Down
71 changes: 44 additions & 27 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
name := "tinvest4s"
import Dependencies.{Libraries, _}

version := "0.1"
ThisBuild / scalaVersion := "2.13.4"
ThisBuild / version := "0.1"
ThisBuild / organization := "dev.github.ainr"
ThisBuild / organizationName := "ainr"
ThisBuild / name := "tinvest4s"

scalaVersion := "2.13.4"
lazy val root = (project in file("."))
.settings(
name := "tinvest4s"
)
.aggregate(core, tests)

lazy val circeVersion = "0.13.0"
lazy val http4sVersion = "0.21.7"

libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "3.2.0",
"org.typelevel" %% "cats-core" % "2.1.1",
"org.typelevel" %% "cats-effect" % "2.1.4",
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-blaze-client" % http4sVersion,
"org.http4s" %% "http4s-jdk-http-client" % "0.3.1",
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-literal" % circeVersion,
"io.circe" %% "circe-generic-extras" % circeVersion,
)

sonarProperties := Sonar.properties

scalacOptions ++= Seq(
"-Xfatal-warnings",
"-deprecation"
)
lazy val tests = (project in file("modules/tests"))
.settings(
name := "tinvest4s-test-suite",
libraryDependencies ++= Seq(
Libraries.scalatest,
Libraries.scalamock
)
)
.dependsOn(core)

lazy val core = (project in file("modules/core"))
.settings(
name := "tinvest4s",
sonarProperties := Sonar.properties,
scalacOptions ++= Seq(
"-Xfatal-warnings",
"-deprecation"
),
libraryDependencies ++= Seq(
Libraries.circeCore,
Libraries.circeGeneric,
Libraries.circeGenericExtras,
Libraries.circeLiteral,
Libraries.circeParser,
Libraries.newtype,
Libraries.`sttp-backend-zio`,
Libraries.`sttp-client-core`,
Libraries.`sttp-client3-circe`,
Libraries.catsCore,
Libraries.zio_interop_cats,
Libraries.cats_effect,
Libraries.`async-http-client-backend-cats-ce2`
)
)
Binary file added images/tinvest4s.logo.ico
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package github.ainr.tinvest4s.v1.config

object access {
type Token = String
case class InvestAccessConfig(
token: Token,
isSandbox: Boolean = true
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package github.ainr.tinvest4s.v1.domain

/**
* @param trackingId
* @param status
* @param payload
*/
case class InvestApiError(trackingId: String, status: String, payload: InvestApiErrorPayload)
case class InvestApiErrorPayload(message: Option[String], code: Option[String])

case class EmptyPayload()
case class EmptyResponse(trackingId: String, status: String, payload: EmptyPayload)
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package github.ainr.tinvest4s.models
package github.ainr.tinvest4s.v1.domain

import github.ainr.tinvest4s.models.CandleResolution.CandleResolution
import github.ainr.tinvest4s.models.FIGI.FIGI
import github.ainr.tinvest4s.models.TradeStatus.TradeStatus
import github.ainr.tinvest4s.v1.domain.TradeStatus.TradeStatus
import github.ainr.tinvest4s.v1.domain.schemas.{CandleResolution, FIGI, Price, TrackingId}


case class MarketInstrumentListResponse(trackingId: String, status: String, payload: MarketInstrumentList)
Expand Down Expand Up @@ -76,15 +75,15 @@ case class Orderbook(figi: FIGI,
* @param price
* @param quantity
*/
case class OrderResponse(price: Double, quantity: Int)
case class OrderResponse(price: Price, quantity: Int)

/**
*
* @param trackingId
* @param status
* @param payload
*/
case class CandlesResponse(trackingId: String, status: String, payload: Candles)
case class CandlesResponse(trackingId: TrackingId, status: String, payload: Candles)

/**
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package github.ainr.tinvest4s.models
package github.ainr.tinvest4s.v1.domain

/**
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package github.ainr.tinvest4s.v1.domain

/* TODO: Корректно структурировать это все */
object schemas {
type FIGI = String
type Price = Double
type TrackingId = String
type OrderId = String
type BrokerAccountId = String
type InstrumentType = String
type Balance = Double
type Lots = Int;

sealed trait Response

case class MoneyAmount(currency: String, value: Double)

type Currency = String
object Currency {
val RUB: Currency = "RUB"
val USD: Currency = "USD"
val EUR: Currency = "EUR"
val GBP: Currency = "GBP"
val HKD: Currency = "HKD"
val CHF: Currency = "CHF"
val JPY: Currency = "JPY"
val CNY: Currency = "CNY"
val TRY: Currency = "TRY"
val UnknownCurrency: Currency = "UnknownCurrency"
}

type Operation = String
object Operation {
val Buy: Operation = "Buy"
val Sell: Operation = "Sell"
}

type OrderStatus = String
object OrderStatus {
val New: OrderStatus = "New"
val PartiallyFill: OrderStatus = "PartiallyFill"
val Fill: OrderStatus = "Fill"
val Cancelled: OrderStatus = "Cancelled"
val Replaced: OrderStatus = "Replaced"
val PendingCancel: OrderStatus = "PendingCancel"
val Rejected: OrderStatus = "Rejected"
val PendingReplace: OrderStatus = "PendingReplace"
val PendingNew: OrderStatus = "PendingNew"
}

type CandleResolution = String
object CandleResolution {
val `1min` : CandleResolution = "1min"
val `2min` : CandleResolution = "2min"
val `3min` : CandleResolution = "3min"
val `5min` : CandleResolution = "5min"
val `10min`: CandleResolution = "10min"
val `15min`: CandleResolution = "15min"
val `30min`: CandleResolution = "30min"
val hour : CandleResolution = "hour"
val day : CandleResolution = "day"
val week : CandleResolution = "week"
val month : CandleResolution = "month"
}

case class PortfolioResponse(
trackingId: TrackingId,
payload: Portfolio,
status: String
) extends Response

case class Portfolio(positions: Seq[PortfolioPosition])

case class PortfolioPosition(
figi: FIGI,
ticker: Option[String],
isin: Option[String],
instrumentType: InstrumentType,
balance: Balance,
blocked: Option[Double],
expectedYield: Option[MoneyAmount],
lots: Lots,
averagePositionPrice: Option[MoneyAmount],
averagePositionPriceNoNkd: Option[MoneyAmount],
name: String
)

case class MarketOrderRequest(lots: Lots, operation: Operation)
case class LimitOrderRequest(lots: Lots, operation: Operation, price: Price)
case class OrderResponse(
trackingId: String,
status: String,
payload: PlacedOrder
) extends Response

case class PlacedOrder(
orderId: OrderId,
operation: Operation,
status: OrderStatus,
rejectReason: Option[String],
message: Option[String],
requestedLots: Lots,
executedLots: Lots,
commission: Option[MoneyAmount]
)

case class Order(
orderId: OrderId,
figi: FIGI,
operation: Operation,
status: OrderStatus,
requestedLots: Lots,
executedLots: Lots,
price: Price
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package github.ainr.tinvest4s.v1.examples

import cats.Monad
import cats.effect.{Concurrent, ContextShift, ExitCode, IO, IOApp, Resource, Sync}
import github.ainr.tinvest4s.v1.config.access.InvestAccessConfig
import github.ainr.tinvest4s.v1.domain.schemas.Operation
import github.ainr.tinvest4s.v1.http.client.InvestApiClient
import github.ainr.tinvest4s.v1.http.client.interpreters.InvestApiSttpClient
import github.ainr.tinvest4s.v1.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError
import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend

object CatsBackendExample extends IOApp {

override def run(args: List[String]): IO[ExitCode] = {
createClient[IO]()
.use { investApi =>
for {
portfolio <- investApi.portfolio()
_ <- IO.delay(println(portfolio))
limitOrderResult <- investApi.limitOrder("BBG005HLSZ23", 1, Operation.Buy, 10)
_ <- IO.delay(println(limitOrderResult))
marketOrderResult <- investApi.marketOrder("BBG005HLSZ23", 1, Operation.Sell)
_ <- IO.delay(println(marketOrderResult))
} yield ExitCode.Success
}
}

def createClient[F[_] : Sync : Monad : Concurrent : ContextShift](): Resource[F, InvestApiClient[F]] = {
val config = InvestAccessConfig(token = "t.kkxK5DlAIBodw5moQBIDF1zKSMD-Ov4Kfr5hrBSrTRaxOcRTaeSVKYIdiZXsbSuakLyq9fUK0NUe672oItp6xA")

def errorHandler(error: InvestApiResponseError): Unit = {
println(error)
}

AsyncHttpClientCatsBackend.resource().map {
backend => new InvestApiSttpClient[F](config, backend)(errorHandler)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package github.ainr.tinvest4s.v1.http.client

import github.ainr.tinvest4s.v1.domain.schemas.{BrokerAccountId, FIGI, Lots, Operation, OrderResponse, PortfolioResponse, Price}

trait InvestApiClient[F[_]] {

def portfolio(brokerAccountId: Option[BrokerAccountId] = None): F[Option[PortfolioResponse]]

def limitOrder(
figi: FIGI,
lots: Lots,
operation: Operation,
price: Price,
brokerAccountId: Option[BrokerAccountId] = None
): F[Option[OrderResponse]]

def marketOrder(
figi: FIGI,
lots: Lots,
operation: Operation,
brokerAccountId: Option[BrokerAccountId] = None
): F[Option[OrderResponse]]
}
Loading