From 81360967cedd4150a45ffc11ad3e5e1cc31c2f17 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Tue, 16 Mar 2021 08:33:57 +0500 Subject: [PATCH 01/10] restructurisation --- build.sbt | 67 ++++++++++++------- .../tinvest4s/models/CandleResolution.scala | 0 .../ainr/tinvest4s/models/Currency.scala | 0 .../github/ainr/tinvest4s/models/Error.scala | 0 .../github/ainr/tinvest4s/models/FIGI.scala | 0 .../github/ainr/tinvest4s/models/Market.scala | 0 .../ainr/tinvest4s/models/Operation.scala | 0 .../ainr/tinvest4s/models/OrderStatus.scala | 0 .../github/ainr/tinvest4s/models/Orders.scala | 0 .../ainr/tinvest4s/models/Portfolio.scala | 0 .../ainr/tinvest4s/models/TradeStatus.scala | 0 .../tinvest4s/rest/client/TInvestApi.scala | 0 .../rest/client/TInvestApiHttp4s.scala | 0 .../websocket/client/TInvestWSApi.scala | 0 .../client/TInvestWSAuthorization.scala | 0 .../websocket/request/CandleRequest.scala | 0 .../request/InstrumentInfoRequest.scala | 0 .../websocket/request/OrderBookRequest.scala | 0 .../websocket/request/TInvestWSRequest.scala | 0 .../websocket/response/CandleResponse.scala | 0 .../response/InstrumentInfoResponse.scala | 0 .../response/OrderBookResponse.scala | 0 .../response/TInvestWSResponse.scala | 0 .../websocket/response/WrongResponse.scala | 0 {src => modules}/test/scala/TestTest.scala | 0 project/Dependencies.scala | 35 ++++++++++ project/build.properties | 1 + 27 files changed, 79 insertions(+), 24 deletions(-) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/Currency.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/Error.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/FIGI.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/Market.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/Operation.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/Orders.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/Portfolio.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala (100%) rename {src => modules/core/src}/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala (100%) rename {src => modules}/test/scala/TestTest.scala (100%) create mode 100644 project/Dependencies.scala create mode 100644 project/build.properties diff --git a/build.sbt b/build.sbt index 0d1045a..ad88e7c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,31 +1,50 @@ -name := "tinvest4s" +import Dependencies._ -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, -) +lazy val tests = (project in file("modules/tests")) + .configs(IntegrationTest) + .settings( + name := "tinvest4s-test-suite", + scalacOptions ++= List("-Ymacro-annotations", "-Yrangepos", "-Wconf:cat=unused:info"), + testFrameworks += new TestFramework("weaver.framework.CatsEffect"), + Defaults.itSettings + ) + .dependsOn(core) -sonarProperties := Sonar.properties +lazy val core = (project in file("modules/core")) + .settings( + name := "tinvest4s", + sonarProperties := Sonar.properties, + scalacOptions ++= Seq( + "-Xfatal-warnings", + "-deprecation" + ), + libraryDependencies ++= Seq( + Libraries.catsCore, + Libraries.catsEffect, + Libraries.circeCore, + Libraries.circeGeneric, + Libraries.circeGenericExtras, + Libraries.circeLiteral, + Libraries.circeParser, + Libraries.http4sBlazeClient, + Libraries.http4sCirce, + Libraries.http4sDsl, + Libraries.http4sJdkHttpClient, + Libraries.scalatest + ) + ) -scalacOptions ++= Seq( - "-Xfatal-warnings", - "-deprecation" -) diff --git a/src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/Currency.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Currency.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/Currency.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/Currency.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/Error.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Error.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/Error.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/Error.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/FIGI.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/FIGI.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/FIGI.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/FIGI.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/Market.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Market.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/Market.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/Market.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/Operation.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Operation.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/Operation.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/Operation.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/Orders.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Orders.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/Orders.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/Orders.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/Portfolio.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Portfolio.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/Portfolio.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/Portfolio.scala diff --git a/src/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala diff --git a/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala diff --git a/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala diff --git a/src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala similarity index 100% rename from src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala diff --git a/src/test/scala/TestTest.scala b/modules/test/scala/TestTest.scala similarity index 100% rename from src/test/scala/TestTest.scala rename to modules/test/scala/TestTest.scala diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 0000000..c36599b --- /dev/null +++ b/project/Dependencies.scala @@ -0,0 +1,35 @@ +import sbt._ + +object Dependencies { + + object V { + val http4s = "0.21.7" + val circe = "0.13.0" + val scalatest = "3.2.0" + val cats = "2.1.1" + val http4sJdkHttpClient = "0.3.1" + } + + object Libraries { + + val scalatest = "org.scalatest" %% "scalatest" % V.scalatest + + val catsCore = "org.typelevel" %% "cats-core" % V.cats + val catsEffect = "org.typelevel" %% "cats-effect" % V.cats + + val circeCore = "io.circe" %% "circe-core" % V.circe + val circeParser = "io.circe" %% "circe-parser" % V.circe + val circeGeneric = "io.circe" %% "circe-generic" % V.circe + val circeLiteral = "io.circe" %% "circe-literal" % V.circe + val circeGenericExtras = "io.circe" %% "circe-generic-extras" % V.circe + + val http4sDsl = "org.http4s" %% "http4s-dsl" % V.http4s + val http4sCirce = "org.http4s" %% "http4s-circe" % V.http4s + val http4sBlazeClient = "org.http4s" %% "http4s-blaze-client" % V.http4s + val http4sJdkHttpClient = "org.http4s" %% "http4s-jdk-http-client" % V.http4sJdkHttpClient + } + + object CompilerPlugin { + + } +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..0837f7a --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.13 From fe049fcd4e6584f312c074d6b7e09a45c3d90038 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Thu, 18 Mar 2021 08:47:04 +0500 Subject: [PATCH 02/10] wip --- build.sbt | 13 +- .../tinvest4s/{models => domain}/Error.scala | 4 +- .../tinvest4s/{models => domain}/Market.scala | 11 +- .../tinvest4s/{models => domain}/Orders.scala | 18 +- .../{models => domain}/Portfolio.scala | 6 +- .../{models => domain}/TradeStatus.scala | 2 +- .../ainr/tinvest4s/domain/schemas.scala | 57 ++++++ .../client/TinkoffInvestClient.scala} | 56 +++--- .../TinkoffInvestClientImpl.scala} | 83 +++++---- .../github/ainr/tinvest4s/http/json.scala | 25 +++ .../tinvest4s/models/CandleResolution.scala | 19 -- .../ainr/tinvest4s/models/Currency.scala | 17 -- .../github/ainr/tinvest4s/models/FIGI.scala | 5 - .../ainr/tinvest4s/models/Operation.scala | 10 -- .../ainr/tinvest4s/models/OrderStatus.scala | 17 -- .../websocket/client/TInvestWSApi.scala | 166 ------------------ .../client/TInvestWSAuthorization.scala | 15 -- .../websocket/request/CandleRequest.scala | 16 -- .../request/InstrumentInfoRequest.scala | 14 -- .../websocket/request/OrderBookRequest.scala | 15 -- .../websocket/request/TInvestWSRequest.scala | 6 - .../websocket/response/CandleResponse.scala | 34 ---- .../response/InstrumentInfoResponse.scala | 32 ---- .../response/OrderBookResponse.scala | 26 --- .../response/TInvestWSResponse.scala | 6 - .../websocket/response/WrongResponse.scala | 9 - .../src/main}/scala/TestTest.scala | 0 project/Dependencies.scala | 6 +- 28 files changed, 178 insertions(+), 510 deletions(-) rename modules/core/src/main/scala/github/ainr/tinvest4s/{models => domain}/Error.scala (63%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{models => domain}/Market.scala (88%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{models => domain}/Orders.scala (80%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{models => domain}/Portfolio.scala (79%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{models => domain}/TradeStatus.scala (80%) create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala rename modules/core/src/main/scala/github/ainr/tinvest4s/{rest/client/TInvestApi.scala => http/client/TinkoffInvestClient.scala} (51%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{rest/client/TInvestApiHttp4s.scala => http/client/interpreters/TinkoffInvestClientImpl.scala} (58%) create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/models/Currency.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/models/FIGI.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/models/Operation.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala rename modules/{test => tests/src/main}/scala/TestTest.scala (100%) diff --git a/build.sbt b/build.sbt index ad88e7c..c6fbddb 100644 --- a/build.sbt +++ b/build.sbt @@ -12,14 +12,13 @@ lazy val root = (project in file(".")) ) .aggregate(core, tests) - lazy val tests = (project in file("modules/tests")) - .configs(IntegrationTest) .settings( name := "tinvest4s-test-suite", - scalacOptions ++= List("-Ymacro-annotations", "-Yrangepos", "-Wconf:cat=unused:info"), - testFrameworks += new TestFramework("weaver.framework.CatsEffect"), - Defaults.itSettings + libraryDependencies ++= Seq( + Libraries.scalatest, + Libraries.scalamock + ) ) .dependsOn(core) @@ -43,8 +42,6 @@ lazy val core = (project in file("modules/core")) Libraries.http4sCirce, Libraries.http4sDsl, Libraries.http4sJdkHttpClient, - Libraries.scalatest + Libraries.newtype ) ) - - diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Error.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala similarity index 63% rename from modules/core/src/main/scala/github/ainr/tinvest4s/models/Error.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala index 6893aa4..95553a5 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Error.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala @@ -1,4 +1,4 @@ -package github.ainr.tinvest4s.models +package github.ainr.tinvest4s.domain /** * @@ -6,7 +6,7 @@ package github.ainr.tinvest4s.models * @param status * @param payload */ -case class TInvestError(trackingId: String, status: String, payload: Payload) +case class InvestClientError(trackingId: String, status: String, payload: Payload) extends Throwable case class Payload(message: Option[String], code: Option[String]) case class EmptyPayload() diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Market.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Market.scala similarity index 88% rename from modules/core/src/main/scala/github/ainr/tinvest4s/models/Market.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/domain/Market.scala index 4aeffdf..0119e8a 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Market.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Market.scala @@ -1,8 +1,7 @@ -package github.ainr.tinvest4s.models +package github.ainr.tinvest4s.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.domain.TradeStatus.TradeStatus +import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI, Price, TrackingId} case class MarketInstrumentListResponse(trackingId: String, status: String, payload: MarketInstrumentList) @@ -76,7 +75,7 @@ case class Orderbook(figi: FIGI, * @param price * @param quantity */ -case class OrderResponse(price: Double, quantity: Int) +case class OrderResponse(price: Price, quantity: Int) /** * @@ -84,7 +83,7 @@ case class OrderResponse(price: Double, quantity: Int) * @param status * @param payload */ -case class CandlesResponse(trackingId: String, status: String, payload: Candles) +case class CandlesResponse(trackingId: TrackingId, status: String, payload: Candles) /** * diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Orders.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Orders.scala similarity index 80% rename from modules/core/src/main/scala/github/ainr/tinvest4s/models/Orders.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/domain/Orders.scala index c0fd3d9..40bfd14 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Orders.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Orders.scala @@ -1,8 +1,6 @@ -package github.ainr.tinvest4s.models +package github.ainr.tinvest4s.domain -import github.ainr.tinvest4s.models.Currency.Currency -import github.ainr.tinvest4s.models.Operation.Operation -import github.ainr.tinvest4s.models.OrderStatus.OrderStatus +import github.ainr.tinvest4s.domain.schemas._ /** * @@ -17,7 +15,7 @@ case class MarketOrderRequest(lots: Int, operation: Operation) * @param operation * @param price */ -case class LimitOrderRequest(lots: Int, operation: Operation, price: Double) +case class LimitOrderRequest(lots: Int, operation: Operation, price: Price) /** * @@ -58,17 +56,11 @@ case class PlacedOrder(orderId: String, * @param price */ case class Order(orderId: String, - figi: String, + figi: FIGI, operation: Operation, status: OrderStatus, requestedLots: Int, executedLots: Int, - price: Double) + price: Price) -/** - * - * @param currency - * @param value - */ case class MoneyAmount(currency: Currency, value: Double) - diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Portfolio.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Portfolio.scala similarity index 79% rename from modules/core/src/main/scala/github/ainr/tinvest4s/models/Portfolio.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/domain/Portfolio.scala index c566935..b355e4c 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Portfolio.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Portfolio.scala @@ -1,6 +1,6 @@ -package github.ainr.tinvest4s.models +package github.ainr.tinvest4s.domain -import github.ainr.tinvest4s.models.FIGI.FIGI +import github.ainr.tinvest4s.domain.schemas.{FIGI, TrackingId} /** * @@ -8,7 +8,7 @@ import github.ainr.tinvest4s.models.FIGI.FIGI * @param payload * @param status */ -case class PortfolioResponse(trackingId: String, payload: Portfolio, status: String) +case class PortfolioResponse(trackingId: TrackingId, payload: Portfolio, status: String) /** * diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/TradeStatus.scala similarity index 80% rename from modules/core/src/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/domain/TradeStatus.scala index 6a52b9b..2a696cc 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/TradeStatus.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/TradeStatus.scala @@ -1,4 +1,4 @@ -package github.ainr.tinvest4s.models +package github.ainr.tinvest4s.domain /** * diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala new file mode 100644 index 0000000..517ca42 --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala @@ -0,0 +1,57 @@ +package github.ainr.tinvest4s.domain + +object schemas { + + case class FIGI(value: String) + case class Price(value: Double) + + case class Currency(value: String) + object Currency { + val RUB = "RUB" + val USD = "USD" + val EUR = "EUR" + val GBP = "GBP" + val HKD = "HKD" + val CHF = "CHF" + val JPY = "JPY" + val CNY = "CNY" + val TRY = "TRY" + } + + case class Operation(value: String) + object Operation { + val Buy = "Buy" + val Sell = "Sell" + } + + case class OrderStatus(value: String) + object OrderStatus { + val New = "New" + val PartiallyFill = "PartiallyFill" + val Fill = "Fill" + val Cancelled = "Cancelled" + val Replaced = "Replaced" + val PendingCancel = "PendingCancel" + val Rejected = "Rejected" + val PendingReplace = "PendingReplace" + val PendingNew = "PendingNew" + } + + case class CandleResolution(value: String) + object CandleResolution { + val `1min` = "1min" + val `2min` = "2min" + val `3min` = "3min" + val `5min` = "5min" + val `10min` = "10min" + val `15min` = "15min" + val `30min` = "30min" + val hour = "hour" + val day = "day" + val week = "week" + val month = "month" + } + + case class TrackingId(value: String) + +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/TinkoffInvestClient.scala similarity index 51% rename from modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/http/client/TinkoffInvestClient.scala index 0c90c1c..1c7dbc4 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApi.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/TinkoffInvestClient.scala @@ -1,44 +1,44 @@ -package github.ainr.tinvest4s.rest.client - -import github.ainr.tinvest4s.models.CandleResolution.CandleResolution -import github.ainr.tinvest4s.models.FIGI.FIGI -import github.ainr.tinvest4s.models.{CandlesResponse, EmptyResponse, LimitOrderRequest, MarketInstrumentListResponse, MarketOrderRequest, OrderbookResponse, OrdersResponse, PortfolioResponse, TInvestError} +package github.ainr.tinvest4s.http.client +import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI} +import github.ainr.tinvest4s.domain._ /** * Класс для взаимодейсвия с REST OpenApi Тинькофф Инвестиций + * * @see [[https://tinkoffcreditsystems.github.io/invest-openapi/rest/]] * @author [[https://github.com/a-khakimov/]] */ -trait TInvestApi[F[_]] { +trait TinkoffInvestClient[F[_]] { /** * Получить портфель клиента + * * @return PortfolioResponse - Успешный ответ * TInvestError - Ошибка * */ - def getPortfolio: F[Either[TInvestError, PortfolioResponse]] + def getPortfolio: F[Either[InvestClientError, PortfolioResponse]] /** - * Создать лимитную заявку + * Создать лимитную заявку * - * @param figi FIGI - * @param request Параметры запроса - * @return OrdersResponse - Успешный ответ - * TInvestError - Ошибка - * */ - def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[TInvestError, OrdersResponse]] + * @param figi FIGI + * @param request Параметры запроса + * @return OrdersResponse - Успешный ответ + * TInvestError - Ошибка + * */ + def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[InvestClientError, OrdersResponse]] /** * Создание рыночной заявки * - * @param figi FIGI + * @param figi FIGI * @param request Параметры запроса * @return OrdersResponse - Успешный ответ * TInvestError - Ошибка * */ - def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[TInvestError, OrdersResponse]] + def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[InvestClientError, OrdersResponse]] /** * Отмена заявки @@ -47,41 +47,43 @@ trait TInvestApi[F[_]] { * @return EmptyResponse - Успешный ответ * TInvestError - Ошибка * */ - def cancelOrder(orderId: String): F[Either[TInvestError, EmptyResponse]] + def cancelOrder(orderId: String): F[Either[InvestClientError, EmptyResponse]] /** * Получить список акций * */ - def stocks(): F[Either[TInvestError, MarketInstrumentListResponse]] + def stocks(): F[Either[InvestClientError, MarketInstrumentListResponse]] /** * Получить список облигаций * */ - def bonds(): F[Either[TInvestError, MarketInstrumentListResponse]] + def bonds(): F[Either[InvestClientError, MarketInstrumentListResponse]] /** * Получить список ETF * */ - def etfs(): F[Either[TInvestError, MarketInstrumentListResponse]] + def etfs(): F[Either[InvestClientError, MarketInstrumentListResponse]] /** * Получить список валютных пар * */ - def currencies(): F[Either[TInvestError, MarketInstrumentListResponse]] + def currencies(): F[Either[InvestClientError, MarketInstrumentListResponse]] /** * Получение стакана по FIGI - * @param figi FIGI + * + * @param figi FIGI * @param depth Глубина стакана * */ - def orderbook(figi: FIGI, depth: Int): F[Either[TInvestError, OrderbookResponse]] + def orderbook(figi: FIGI, depth: Int): F[Either[InvestClientError, OrderbookResponse]] /** * Получение исторических свечей по FIGI - * @param figi FIGI + * + * @param figi FIGI * @param interval Интервал свечи - * @param from Начало временного промежутка - * @param to Конец временного промежутка + * @param from Начало временного промежутка + * @param to Конец временного промежутка * */ - def candles(figi: FIGI, interval: CandleResolution, from: String, to: String): F[Either[TInvestError, CandlesResponse]] + def candles(figi: FIGI, interval: CandleResolution, from: String, to: String): F[Either[InvestClientError, CandlesResponse]] } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/TinkoffInvestClientImpl.scala similarity index 58% rename from modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/TinkoffInvestClientImpl.scala index a3f1960..9395303 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/rest/client/TInvestApiHttp4s.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/TinkoffInvestClientImpl.scala @@ -1,17 +1,18 @@ -package github.ainr.tinvest4s.rest.client +package github.ainr.tinvest4s.http.client.interpreters import cats.MonadError import cats.effect.{ConcurrentEffect, ContextShift} import cats.implicits._ -import github.ainr.tinvest4s.models.CandleResolution.CandleResolution -import github.ainr.tinvest4s.models.FIGI.FIGI -import github.ainr.tinvest4s.models.{CandlesResponse, EmptyResponse, LimitOrderRequest, MarketInstrumentListResponse, MarketOrderRequest, OrderbookResponse, OrdersResponse, Payload, PortfolioResponse, TInvestError} +import github.ainr.tinvest4s.domain._ +import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI} +import github.ainr.tinvest4s.http.client.TinkoffInvestClient +import github.ainr.tinvest4s.http.json._ import io.circe.generic.auto._ import org.http4s.Status.Successful -import org.http4s.circe.CirceEntityCodec.{circeEntityDecoder, circeEntityEncoder} +import org.http4s._ +import org.http4s.circe.CirceEntityCodec.circeEntityDecoder import org.http4s.client.Client import org.http4s.headers.{Accept, Authorization} -import org.http4s._ /** * @todo Добавить параметр для установки режима sandbox и биржевой торговли @@ -21,21 +22,20 @@ import org.http4s._ * case error => error.as[TInvestError].map(Left(_).withRight[EmptyResponse]) * }}} */ -class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], +final class TinkoffInvestClientImpl[F[_] : ConcurrentEffect: ContextShift](client: Client[F], token: String, sandbox: Boolean = true)( implicit F: MonadError[F, Throwable] -) extends TInvestApi[F] { +) extends TinkoffInvestClient[F] { private lazy val baseUrl: String = "https://api-invest.tinkoff.ru/openapi/sandbox" private lazy val auth = Authorization(Credentials.Token(AuthScheme.Bearer, token)) private lazy val mediaTypeJson = Accept(MediaType.application.json) private lazy val baseRequest = Request[F]().putHeaders(auth, mediaTypeJson) - - override def getPortfolio: F[Either[TInvestError, PortfolioResponse]] = { + override def getPortfolio: F[Either[InvestClientError, PortfolioResponse]] = { Uri.fromString(s"$baseUrl/portfolio") match { - case Left(e) => Left(TInvestError("", "Wrong url", Payload(Some(e.details), None))).withRight[PortfolioResponse].pure[F] + case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[PortfolioResponse].pure[F] case Right(uri) => { for { result <- client run { @@ -43,17 +43,17 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], .withMethod(Method.GET) .withUri(uri) } use { - case Successful(resp) => resp.as[PortfolioResponse].map(Right(_).withLeft[TInvestError]) - case error => error.as[TInvestError].map(Left(_).withRight[PortfolioResponse]) + case Successful(resp) => resp.as[PortfolioResponse].map(Right(_).withLeft[InvestClientError]) + case error => error.as[InvestClientError].map(Left(_).withRight[PortfolioResponse]) } } yield result } } } - override def cancelOrder(orderId: String): F[Either[TInvestError, EmptyResponse]] = { + override def cancelOrder(orderId: String): F[Either[InvestClientError, EmptyResponse]] = { Uri.fromString(s"$baseUrl/orders/cancel?orderId=$orderId") match { - case Left(e) => Left(TInvestError("", "Wrong url", Payload(Some(e.details), None))).withRight[EmptyResponse].pure[F] + case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[EmptyResponse].pure[F] case Right(uri) => { for { result <- client.run( @@ -61,8 +61,8 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], .withMethod(Method.POST) .withUri(uri) ) use { - case Successful(resp) => resp.as[EmptyResponse].map(Right(_).withLeft[TInvestError]) - case error => error.as[TInvestError].map(Left(_).withRight[EmptyResponse]) + case Successful(resp) => resp.as[EmptyResponse].map(Right(_).withLeft[InvestClientError]) + case error => error.as[InvestClientError].map(Left(_).withRight[EmptyResponse]) } } yield result } @@ -74,9 +74,9 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], * @param figi FIGI * @param request Параметры лимитной заявки * */ - override def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[TInvestError, OrdersResponse]] = { + override def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { Uri.fromString(s"$baseUrl/orders/limit-order?figi=$figi") match { - case Left(e) => Left(TInvestError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrdersResponse].pure[F] + case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrdersResponse].pure[F] case Right(uri) => { for { result <- client.run( @@ -85,17 +85,17 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], .withEntity(request) .withUri(uri) ) use { - case Successful(resp) => resp.as[OrdersResponse].map(Right(_).withLeft[TInvestError]) - case error => error.as[TInvestError].map(Left(_).withRight[OrdersResponse]) + case Successful(resp) => resp.as[OrdersResponse].map(Right(_).withLeft[InvestClientError]) + case error => error.as[InvestClientError].map(Left(_).withRight[OrdersResponse]) } } yield result } } } - override def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[TInvestError, OrdersResponse]] = { + override def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { Uri.fromString(s"$baseUrl/orders/market-order?figi=$figi") match { - case Left(e) => Left(TInvestError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrdersResponse].pure[F] + case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrdersResponse].pure[F] case Right(uri) => { for { result <- client run { @@ -104,17 +104,17 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], .withEntity(request) .withUri(uri) } use { - case Successful(resp) => resp.as[OrdersResponse].map(Right(_).withLeft[TInvestError]) - case error => error.as[TInvestError].map(Left(_).withRight[OrdersResponse]) + case Successful(resp) => resp.as[OrdersResponse].map(Right(_).withLeft[InvestClientError]) + case error => error.as[InvestClientError].map(Left(_).withRight[OrdersResponse]) } } yield result } } } - private def getMarketInstrumentList(instrument: String): F[Either[TInvestError, MarketInstrumentListResponse]] = { + private def getMarketInstrumentList(instrument: String): F[Either[InvestClientError, MarketInstrumentListResponse]] = { Uri.fromString(s"$baseUrl/market/$instrument") match { - case Left(e) => Left(TInvestError("", "Wrong url", Payload(Some(e.details), None))).withRight[MarketInstrumentListResponse].pure[F] + case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[MarketInstrumentListResponse].pure[F] case Right(uri) => { for { result <- client run { @@ -122,33 +122,33 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], .withMethod(Method.GET) .withUri(uri) } use { - case Successful(resp) => resp.as[MarketInstrumentListResponse].map(Right(_).withLeft[TInvestError]) - case error => error.as[TInvestError].map(Left(_).withRight[MarketInstrumentListResponse]) + case Successful(resp) => resp.as[MarketInstrumentListResponse].map(Right(_).withLeft[InvestClientError]) + case error => error.as[InvestClientError].map(Left(_).withRight[MarketInstrumentListResponse]) } } yield result } } } - override def stocks(): F[Either[TInvestError, MarketInstrumentListResponse]] = { + override def stocks(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { getMarketInstrumentList("stocks") } - override def bonds(): F[Either[TInvestError, MarketInstrumentListResponse]] = { + override def bonds(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { getMarketInstrumentList("bonds") } - override def etfs(): F[Either[TInvestError, MarketInstrumentListResponse]] = { + override def etfs(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { getMarketInstrumentList("etfs") } - override def currencies(): F[Either[TInvestError, MarketInstrumentListResponse]] = { + override def currencies(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { getMarketInstrumentList("currencies") } - override def orderbook(figi: FIGI, depth: Int): F[Either[TInvestError, OrderbookResponse]] = { + override def orderbook(figi: FIGI, depth: Int): F[Either[InvestClientError, OrderbookResponse]] = { Uri.fromString(s"$baseUrl/market/orderbook?figi=$figi&depth=$depth") match { - case Left(e) => Left(TInvestError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrderbookResponse].pure[F] + case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrderbookResponse].pure[F] case Right(uri) => { for { result <- client run { @@ -156,8 +156,8 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], .withMethod(Method.GET) .withUri(uri) } use { - case Successful(resp) => resp.as[OrderbookResponse].map(Right(_).withLeft[TInvestError]) - case error => error.as[TInvestError].map(Left(_).withRight[OrderbookResponse]) + case Successful(resp) => resp.as[OrderbookResponse].map(Right(_).withLeft[InvestClientError]) + case error => error.as[InvestClientError].map(Left(_).withRight[OrderbookResponse]) } } yield result } @@ -167,9 +167,9 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], override def candles(figi: FIGI, interval: CandleResolution, from: String, - to: String): F[Either[TInvestError, CandlesResponse]] = { + to: String): F[Either[InvestClientError, CandlesResponse]] = { Uri.fromString(s"$baseUrl/market/candles?figi=$figi&interval=$interval&from=$from&to=$to") match { - case Left(e) => Left(TInvestError("", "Wrong url", Payload(Some(e.details), None))).withRight[CandlesResponse].pure[F] + case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[CandlesResponse].pure[F] case Right(uri) => { for { result <- client run { @@ -177,12 +177,11 @@ class TInvestApiHttp4s[F[_] : ConcurrentEffect: ContextShift](client: Client[F], .withMethod(Method.GET) .withUri(uri) } use { - case Successful(resp) => resp.as[CandlesResponse].map(Right(_).withLeft[TInvestError]) - case error => error.as[TInvestError].map(Left(_).withRight[CandlesResponse]) + case Successful(resp) => resp.as[CandlesResponse].map(Right(_).withLeft[InvestClientError]) + case error => error.as[InvestClientError].map(Left(_).withRight[CandlesResponse]) } } yield result } } } } - diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala new file mode 100644 index 0000000..d7a274d --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala @@ -0,0 +1,25 @@ +package github.ainr.tinvest4s.http + +import cats.Applicative +import github.ainr.tinvest4s.domain.schemas.{FIGI, TrackingId} +import io.circe._ +import io.circe.generic.semiauto._ + +object json extends JsonCodecs { + + import io.circe.Encoder + import org.http4s.EntityEncoder + import org.http4s.circe.jsonEncoderOf + + implicit def deriveEntityEncoder[F[_]: Applicative, A: Encoder]: EntityEncoder[F, A] = jsonEncoderOf[F, A] +} + +private[http] trait JsonCodecs { + + implicit val trackingIdDecoder: Decoder[TrackingId] = deriveDecoder[TrackingId] + implicit val trackingIdEncoder: Encoder[TrackingId] = deriveEncoder[TrackingId] + + implicit val FIGIDecoder: Decoder[FIGI] = deriveDecoder[FIGI] + implicit val FIGIEncoder: Encoder[FIGI] = deriveEncoder[FIGI] + +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala deleted file mode 100644 index 4d3ccfe..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/CandleResolution.scala +++ /dev/null @@ -1,19 +0,0 @@ -package github.ainr.tinvest4s.models - -/** - * - */ -object CandleResolution { - type CandleResolution = String - val `1min` = "1min" - val `2min` = "2min" - val `3min` = "3min" - val `5min` = "5min" - val `10min` = "10min" - val `15min` = "15min" - val `30min` = "30min" - val hour = "hour" - val day = "day" - val week = "week" - val month = "month" -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Currency.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Currency.scala deleted file mode 100644 index c368092..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Currency.scala +++ /dev/null @@ -1,17 +0,0 @@ -package github.ainr.tinvest4s.models - -/** - * - */ -object Currency { - type Currency = String - val RUB = "RUB" - val USD = "USD" - val EUR = "EUR" - val GBP = "GBP" - val HKD = "HKD" - val CHF = "CHF" - val JPY = "JPY" - val CNY = "CNY" - val TRY = "TRY" -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/FIGI.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/FIGI.scala deleted file mode 100644 index 09712db..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/FIGI.scala +++ /dev/null @@ -1,5 +0,0 @@ -package github.ainr.tinvest4s.models - -object FIGI { - type FIGI = String -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Operation.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/Operation.scala deleted file mode 100644 index cd592bc..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/Operation.scala +++ /dev/null @@ -1,10 +0,0 @@ -package github.ainr.tinvest4s.models - -/** - * - */ -object Operation { - type Operation = String - val Buy = "Buy" - val Sell = "Sell" -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala deleted file mode 100644 index 78c752f..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/models/OrderStatus.scala +++ /dev/null @@ -1,17 +0,0 @@ -package github.ainr.tinvest4s.models - -/** - * - */ -object OrderStatus { - type OrderStatus = String - val New = "New" - val PartiallyFill = "PartiallyFill" - val Fill = "Fill" - val Cancelled = "Cancelled" - val Replaced = "Replaced" - val PendingCancel = "PendingCancel" - val Rejected = "Rejected" - val PendingReplace = "PendingReplace" - val PendingNew = "PendingNew" -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala deleted file mode 100644 index c044549..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSApi.scala +++ /dev/null @@ -1,166 +0,0 @@ -package github.ainr.tinvest4s.websocket.client - -import cats.effect.{Concurrent, ConcurrentEffect, ContextShift, Timer} -import cats.syntax.functor._ -import github.ainr.tinvest4s.models.CandleResolution.CandleResolution -import github.ainr.tinvest4s.models.FIGI.FIGI -import github.ainr.tinvest4s.websocket.request.{CandleRequest, InstrumentInfoRequest, OrderBookRequest} -import github.ainr.tinvest4s.websocket.response.{CandleResponse, InstrumentInfoResponse, OrderBookResponse, TInvestWSResponse} -import io.circe.generic.auto._ -import io.circe.parser.decode -import io.circe.syntax.EncoderOps -import io.circe.{Decoder, Encoder} -import org.http4s.client.jdkhttpclient.{WSConnectionHighLevel, WSFrame} - -/** - * Класс для взаимодейсвия со Streaming OpenApi Тинькофф Инвестиций - * @see [[https://tinkoffcreditsystems.github.io/invest-openapi/marketdata/]] - * @author [[https://github.com/a-khakimov/]] - */ -trait TInvestWSApi[F[_]] { - - /** - * Подписка на свечи - * @param figi FIGI - * @param interval Интервал - * */ - def subscribeCandle(figi: FIGI, interval: CandleResolution): F[Unit] - - /** - * Подписка на стакан - * @param figi FIGI - * @param depth Глубина стакана (0 < depth <= 20) - * */ - def subscribeOrderbook(figi: FIGI, depth: Int): F[Unit] - - /** - * Подписка на информацию об инструменте - * @param figi FIGI - * */ - def subscribeInstrumentInfo(figi: FIGI): F[Unit] - - /** - * Отписка от свечей - * @param figi FIGI - * @param interval Интервал - * */ - def unsubscribeCandle(figi: FIGI, interval: CandleResolution): F[Unit] - - /** - * Отписка от стакана - * @param figi FIGI - * @param depth Глубина стакана (0 < depth <= 20) - * */ - def unsubscribeOrderbook(figi: FIGI, depth: Int): F[Unit] - - /** - * Отписка от информации об инструменте - * @param figi FIGI - * */ - def unsubscribeInstrumentInfo(figi: FIGI): F[Unit] - - /** - * Начать выполнение клиента - * */ - def listen(): F[List[TInvestWSResponse]] -} - -/** - * Класс для получения и обработки событий со Streaming сервера. - * Пример использования: - * - * {{{ - * class StreamingEvents[F[_]: Sync] extends TInvestWSHandler[F] { - * override def handle(response: TInvestWSResponse): F[Unit] = { - * response match { - * case CandleResponse(_, _, candle) => ??? - * case OrderBookResponse(_, _, orderBook) => ??? - * case InstrumentInfoResponse(_, _, instrumentInfo) => ??? - * } - * } - * } - * }}} - */ -trait TInvestWSHandler[F[_]] { - /** - * Метод будет вызван по наступлению события - * @param response Параметр будет содержать необходимые данные - * */ - def handle(response: TInvestWSResponse): F[Unit] -} - -class TInvestWSApiHttp4s[F[_] : ConcurrentEffect: Timer: ContextShift : Concurrent] -(connection: WSConnectionHighLevel[F], handler: TInvestWSHandler[F]) - extends TInvestWSApi[F] { - - override def subscribeCandle(figi: FIGI, interval: String): F[Unit] = { - connection.send { - WSFrame.Text { - CandleRequest("candle:subscribe", figi, interval).asJson.noSpaces - } - } - } - - override def subscribeOrderbook(figi: FIGI, depth: Int): F[Unit] = { - connection.send { - WSFrame.Text { - OrderBookRequest("orderbook:subscribe", figi, depth).asJson.noSpaces - } - } - } - - override def subscribeInstrumentInfo(figi: FIGI): F[Unit] = { - connection.send { - WSFrame.Text { - InstrumentInfoRequest("instrument_info:subscribe", figi).asJson.noSpaces - } - } - } - - override def unsubscribeCandle(figi: FIGI, interval: String): F[Unit] = { - connection.send { - WSFrame.Text { - CandleRequest("candle:subscribe", figi, interval).asJson.noSpaces - } - } - } - - override def unsubscribeOrderbook(figi: FIGI, depth: Int): F[Unit] = { - connection.send { - WSFrame.Text { - OrderBookRequest("orderbook:unsubscribe", figi, depth).asJson.noSpaces - } - } - } - - override def unsubscribeInstrumentInfo(figi: FIGI): F[Unit] = { - connection.send { - WSFrame.Text { - InstrumentInfoRequest("instrument_info:unsubscribe", figi).asJson.noSpaces - } - } - } - - def listen(): F[List[TInvestWSResponse]] = { - connection - .receiveStream - .collect { case WSFrame.Text(str, _) => decode[TInvestWSResponse](str) } - .collect { case Right(response) => response } - .evalTap { data => handler.handle(data) } - .compile - .toList - } - - implicit val decodeTInvestWSResponse: Decoder[TInvestWSResponse] = - List[Decoder[TInvestWSResponse]]( - Decoder[CandleResponse].widen, - Decoder[OrderBookResponse].widen, - Decoder[InstrumentInfoResponse].widen, - ).reduceLeft(_ or _) - - implicit val encodeTInvestWSResponse: Encoder[TInvestWSResponse] = Encoder.instance { - case cr @ CandleResponse(_, _, _) => cr.asJson - case or @ OrderBookResponse(_, _, _) => or.asJson - case ir @ InstrumentInfoResponse(_, _, _) => ir.asJson - } -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala deleted file mode 100644 index 317e07f..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/client/TInvestWSAuthorization.scala +++ /dev/null @@ -1,15 +0,0 @@ -package github.ainr.tinvest4s.websocket.client - -import org.http4s.client.jdkhttpclient.WSRequest -import org.http4s.implicits.http4sLiteralsSyntax -import org.http4s.{Header, Headers} - -/** - * - */ -case class TInvestWSAuthorization() { - private val wsUri = uri"wss://api-invest.tinkoff.ru/openapi/md/v1/md-openapi/ws" - def withToken(token: String): WSRequest = { - WSRequest(wsUri, Headers.of(Header("Authorization", s"Bearer $token"))) - } -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala deleted file mode 100644 index 65112a5..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/CandleRequest.scala +++ /dev/null @@ -1,16 +0,0 @@ -package github.ainr.tinvest4s.websocket.request - -import github.ainr.tinvest4s.models.FIGI.FIGI - -/** - * Запрос для подписки/отписки на свечу - * - * @param event Событие подписка(candle:subscribe) или отписка(candle:unsubscribe) - * @param figi FIGI - * @param interval Интервал - * @param request_id ID запроса - * @todo Типизировать параметр event - * @todo Типизировать параметр interval - */ -case class CandleRequest(event: String, figi: FIGI, interval: String, request_id: Option[String] = None) - extends TInvestWSRequest \ No newline at end of file diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala deleted file mode 100644 index a3de2f9..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/InstrumentInfoRequest.scala +++ /dev/null @@ -1,14 +0,0 @@ -package github.ainr.tinvest4s.websocket.request - -import github.ainr.tinvest4s.models.FIGI.FIGI - -/** - * Запрос для подписки/отписки на информацию от инструменте - * - * @param event Событие подписка(candle:subscribe) или отписка(candle:unsubscribe) - * @param figi FIGI - * @param request_id ID запроса - * @todo Типизировать параметр event - */ -case class InstrumentInfoRequest(event: String, figi: FIGI, request_id: Option[String] = None) - extends TInvestWSRequest diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala deleted file mode 100644 index 6121771..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/OrderBookRequest.scala +++ /dev/null @@ -1,15 +0,0 @@ -package github.ainr.tinvest4s.websocket.request - -import github.ainr.tinvest4s.models.FIGI.FIGI - -/** - * Запрос для подписки/отписки на стакан - * - * @param event Событие подписка(candle:subscribe) или отписка(candle:unsubscribe) - * @param figi FIGI - * @param depth Глубина стакана - * @param request_id ID запроса - * @todo Типизировать параметр event - */ -case class OrderBookRequest(event: String, figi: FIGI, depth: Int, request_id: Option[String] = None) - extends TInvestWSRequest diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala deleted file mode 100644 index 2a5bb0a..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/request/TInvestWSRequest.scala +++ /dev/null @@ -1,6 +0,0 @@ -package github.ainr.tinvest4s.websocket.request - -/** - * - */ -trait TInvestWSRequest diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala deleted file mode 100644 index abd9ae8..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/CandleResponse.scala +++ /dev/null @@ -1,34 +0,0 @@ -package github.ainr.tinvest4s.websocket.response - -import github.ainr.tinvest4s.models.FIGI.FIGI - -/** - * Формат ответа от Streaming сервера на подписку на свечи - * - * @param event Название события - * @param time Время в формате RFC3339Nano - * @param payload Структура свечи - */ -case class CandleResponse(event: String, - time: String, - payload: CandlePayload) extends TInvestWSResponse - -/** - * Структура свечи - * @param o Цена открытия - * @param c Цена закрытия - * @param h Наибольшая цена - * @param l Наименьшая цена - * @param v Объем торгов - * @param time RFC3339 - * @param interval Интервал - * @param figi FIGI - */ -case class CandlePayload(o: Double, - c: Double, - h: Double, - l: Double, - v: Double, - time: String, - interval: String, - figi: FIGI) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala deleted file mode 100644 index d009514..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/InstrumentInfoResponse.scala +++ /dev/null @@ -1,32 +0,0 @@ -package github.ainr.tinvest4s.websocket.response - -import github.ainr.tinvest4s.models.FIGI.FIGI - -/** - * Формат ответа от Streaming сервера на подписку на информацию об инструменте - * - * @param event Название события - * @param time Время в формате RFC3339Nano - * @param payload Структура с информацией по инструменту - */ -case class InstrumentInfoResponse(event: String, - time: String, - payload: InstrumentInfoPayload) extends TInvestWSResponse - -/** - * Структура с информацией по инструменту - * @param trade_status Статус торгов - * @param min_price_increment Шаг цены - * @param lot Лот - * @param accrued_interest НКД. Возвращается только для бондов - * @param limit_up Верхняя граница заявки. Возвращается только для RTS инструментов - * @param limit_down Нижняя граница заявки. Возвращается только для RTS инструментов - * @param figi FIGI - */ -case class InstrumentInfoPayload(trade_status: String, - min_price_increment: Double, - lot: Double, - accrued_interest: Option[Double], - limit_up: Option[Double], - limit_down: Option[Double], - figi: FIGI) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala deleted file mode 100644 index ef8813f..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/OrderBookResponse.scala +++ /dev/null @@ -1,26 +0,0 @@ -package github.ainr.tinvest4s.websocket.response - -import github.ainr.tinvest4s.models.FIGI.FIGI - -/** - * Формат ответа от Streaming сервера на подписку на стакан - * - * @param event Название события - * @param time Время в формате RFC3339Nano - * @param payload Структура со стаканом - */ -case class OrderBookResponse(event: String, - time: String, // RFC3339Nano - payload: OrderBookPayload) extends TInvestWSResponse - -/** - * Структура со стаканом - * @param depth Глубина стакана - * @param bids Массив `[Цена, количество]` предложений цены - * @param asks Массив `[Цена, количество]` запросов цены - * @param figi FIGI - */ -case class OrderBookPayload(depth: Int, - bids: Seq[(Double, Double)], - asks: Seq[(Double, Double)], - figi: FIGI) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala deleted file mode 100644 index 6dca7d4..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/TInvestWSResponse.scala +++ /dev/null @@ -1,6 +0,0 @@ -package github.ainr.tinvest4s.websocket.response - -/** - * Ответ от Streaming сервера - */ -trait TInvestWSResponse diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala deleted file mode 100644 index e6c6a46..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/websocket/response/WrongResponse.scala +++ /dev/null @@ -1,9 +0,0 @@ -package github.ainr.tinvest4s.websocket.response - - -case class WrongResponse(event: String, - time: String, // RFC3339Nano - playload: WrongResponsePlayload) - -case class WrongResponsePlayload(error: String, - request_id: Option[String]) diff --git a/modules/test/scala/TestTest.scala b/modules/tests/src/main/scala/TestTest.scala similarity index 100% rename from modules/test/scala/TestTest.scala rename to modules/tests/src/main/scala/TestTest.scala diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c36599b..18d4b51 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,14 +5,18 @@ object Dependencies { object V { val http4s = "0.21.7" val circe = "0.13.0" - val scalatest = "3.2.0" val cats = "2.1.1" val http4sJdkHttpClient = "0.3.1" + val newtype = "0.4.3" + val scalatest = "3.2.0" + val scalamock = "5.1.0" } object Libraries { + val newtype = "io.estatico" %% "newtype" % V.newtype val scalatest = "org.scalatest" %% "scalatest" % V.scalatest + val scalamock = "org.scalamock" %% "scalamock" % V.scalamock val catsCore = "org.typelevel" %% "cats-core" % V.cats val catsEffect = "org.typelevel" %% "cats-effect" % V.cats From b73efdae14d66fc8ac317843779b6aba6dd43caf Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Fri, 19 Mar 2021 23:41:23 +0500 Subject: [PATCH 03/10] remove implementations --- build.sbt | 9 +- ...fInvestClient.scala => InvestClient.scala} | 4 +- .../interpreters/InvestClientImpl.scala | 81 ++++++++ .../TinkoffInvestClientImpl.scala | 187 ------------------ .../github/ainr/tinvest4s/http/json.scala | 6 - project/Dependencies.scala | 3 + 6 files changed, 88 insertions(+), 202 deletions(-) rename modules/core/src/main/scala/github/ainr/tinvest4s/http/client/{TinkoffInvestClient.scala => InvestClient.scala} (96%) create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/TinkoffInvestClientImpl.scala diff --git a/build.sbt b/build.sbt index c6fbddb..c5607d8 100644 --- a/build.sbt +++ b/build.sbt @@ -31,17 +31,12 @@ lazy val core = (project in file("modules/core")) "-deprecation" ), libraryDependencies ++= Seq( - Libraries.catsCore, - Libraries.catsEffect, Libraries.circeCore, Libraries.circeGeneric, Libraries.circeGenericExtras, Libraries.circeLiteral, Libraries.circeParser, - Libraries.http4sBlazeClient, - Libraries.http4sCirce, - Libraries.http4sDsl, - Libraries.http4sJdkHttpClient, - Libraries.newtype + Libraries.newtype, + Libraries.`httpclient-backend-zio` ) ) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/TinkoffInvestClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestClient.scala similarity index 96% rename from modules/core/src/main/scala/github/ainr/tinvest4s/http/client/TinkoffInvestClient.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestClient.scala index 1c7dbc4..88c7364 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/TinkoffInvestClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestClient.scala @@ -10,7 +10,7 @@ import github.ainr.tinvest4s.domain._ * @author [[https://github.com/a-khakimov/]] */ -trait TinkoffInvestClient[F[_]] { +trait InvestClient[F[_]] { /** * Получить портфель клиента @@ -18,7 +18,7 @@ trait TinkoffInvestClient[F[_]] { * @return PortfolioResponse - Успешный ответ * TInvestError - Ошибка * */ - def getPortfolio: F[Either[InvestClientError, PortfolioResponse]] + def portfolio: F[Either[InvestClientError, PortfolioResponse]] /** * Создать лимитную заявку diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala new file mode 100644 index 0000000..31cefd8 --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala @@ -0,0 +1,81 @@ +package github.ainr.tinvest4s.http.client.interpreters + +import cats.MonadError +import github.ainr.tinvest4s.domain._ +import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI} +import github.ainr.tinvest4s.http.client.InvestClient + +/** + * @todo Добавить параметр для установки режима sandbox и биржевой торговли + * @todo Избавиться от повторяющихся фрагментов кода + * {{{ + * case Successful(resp) => resp.as[EmptyResponse].map(Right(_).withLeft[TInvestError]) + * case error => error.as[TInvestError].map(Left(_).withRight[EmptyResponse]) + * }}} + */ +final class InvestClientImpl[F[_]]( + token: String, + sandbox: Boolean = true +) extends InvestClient[F] { + + private lazy val baseUrl: String = "https://api-invest.tinkoff.ru/openapi/sandbox" + + override def portfolio: F[Either[InvestClientError, PortfolioResponse]] = { + //Uri.fromString(s"$baseUrl/portfolio") + ??? + } + + override def cancelOrder(orderId: String): F[Either[InvestClientError, EmptyResponse]] = { + //Uri.fromString(s"$baseUrl/orders/cancel?orderId=$orderId") + ??? + } + + /** + * Создать лимитную заявку + * @param figi FIGI + * @param request Параметры лимитной заявки + * */ + override def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { + //Uri.fromString(s"$baseUrl/orders/limit-order?figi=$figi") + ??? + } + + override def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { + //Uri.fromString(s"$baseUrl/orders/market-order?figi=$figi") + ??? + } + + private def getMarketInstrumentList(instrument: String): F[Either[InvestClientError, MarketInstrumentListResponse]] = { + //Uri.fromString(s"$baseUrl/market/$instrument") + ??? + } + + override def stocks(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { + getMarketInstrumentList("stocks") + } + + override def bonds(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { + getMarketInstrumentList("bonds") + } + + override def etfs(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { + getMarketInstrumentList("etfs") + } + + override def currencies(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { + getMarketInstrumentList("currencies") + } + + override def orderbook(figi: FIGI, depth: Int): F[Either[InvestClientError, OrderbookResponse]] = { + //Uri.fromString(s"$baseUrl/market/orderbook?figi=$figi&depth=$depth") + ??? + } + + override def candles(figi: FIGI, + interval: CandleResolution, + from: String, + to: String): F[Either[InvestClientError, CandlesResponse]] = { + //Uri.fromString(s"$baseUrl/market/candles?figi=$figi&interval=$interval&from=$from&to=$to") + ??? + } +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/TinkoffInvestClientImpl.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/TinkoffInvestClientImpl.scala deleted file mode 100644 index 9395303..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/TinkoffInvestClientImpl.scala +++ /dev/null @@ -1,187 +0,0 @@ -package github.ainr.tinvest4s.http.client.interpreters - -import cats.MonadError -import cats.effect.{ConcurrentEffect, ContextShift} -import cats.implicits._ -import github.ainr.tinvest4s.domain._ -import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI} -import github.ainr.tinvest4s.http.client.TinkoffInvestClient -import github.ainr.tinvest4s.http.json._ -import io.circe.generic.auto._ -import org.http4s.Status.Successful -import org.http4s._ -import org.http4s.circe.CirceEntityCodec.circeEntityDecoder -import org.http4s.client.Client -import org.http4s.headers.{Accept, Authorization} - -/** - * @todo Добавить параметр для установки режима sandbox и биржевой торговли - * @todo Избавиться от повторяющихся фрагментов кода - * {{{ - * case Successful(resp) => resp.as[EmptyResponse].map(Right(_).withLeft[TInvestError]) - * case error => error.as[TInvestError].map(Left(_).withRight[EmptyResponse]) - * }}} - */ -final class TinkoffInvestClientImpl[F[_] : ConcurrentEffect: ContextShift](client: Client[F], - token: String, - sandbox: Boolean = true)( - implicit F: MonadError[F, Throwable] -) extends TinkoffInvestClient[F] { - - private lazy val baseUrl: String = "https://api-invest.tinkoff.ru/openapi/sandbox" - private lazy val auth = Authorization(Credentials.Token(AuthScheme.Bearer, token)) - private lazy val mediaTypeJson = Accept(MediaType.application.json) - private lazy val baseRequest = Request[F]().putHeaders(auth, mediaTypeJson) - - override def getPortfolio: F[Either[InvestClientError, PortfolioResponse]] = { - Uri.fromString(s"$baseUrl/portfolio") match { - case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[PortfolioResponse].pure[F] - case Right(uri) => { - for { - result <- client run { - baseRequest - .withMethod(Method.GET) - .withUri(uri) - } use { - case Successful(resp) => resp.as[PortfolioResponse].map(Right(_).withLeft[InvestClientError]) - case error => error.as[InvestClientError].map(Left(_).withRight[PortfolioResponse]) - } - } yield result - } - } - } - - override def cancelOrder(orderId: String): F[Either[InvestClientError, EmptyResponse]] = { - Uri.fromString(s"$baseUrl/orders/cancel?orderId=$orderId") match { - case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[EmptyResponse].pure[F] - case Right(uri) => { - for { - result <- client.run( - baseRequest - .withMethod(Method.POST) - .withUri(uri) - ) use { - case Successful(resp) => resp.as[EmptyResponse].map(Right(_).withLeft[InvestClientError]) - case error => error.as[InvestClientError].map(Left(_).withRight[EmptyResponse]) - } - } yield result - } - } - } - - /** - * Создать лимитную заявку - * @param figi FIGI - * @param request Параметры лимитной заявки - * */ - override def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { - Uri.fromString(s"$baseUrl/orders/limit-order?figi=$figi") match { - case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrdersResponse].pure[F] - case Right(uri) => { - for { - result <- client.run( - baseRequest - .withMethod(Method.POST) - .withEntity(request) - .withUri(uri) - ) use { - case Successful(resp) => resp.as[OrdersResponse].map(Right(_).withLeft[InvestClientError]) - case error => error.as[InvestClientError].map(Left(_).withRight[OrdersResponse]) - } - } yield result - } - } - } - - override def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { - Uri.fromString(s"$baseUrl/orders/market-order?figi=$figi") match { - case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrdersResponse].pure[F] - case Right(uri) => { - for { - result <- client run { - baseRequest - .withMethod(Method.POST) - .withEntity(request) - .withUri(uri) - } use { - case Successful(resp) => resp.as[OrdersResponse].map(Right(_).withLeft[InvestClientError]) - case error => error.as[InvestClientError].map(Left(_).withRight[OrdersResponse]) - } - } yield result - } - } - } - - private def getMarketInstrumentList(instrument: String): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - Uri.fromString(s"$baseUrl/market/$instrument") match { - case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[MarketInstrumentListResponse].pure[F] - case Right(uri) => { - for { - result <- client run { - baseRequest - .withMethod(Method.GET) - .withUri(uri) - } use { - case Successful(resp) => resp.as[MarketInstrumentListResponse].map(Right(_).withLeft[InvestClientError]) - case error => error.as[InvestClientError].map(Left(_).withRight[MarketInstrumentListResponse]) - } - } yield result - } - } - } - - override def stocks(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("stocks") - } - - override def bonds(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("bonds") - } - - override def etfs(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("etfs") - } - - override def currencies(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("currencies") - } - - override def orderbook(figi: FIGI, depth: Int): F[Either[InvestClientError, OrderbookResponse]] = { - Uri.fromString(s"$baseUrl/market/orderbook?figi=$figi&depth=$depth") match { - case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[OrderbookResponse].pure[F] - case Right(uri) => { - for { - result <- client run { - baseRequest - .withMethod(Method.GET) - .withUri(uri) - } use { - case Successful(resp) => resp.as[OrderbookResponse].map(Right(_).withLeft[InvestClientError]) - case error => error.as[InvestClientError].map(Left(_).withRight[OrderbookResponse]) - } - } yield result - } - } - } - - override def candles(figi: FIGI, - interval: CandleResolution, - from: String, - to: String): F[Either[InvestClientError, CandlesResponse]] = { - Uri.fromString(s"$baseUrl/market/candles?figi=$figi&interval=$interval&from=$from&to=$to") match { - case Left(e) => Left(InvestClientError("", "Wrong url", Payload(Some(e.details), None))).withRight[CandlesResponse].pure[F] - case Right(uri) => { - for { - result <- client run { - baseRequest - .withMethod(Method.GET) - .withUri(uri) - } use { - case Successful(resp) => resp.as[CandlesResponse].map(Right(_).withLeft[InvestClientError]) - case error => error.as[InvestClientError].map(Left(_).withRight[CandlesResponse]) - } - } yield result - } - } - } -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala index d7a274d..7c80abc 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala @@ -1,17 +1,11 @@ package github.ainr.tinvest4s.http -import cats.Applicative import github.ainr.tinvest4s.domain.schemas.{FIGI, TrackingId} import io.circe._ import io.circe.generic.semiauto._ object json extends JsonCodecs { - import io.circe.Encoder - import org.http4s.EntityEncoder - import org.http4s.circe.jsonEncoderOf - - implicit def deriveEntityEncoder[F[_]: Applicative, A: Encoder]: EntityEncoder[F, A] = jsonEncoderOf[F, A] } private[http] trait JsonCodecs { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 18d4b51..7a5fb50 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,10 +10,13 @@ object Dependencies { val newtype = "0.4.3" val scalatest = "3.2.0" val scalamock = "5.1.0" + val `httpclient-backend-zio` = "3.1.9" } object Libraries { + val `httpclient-backend-zio` = "com.softwaremill.sttp.client3" %% "httpclient-backend-zio" % V.`httpclient-backend-zio` + val newtype = "io.estatico" %% "newtype" % V.newtype val scalatest = "org.scalatest" %% "scalatest" % V.scalatest val scalamock = "org.scalamock" %% "scalamock" % V.scalamock From 2c63ce3701a7752c57c1d171d4613518c4a5ed6f Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Mon, 22 Mar 2021 00:18:46 +0500 Subject: [PATCH 04/10] sttp refactoring wip --- build.sbt | 5 +- .../github/ainr/tinvest4s/config/access.scala | 9 ++ .../github/ainr/tinvest4s/domain/Error.scala | 4 +- .../github/ainr/tinvest4s/domain/Orders.scala | 66 ---------- .../ainr/tinvest4s/domain/Portfolio.scala | 45 ------- .../ainr/tinvest4s/domain/schemas.scala | 115 ++++++++++++------ .../http/client/InvestApiClient.scala | 7 ++ .../tinvest4s/http/client/InvestClient.scala | 89 -------------- .../interpreters/InvestApiSttpClient.scala | 45 +++++++ .../interpreters/InvestClientImpl.scala | 81 ------------ .../http/error/DefaultErrorHandler.scala | 22 ++++ .../tinvest4s/http/error/ErrorHandler.scala | 9 ++ .../github/ainr/tinvest4s/http/json.scala | 25 +++- .../github/ainr/tinvest4s/test/TestApp.scala | 64 ++++++++++ .../main/scala/InvestRequestImplSpec.scala | 60 +++++++++ project/Dependencies.scala | 12 +- 16 files changed, 325 insertions(+), 333 deletions(-) create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/config/access.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/domain/Orders.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/domain/Portfolio.scala create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestClient.scala create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala create mode 100644 modules/tests/src/main/scala/InvestRequestImplSpec.scala diff --git a/build.sbt b/build.sbt index c5607d8..b3a6b98 100644 --- a/build.sbt +++ b/build.sbt @@ -37,6 +37,9 @@ lazy val core = (project in file("modules/core")) Libraries.circeLiteral, Libraries.circeParser, Libraries.newtype, - Libraries.`httpclient-backend-zio` + Libraries.`sttp-backend-zio`, + Libraries.`sttp-client-core`, + Libraries.`sttp-client3-circe`, + Libraries.catsCore ) ) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/config/access.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/config/access.scala new file mode 100644 index 0000000..145b097 --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/config/access.scala @@ -0,0 +1,9 @@ +package github.ainr.tinvest4s.config + +object access { + type Token = String + case class InvestAccessConfig( + token: Token, + isSandbox: Boolean = true + ) +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala index 95553a5..80b0aa5 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala @@ -6,8 +6,8 @@ package github.ainr.tinvest4s.domain * @param status * @param payload */ -case class InvestClientError(trackingId: String, status: String, payload: Payload) extends Throwable -case class Payload(message: Option[String], code: Option[String]) +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) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Orders.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Orders.scala deleted file mode 100644 index 40bfd14..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Orders.scala +++ /dev/null @@ -1,66 +0,0 @@ -package github.ainr.tinvest4s.domain - -import github.ainr.tinvest4s.domain.schemas._ - -/** - * - * @param lots - * @param operation - */ -case class MarketOrderRequest(lots: Int, operation: Operation) - -/** - * - * @param lots - * @param operation - * @param price - */ -case class LimitOrderRequest(lots: Int, operation: Operation, price: Price) - -/** - * - * @param trackingId - * @param status - * @param payload - */ -case class OrdersResponse(trackingId: String, status: String, payload: PlacedOrder) - -/** - * - * @param orderId - * @param operation - * @param status - * @param rejectReason - * @param message - * @param requestedLots - * @param executedLots - * @param commission - */ -case class PlacedOrder(orderId: String, - operation: Operation, - status: OrderStatus, - rejectReason: Option[String], - message: Option[String], - requestedLots: Int, - executedLots: Int, - commission: Option[MoneyAmount]) - -/** - * - * @param orderId - * @param figi - * @param operation - * @param status - * @param requestedLots - * @param executedLots - * @param price - */ -case class Order(orderId: String, - figi: FIGI, - operation: Operation, - status: OrderStatus, - requestedLots: Int, - executedLots: Int, - price: Price) - -case class MoneyAmount(currency: Currency, value: Double) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Portfolio.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Portfolio.scala deleted file mode 100644 index b355e4c..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Portfolio.scala +++ /dev/null @@ -1,45 +0,0 @@ -package github.ainr.tinvest4s.domain - -import github.ainr.tinvest4s.domain.schemas.{FIGI, TrackingId} - -/** - * - * @param trackingId - * @param payload - * @param status - */ -case class PortfolioResponse(trackingId: TrackingId, payload: Portfolio, status: String) - -/** - * - * @param positions - */ -case class Portfolio(positions: Seq[PortfolioPosition]) - -/** - * - * @param figi - * @param ticker - * @param isin - * @param instrumentType - * @param balance - * @param blocked - * @param expectedYield - * @param lots - * @param averagePositionPrice - * @param averagePositionPriceNoNkd - * @param name - */ -case class PortfolioPosition( - figi: FIGI, - ticker: Option[String], - isin: Option[String], - instrumentType: String, - balance: Double, - blocked: Option[Double], - expectedYield: Option[MoneyAmount], - lots: Int, - averagePositionPrice: Option[MoneyAmount], - averagePositionPriceNoNkd: Option[MoneyAmount], - name: String -) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala index 517ca42..866e1bd 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala @@ -2,27 +2,34 @@ package github.ainr.tinvest4s.domain object schemas { - case class FIGI(value: String) - case class Price(value: Double) - - case class Currency(value: String) - object Currency { - val RUB = "RUB" - val USD = "USD" - val EUR = "EUR" - val GBP = "GBP" - val HKD = "HKD" - val CHF = "CHF" - val JPY = "JPY" - val CNY = "CNY" - val TRY = "TRY" - } + type FIGI = String + type Price = Double + type TrackingId = String + type OrderId = String + type BrokerAccountId = String + type InstrumentType = String + type Balance = Double + type Lots = Int; - case class Operation(value: String) - object Operation { - val Buy = "Buy" - val Sell = "Sell" - } + trait Response; + + case class MoneyAmount(currency: String, value: Double) + + sealed trait Currency { def name: String } + case class RUB() extends Currency() { override def name: String = "RUB" } + case class USD() extends Currency() { override def name: String = "USD" } + case class EUR() extends Currency() { override def name: String = "EUR" } + case class GBP() extends Currency() { override def name: String = "GBP" } + case class HKD() extends Currency() { override def name: String = "HKD" } + case class CHF() extends Currency() { override def name: String = "CHF" } + case class JPY() extends Currency() { override def name: String = "JPY" } + case class CNY() extends Currency() { override def name: String = "CNY" } + case class TRY() extends Currency() { override def name: String = "TRY" } + case class UnknownCurrency() extends Currency() { override def name: String = "UnknownCurrency" } + + sealed trait Operation { def name: String } + case class Buy() extends Operation { override def name: String = "Buy" } + case class Sell() extends Operation { override def name: String = "Sell" } case class OrderStatus(value: String) object OrderStatus { @@ -37,21 +44,59 @@ object schemas { val PendingNew = "PendingNew" } - case class CandleResolution(value: String) - object CandleResolution { - val `1min` = "1min" - val `2min` = "2min" - val `3min` = "3min" - val `5min` = "5min" - val `10min` = "10min" - val `15min` = "15min" - val `30min` = "30min" - val hour = "hour" - val day = "day" - val week = "week" - val month = "month" - } + trait CandleResolution { def value: String } + case class `1min` () extends CandleResolution { override def value: String = "1min" } + case class `2min` () extends CandleResolution { override def value: String = "2min" } + case class `3min` () extends CandleResolution { override def value: String = "3min" } + case class `5min` () extends CandleResolution { override def value: String = "5min" } + case class `10min`() extends CandleResolution { override def value: String = "10min" } + case class `15min`() extends CandleResolution { override def value: String = "15min" } + case class `30min`() extends CandleResolution { override def value: String = "30min" } + case class hour () extends CandleResolution { override def value: String = "hour" } + case class day () extends CandleResolution { override def value: String = "day" } + case class week () extends CandleResolution { override def value: String = "week" } + case class month () extends CandleResolution { override def value: String = "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 OrdersResponse(trackingId: String, status: String, payload: PlacedOrder) extends Response - case class TrackingId(value: String) + 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 + ) } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala new file mode 100644 index 0000000..006df48 --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala @@ -0,0 +1,7 @@ +package github.ainr.tinvest4s.http.client + +import github.ainr.tinvest4s.domain.schemas.{BrokerAccountId, PortfolioResponse} + +trait InvestApiClient[F[_]] { + def portfolio(brokerAccountId: Option[BrokerAccountId] = None): F[PortfolioResponse] +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestClient.scala deleted file mode 100644 index 88c7364..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestClient.scala +++ /dev/null @@ -1,89 +0,0 @@ -package github.ainr.tinvest4s.http.client - -import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI} -import github.ainr.tinvest4s.domain._ - -/** - * Класс для взаимодейсвия с REST OpenApi Тинькофф Инвестиций - * - * @see [[https://tinkoffcreditsystems.github.io/invest-openapi/rest/]] - * @author [[https://github.com/a-khakimov/]] - */ - -trait InvestClient[F[_]] { - - /** - * Получить портфель клиента - * - * @return PortfolioResponse - Успешный ответ - * TInvestError - Ошибка - * */ - def portfolio: F[Either[InvestClientError, PortfolioResponse]] - - /** - * Создать лимитную заявку - * - * @param figi FIGI - * @param request Параметры запроса - * @return OrdersResponse - Успешный ответ - * TInvestError - Ошибка - * */ - def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[InvestClientError, OrdersResponse]] - - /** - * Создание рыночной заявки - * - * @param figi FIGI - * @param request Параметры запроса - * @return OrdersResponse - Успешный ответ - * TInvestError - Ошибка - * */ - def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[InvestClientError, OrdersResponse]] - - /** - * Отмена заявки - * - * @param orderId ID заявки - * @return EmptyResponse - Успешный ответ - * TInvestError - Ошибка - * */ - def cancelOrder(orderId: String): F[Either[InvestClientError, EmptyResponse]] - - /** - * Получить список акций - * */ - def stocks(): F[Either[InvestClientError, MarketInstrumentListResponse]] - - /** - * Получить список облигаций - * */ - def bonds(): F[Either[InvestClientError, MarketInstrumentListResponse]] - - /** - * Получить список ETF - * */ - def etfs(): F[Either[InvestClientError, MarketInstrumentListResponse]] - - /** - * Получить список валютных пар - * */ - def currencies(): F[Either[InvestClientError, MarketInstrumentListResponse]] - - /** - * Получение стакана по FIGI - * - * @param figi FIGI - * @param depth Глубина стакана - * */ - def orderbook(figi: FIGI, depth: Int): F[Either[InvestClientError, OrderbookResponse]] - - /** - * Получение исторических свечей по FIGI - * - * @param figi FIGI - * @param interval Интервал свечи - * @param from Начало временного промежутка - * @param to Конец временного промежутка - * */ - def candles(figi: FIGI, interval: CandleResolution, from: String, to: String): F[Either[InvestClientError, CandlesResponse]] -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala new file mode 100644 index 0000000..a9e2f0d --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala @@ -0,0 +1,45 @@ +package github.ainr.tinvest4s.http.client.interpreters + +import cats.Monad +import cats.implicits._ +import github.ainr.tinvest4s.config.access.{InvestAccessConfig, Token} +import github.ainr.tinvest4s.domain.InvestApiError +import github.ainr.tinvest4s.domain.schemas.{BrokerAccountId, PortfolioResponse} +import github.ainr.tinvest4s.http.client.InvestApiClient +import github.ainr.tinvest4s.http.error.ErrorHandler +import github.ainr.tinvest4s.http.json._ +import sttp.client3.circe.asJsonEither +import sttp.client3.{SttpBackend, UriContext, basicRequest} + +final class InvestApiSttpClient[F[_]: Monad] +( + config: InvestAccessConfig, + backend: SttpBackend[F, Any], + errorHandler: ErrorHandler[F] +) extends InvestApiClient[F] { + + private lazy val token: Token = config.token + + private lazy val baseUrl: String = { + s"https://api-invest.tinkoff.ru/openapi/${ + if (config.isSandbox) "sandbox" + else "" + }" + } + + override def portfolio(brokerAccountId: Option[BrokerAccountId] = None) = { + val uri = brokerAccountId match { + case Some(id) => uri"$baseUrl/portfolio?brokerAccountId=$id" + case _ => uri"$baseUrl/portfolio" + } + + basicRequest + .get(uri) + .auth.bearer(token) + .response(asJsonEither[InvestApiError, PortfolioResponse]) + .send(backend) + .flatMap { r => + errorHandler.handle[InvestApiError, PortfolioResponse](r.body) + } + } +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala deleted file mode 100644 index 31cefd8..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestClientImpl.scala +++ /dev/null @@ -1,81 +0,0 @@ -package github.ainr.tinvest4s.http.client.interpreters - -import cats.MonadError -import github.ainr.tinvest4s.domain._ -import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI} -import github.ainr.tinvest4s.http.client.InvestClient - -/** - * @todo Добавить параметр для установки режима sandbox и биржевой торговли - * @todo Избавиться от повторяющихся фрагментов кода - * {{{ - * case Successful(resp) => resp.as[EmptyResponse].map(Right(_).withLeft[TInvestError]) - * case error => error.as[TInvestError].map(Left(_).withRight[EmptyResponse]) - * }}} - */ -final class InvestClientImpl[F[_]]( - token: String, - sandbox: Boolean = true -) extends InvestClient[F] { - - private lazy val baseUrl: String = "https://api-invest.tinkoff.ru/openapi/sandbox" - - override def portfolio: F[Either[InvestClientError, PortfolioResponse]] = { - //Uri.fromString(s"$baseUrl/portfolio") - ??? - } - - override def cancelOrder(orderId: String): F[Either[InvestClientError, EmptyResponse]] = { - //Uri.fromString(s"$baseUrl/orders/cancel?orderId=$orderId") - ??? - } - - /** - * Создать лимитную заявку - * @param figi FIGI - * @param request Параметры лимитной заявки - * */ - override def limitOrder(figi: FIGI, request: LimitOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { - //Uri.fromString(s"$baseUrl/orders/limit-order?figi=$figi") - ??? - } - - override def marketOrder(figi: FIGI, request: MarketOrderRequest): F[Either[InvestClientError, OrdersResponse]] = { - //Uri.fromString(s"$baseUrl/orders/market-order?figi=$figi") - ??? - } - - private def getMarketInstrumentList(instrument: String): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - //Uri.fromString(s"$baseUrl/market/$instrument") - ??? - } - - override def stocks(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("stocks") - } - - override def bonds(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("bonds") - } - - override def etfs(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("etfs") - } - - override def currencies(): F[Either[InvestClientError, MarketInstrumentListResponse]] = { - getMarketInstrumentList("currencies") - } - - override def orderbook(figi: FIGI, depth: Int): F[Either[InvestClientError, OrderbookResponse]] = { - //Uri.fromString(s"$baseUrl/market/orderbook?figi=$figi&depth=$depth") - ??? - } - - override def candles(figi: FIGI, - interval: CandleResolution, - from: String, - to: String): F[Either[InvestClientError, CandlesResponse]] = { - //Uri.fromString(s"$baseUrl/market/candles?figi=$figi&interval=$interval&from=$from&to=$to") - ??? - } -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala new file mode 100644 index 0000000..ef55d72 --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala @@ -0,0 +1,22 @@ +package github.ainr.tinvest4s.http.error +import cats.Monad +import cats.implicits._ +import github.ainr.tinvest4s.domain.{InvestApiError, schemas} +import io.circe +import sttp.client3.{DeserializationException, HttpError, ResponseException} + +final class DefaultErrorHandler[F[_]: Monad] extends ErrorHandler[F] { + + override def handle[E, R <: schemas.Response] + ( + response: Either[ResponseException[InvestApiError, circe.Error], R] + ): F[R] = { + response match { + case Right(success) => success.pure[F] + case Left(DeserializationException(body, error)) => ??? // error deserializing spotify response + case Left(HttpError(body, statusCode)) => ??? // http error + case Left(error) => ??? // other error + } + } + +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala new file mode 100644 index 0000000..0481d9d --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala @@ -0,0 +1,9 @@ +package github.ainr.tinvest4s.http.error + +import github.ainr.tinvest4s.domain.InvestApiError +import github.ainr.tinvest4s.domain.schemas.Response +import sttp.client3.ResponseException + +trait ErrorHandler[F[_]] { + def handle[E, R <: Response](response: Either[ResponseException[InvestApiError, io.circe.Error], R]): F[R] +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala index 7c80abc..d290164 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala @@ -1,8 +1,9 @@ package github.ainr.tinvest4s.http -import github.ainr.tinvest4s.domain.schemas.{FIGI, TrackingId} -import io.circe._ +import github.ainr.tinvest4s.domain.{InvestApiError, InvestApiErrorPayload} +import github.ainr.tinvest4s.domain.schemas.{Currency, MoneyAmount, Portfolio, PortfolioPosition, PortfolioResponse, TrackingId} import io.circe.generic.semiauto._ +import io.circe._ object json extends JsonCodecs { @@ -10,10 +11,22 @@ object json extends JsonCodecs { private[http] trait JsonCodecs { - implicit val trackingIdDecoder: Decoder[TrackingId] = deriveDecoder[TrackingId] - implicit val trackingIdEncoder: Encoder[TrackingId] = deriveEncoder[TrackingId] + implicit val currencyAmountDecoder: Decoder[Currency] = deriveDecoder + implicit val currencyAmountEncoder: Encoder[Currency] = deriveEncoder + + implicit val moneyAmountDecoder: Decoder[MoneyAmount] = deriveDecoder + implicit val moneyAmountEncoder: Encoder[MoneyAmount] = deriveEncoder + + implicit val portfolioPositionDecoder: Decoder[PortfolioPosition] = deriveDecoder + implicit val portfolioPositionEncoder: Encoder[PortfolioPosition] = deriveEncoder + + implicit val portfolioDecoder: Decoder[Portfolio] = deriveDecoder + implicit val portfolioEncoder: Encoder[Portfolio] = deriveEncoder + + implicit val portfolioResponseDecoder: Decoder[PortfolioResponse] = deriveDecoder + implicit val portfolioResponseEncoder: Encoder[PortfolioResponse] = deriveEncoder - implicit val FIGIDecoder: Decoder[FIGI] = deriveDecoder[FIGI] - implicit val FIGIEncoder: Encoder[FIGI] = deriveEncoder[FIGI] + implicit val investApiErrorPayloadDecoder: Decoder[InvestApiErrorPayload] = deriveDecoder + implicit val investClientErrorDecoder: Decoder[InvestApiError] = deriveDecoder } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala new file mode 100644 index 0000000..a60a93b --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala @@ -0,0 +1,64 @@ +package github.ainr.tinvest4s.test + +import github.ainr.tinvest4s.config.access.InvestAccessConfig +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient +import github.ainr.tinvest4s.http.error.DefaultErrorHandler +import sttp.client3.circe.asJson +import sttp.client3.httpclient.zio.{HttpClientZioBackend, SttpClient, send} +import sttp.client3.{UriContext, basicRequest} +import zio._ +import zio.console.Console + + +object TestApp extends zio.App { + + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { + + HttpClientZioBackend().flatMap { + backend => { + + val config = InvestAccessConfig(token = "t.kkxK5DlAIBodw5moQBIDF1zKSMD-Ov4Kfr5hrBSrTRaxOcRTaeSVKYIdiZXsbSuakLyq9fUK0NUe672oItp6xA") + val errorHandler = new DefaultErrorHandler() + val client = new InvestApiSttpClient(config, backend, errorHandler) + + val sendWithRetries: ZIO[Console with SttpClient, Throwable, Unit] = for { + _ <- console.putStrLn("hui") + p = client.portfolio() + _ <- console.putStrLn(p.toString) + } yield () + + sendWithRetries + } + }.exitCode + } +} + + +object GetAndParseJsonZioCirce extends App { + + + import io.circe.generic.auto._ + + override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = { + + case class HttpBinResponse(origin: String, headers: Map[String, String], i: Int) + + val request = basicRequest + .get(uri"https://httpbin.org/get") + .response(asJson[HttpBinResponse]) + + // create a description of a program, which requires two dependencies in the environment: + // the SttpClient, and the Console + val sendAndPrint: ZIO[Console with SttpClient, Throwable, Unit] = for { + response <- send(request) + _ <- console.putStrLn(s"Got response code: ${response.code}") + _ <- console.putStrLn(response.body.toString) + } yield () + + // provide an implementation for the SttpClient dependency; other dependencies are + // provided by Zio + sendAndPrint + .provideCustomLayer(HttpClientZioBackend.layer()) + .exitCode + } +} diff --git a/modules/tests/src/main/scala/InvestRequestImplSpec.scala b/modules/tests/src/main/scala/InvestRequestImplSpec.scala new file mode 100644 index 0000000..bcab958 --- /dev/null +++ b/modules/tests/src/main/scala/InvestRequestImplSpec.scala @@ -0,0 +1,60 @@ +import github.ainr.tinvest4s.config.access.InvestAccessConfig +import github.ainr.tinvest4s.domain.schemas.{Portfolio, PortfolioPosition, PortfolioResponse} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.client3.testing.SttpBackendStub + +class InvestRequestImplSpec extends AnyFlatSpec with Matchers { + + val testingBackend = SttpBackendStub + .synchronous + .whenRequestMatches(_ => true) + .thenRespond( + """ + |{ + | "trackingId": "b16ed655d2c0205c", + | "payload": { + | "positions": [ + | { + | "figi": "BBG005HLSZ23", + | "ticker": "FXUS", + | "isin": "IE00BD3QHZ91", + | "instrumentType": "Etf", + | "balance": 155, + | "blocked": 0, + | "lots": 155, + | "name": "FinEx Акции американских компаний" + | } + | ] + | }, + | "status": "Ok" + |} + |""".stripMargin) + + val config = InvestAccessConfig("") + //val request = new InvestRequestImpl(config) + val result = + PortfolioResponse( + "b16ed655d2c0205c", + Portfolio( + List( + PortfolioPosition( + "BBG005HLSZ23", + Some("FXUS"), + Some("IE00BD3QHZ91"), + "Etf", + 155.0, + Some(0.0), + None, + 155, + None,None, + "FinEx Акции американских компаний") + ) + ), + "Ok" + ) + + //"InvestRequestImpl" must "work" in { + //testingBackend.send(request.portfolio).body shouldBe Right(result) + //} +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7a5fb50..d779bc5 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,30 +10,26 @@ object Dependencies { val newtype = "0.4.3" val scalatest = "3.2.0" val scalamock = "5.1.0" - val `httpclient-backend-zio` = "3.1.9" + val sttp = "3.1.9" } object Libraries { - val `httpclient-backend-zio` = "com.softwaremill.sttp.client3" %% "httpclient-backend-zio" % V.`httpclient-backend-zio` + val `sttp-client-core` = "com.softwaremill.sttp.client3" %% "core" % V.sttp + val `sttp-backend-zio` = "com.softwaremill.sttp.client3" %% "httpclient-backend-zio" % V.sttp + val `sttp-client3-circe` = "com.softwaremill.sttp.client3" %% "circe" % V.sttp val newtype = "io.estatico" %% "newtype" % V.newtype val scalatest = "org.scalatest" %% "scalatest" % V.scalatest val scalamock = "org.scalamock" %% "scalamock" % V.scalamock val catsCore = "org.typelevel" %% "cats-core" % V.cats - val catsEffect = "org.typelevel" %% "cats-effect" % V.cats val circeCore = "io.circe" %% "circe-core" % V.circe val circeParser = "io.circe" %% "circe-parser" % V.circe val circeGeneric = "io.circe" %% "circe-generic" % V.circe val circeLiteral = "io.circe" %% "circe-literal" % V.circe val circeGenericExtras = "io.circe" %% "circe-generic-extras" % V.circe - - val http4sDsl = "org.http4s" %% "http4s-dsl" % V.http4s - val http4sCirce = "org.http4s" %% "http4s-circe" % V.http4s - val http4sBlazeClient = "org.http4s" %% "http4s-blaze-client" % V.http4s - val http4sJdkHttpClient = "org.http4s" %% "http4s-jdk-http-client" % V.http4sJdkHttpClient } object CompilerPlugin { From 45ee4d24e6e375db719f4001a9dd79d2572f27f9 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Sun, 16 May 2021 12:36:29 +0500 Subject: [PATCH 05/10] CatsBackendExample --- build.sbt | 7 +- .../ainr/tinvest4s/domain/schemas.scala | 3 +- .../interpreters/InvestApiSttpClient.scala | 4 +- .../http/error/DefaultErrorHandler.scala | 1 + .../tinvest4s/test/CatsBackendExample.scala | 29 +++++++++ .../github/ainr/tinvest4s/test/TestApp.scala | 64 ------------------- .../tinvest4s/test/ZioBackendExample.scala | 39 +++++++++++ project/Dependencies.scala | 5 ++ 8 files changed, 83 insertions(+), 69 deletions(-) create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala diff --git a/build.sbt b/build.sbt index b3a6b98..0b5673c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -import Dependencies._ +import Dependencies.{Libraries, _} ThisBuild / scalaVersion := "2.13.4" ThisBuild / version := "0.1" @@ -40,6 +40,9 @@ lazy val core = (project in file("modules/core")) Libraries.`sttp-backend-zio`, Libraries.`sttp-client-core`, Libraries.`sttp-client3-circe`, - Libraries.catsCore + Libraries.catsCore, + Libraries.zio_interop_cats, + Libraries.cats_effect, + Libraries.`async-http-client-backend-cats-ce2` ) ) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala index 866e1bd..e2c98d0 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala @@ -29,7 +29,8 @@ object schemas { sealed trait Operation { def name: String } case class Buy() extends Operation { override def name: String = "Buy" } - case class Sell() extends Operation { override def name: String = "Sell" } + + case class Sell() extends Operation { override def name: String = "Sell" } case class OrderStatus(value: String) object OrderStatus { diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala index a9e2f0d..c1d24ba 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala @@ -9,7 +9,7 @@ import github.ainr.tinvest4s.http.client.InvestApiClient import github.ainr.tinvest4s.http.error.ErrorHandler import github.ainr.tinvest4s.http.json._ import sttp.client3.circe.asJsonEither -import sttp.client3.{SttpBackend, UriContext, basicRequest} +import sttp.client3.{SttpBackend, _} final class InvestApiSttpClient[F[_]: Monad] ( @@ -27,7 +27,7 @@ final class InvestApiSttpClient[F[_]: Monad] }" } - override def portfolio(brokerAccountId: Option[BrokerAccountId] = None) = { + override def portfolio(brokerAccountId: Option[BrokerAccountId] = None): F[PortfolioResponse] = { val uri = brokerAccountId match { case Some(id) => uri"$baseUrl/portfolio?brokerAccountId=$id" case _ => uri"$baseUrl/portfolio" diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala index ef55d72..e2a72ce 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala @@ -1,4 +1,5 @@ package github.ainr.tinvest4s.http.error + import cats.Monad import cats.implicits._ import github.ainr.tinvest4s.domain.{InvestApiError, schemas} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala new file mode 100644 index 0000000..279d4e9 --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala @@ -0,0 +1,29 @@ +package github.ainr.tinvest4s.test + +import cats.Monad +import cats.effect.{Concurrent, ContextShift, ExitCode, IO, IOApp, Resource, Sync} +import github.ainr.tinvest4s.config.access.InvestAccessConfig +import github.ainr.tinvest4s.http.client.InvestApiClient +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient +import github.ainr.tinvest4s.http.error.DefaultErrorHandler +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)) + } yield ExitCode.Success + } + } + + def createClient[F[_]: Sync: Monad: Concurrent: ContextShift](): Resource[F, InvestApiClient[F]] = { + val config = InvestAccessConfig(token = "t.kkxK5DlAIBodw5moQBIDF1zKSMD-Ov4Kfr5hrBSrTRaxOcRTaeSVKYIdiZXsbSuakLyq9fUK0NUe672oItp6xA") + val errorHandler = new DefaultErrorHandler[F]() + AsyncHttpClientCatsBackend.resource().map { + backend => new InvestApiSttpClient[F](config, backend, errorHandler) + } + } +} \ No newline at end of file diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala deleted file mode 100644 index a60a93b..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/test/TestApp.scala +++ /dev/null @@ -1,64 +0,0 @@ -package github.ainr.tinvest4s.test - -import github.ainr.tinvest4s.config.access.InvestAccessConfig -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient -import github.ainr.tinvest4s.http.error.DefaultErrorHandler -import sttp.client3.circe.asJson -import sttp.client3.httpclient.zio.{HttpClientZioBackend, SttpClient, send} -import sttp.client3.{UriContext, basicRequest} -import zio._ -import zio.console.Console - - -object TestApp extends zio.App { - - override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { - - HttpClientZioBackend().flatMap { - backend => { - - val config = InvestAccessConfig(token = "t.kkxK5DlAIBodw5moQBIDF1zKSMD-Ov4Kfr5hrBSrTRaxOcRTaeSVKYIdiZXsbSuakLyq9fUK0NUe672oItp6xA") - val errorHandler = new DefaultErrorHandler() - val client = new InvestApiSttpClient(config, backend, errorHandler) - - val sendWithRetries: ZIO[Console with SttpClient, Throwable, Unit] = for { - _ <- console.putStrLn("hui") - p = client.portfolio() - _ <- console.putStrLn(p.toString) - } yield () - - sendWithRetries - } - }.exitCode - } -} - - -object GetAndParseJsonZioCirce extends App { - - - import io.circe.generic.auto._ - - override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = { - - case class HttpBinResponse(origin: String, headers: Map[String, String], i: Int) - - val request = basicRequest - .get(uri"https://httpbin.org/get") - .response(asJson[HttpBinResponse]) - - // create a description of a program, which requires two dependencies in the environment: - // the SttpClient, and the Console - val sendAndPrint: ZIO[Console with SttpClient, Throwable, Unit] = for { - response <- send(request) - _ <- console.putStrLn(s"Got response code: ${response.code}") - _ <- console.putStrLn(response.body.toString) - } yield () - - // provide an implementation for the SttpClient dependency; other dependencies are - // provided by Zio - sendAndPrint - .provideCustomLayer(HttpClientZioBackend.layer()) - .exitCode - } -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala new file mode 100644 index 0000000..1c8dddd --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala @@ -0,0 +1,39 @@ +package github.ainr.tinvest4s.test + +import cats.implicits.catsStdShowForString +import github.ainr.tinvest4s.config.access.InvestAccessConfig +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient +import github.ainr.tinvest4s.http.error.DefaultErrorHandler +import sttp.client3.httpclient.zio.HttpClientZioBackend +import zio._ +import zio.interop.catz._ + +// TODO: ??? +/* +object ZioBackendExample extends zio.App { + + val layer = HttpClientZioBackend().flatMap { + backend => + val config = InvestAccessConfig(token = "t.kkxK5DlAIBodw5moQBIDF1zKSMD-Ov4Kfr5hrBSrTRaxOcRTaeSVKYIdiZXsbSuakLyq9fUK0NUe672oItp6xA") + val errorHandler = new DefaultErrorHandler() + val client = new InvestApiSttpClient[Task](config, backend, errorHandler) + + for { + _ <- console.putStrLn("hui") + p = client.portfolio() + _ <- console.putStrLn(p.toString) + } yield () + }.toLayer + + + override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { + def z = ZIO.runtime[zio.ZEnv].provideLayer(layer) + + for { + _ <- z + } yield () + + ??? + } +} + */ \ No newline at end of file diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d779bc5..4ba5fff 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -11,6 +11,8 @@ object Dependencies { val scalatest = "3.2.0" val scalamock = "5.1.0" val sttp = "3.1.9" + val zio_interop_cats = "3.0.2.0" + val `async-http-client-backend-cats-ce2` = "3.3.3" } object Libraries { @@ -18,6 +20,7 @@ object Dependencies { val `sttp-client-core` = "com.softwaremill.sttp.client3" %% "core" % V.sttp val `sttp-backend-zio` = "com.softwaremill.sttp.client3" %% "httpclient-backend-zio" % V.sttp val `sttp-client3-circe` = "com.softwaremill.sttp.client3" %% "circe" % V.sttp + val `async-http-client-backend-cats-ce2` = "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats-ce2" % V.`async-http-client-backend-cats-ce2` // for cats-effect 2.x val newtype = "io.estatico" %% "newtype" % V.newtype val scalatest = "org.scalatest" %% "scalatest" % V.scalatest @@ -30,6 +33,8 @@ object Dependencies { val circeGeneric = "io.circe" %% "circe-generic" % V.circe val circeLiteral = "io.circe" %% "circe-literal" % V.circe val circeGenericExtras = "io.circe" %% "circe-generic-extras" % V.circe + val zio_interop_cats = "dev.zio" %% "zio-interop-cats" % V.zio_interop_cats + val cats_effect = "org.typelevel" %% "cats-core" % V.cats } object CompilerPlugin { From 93ccab0e0f103f2ea0a3ba83a28cf8e807153768 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Sun, 16 May 2021 20:32:35 +0500 Subject: [PATCH 06/10] Portfolio method and test --- .../github/ainr/tinvest4s/domain/Error.scala | 1 - .../ainr/tinvest4s/domain/schemas.scala | 3 +- .../http/client/InvestApiClient.scala | 2 +- .../interpreters/InvestApiSttpClient.scala | 51 +++++++-- .../http/error/DefaultErrorHandler.scala | 23 ---- .../tinvest4s/http/error/ErrorHandler.scala | 2 +- .../tinvest4s/test/CatsBackendExample.scala | 6 +- .../tinvest4s/test/ZioBackendExample.scala | 1 - .../main/scala/InvestRequestImplSpec.scala | 60 ----------- .../src/main/scala/v1/api/PortfolioSpec.scala | 102 ++++++++++++++++++ .../v1/api/client/TestInvestApiClient.scala | 19 ++++ 11 files changed, 168 insertions(+), 102 deletions(-) delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala delete mode 100644 modules/tests/src/main/scala/InvestRequestImplSpec.scala create mode 100644 modules/tests/src/main/scala/v1/api/PortfolioSpec.scala create mode 100644 modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala index 80b0aa5..6c8b580 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala @@ -1,7 +1,6 @@ package github.ainr.tinvest4s.domain /** - * * @param trackingId * @param status * @param payload diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala index e2c98d0..6e6e7b1 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala @@ -11,7 +11,8 @@ object schemas { type Balance = Double type Lots = Int; - trait Response; + sealed trait Response + final class EmptyResponse() extends Response case class MoneyAmount(currency: String, value: Double) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala index 006df48..53404bc 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala @@ -3,5 +3,5 @@ package github.ainr.tinvest4s.http.client import github.ainr.tinvest4s.domain.schemas.{BrokerAccountId, PortfolioResponse} trait InvestApiClient[F[_]] { - def portfolio(brokerAccountId: Option[BrokerAccountId] = None): F[PortfolioResponse] + def portfolio(brokerAccountId: Option[BrokerAccountId] = None): F[Option[PortfolioResponse]] } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala index c1d24ba..acc4870 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala @@ -6,16 +6,30 @@ import github.ainr.tinvest4s.config.access.{InvestAccessConfig, Token} import github.ainr.tinvest4s.domain.InvestApiError import github.ainr.tinvest4s.domain.schemas.{BrokerAccountId, PortfolioResponse} import github.ainr.tinvest4s.http.client.InvestApiClient -import github.ainr.tinvest4s.http.error.ErrorHandler +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError import github.ainr.tinvest4s.http.json._ +import io.circe import sttp.client3.circe.asJsonEither -import sttp.client3.{SttpBackend, _} +import sttp.client3.{ResponseException, SttpBackend, _} + +/* +You should create custom error handler +For example: +```scala +error match { + case Left(DeserializationException(body, error)) => ??? // error deserializing response + case Left(HttpError(body, statusCode)) => ??? // http error + case Left(error) => ??? // other error +} +``` + */ final class InvestApiSttpClient[F[_]: Monad] ( config: InvestAccessConfig, - backend: SttpBackend[F, Any], - errorHandler: ErrorHandler[F] + backend: SttpBackend[F, Any] +)( + errorHandler: InvestApiResponseError => Unit = _ => () ) extends InvestApiClient[F] { private lazy val token: Token = config.token @@ -27,19 +41,34 @@ final class InvestApiSttpClient[F[_]: Monad] }" } - override def portfolio(brokerAccountId: Option[BrokerAccountId] = None): F[PortfolioResponse] = { - val uri = brokerAccountId match { - case Some(id) => uri"$baseUrl/portfolio?brokerAccountId=$id" - case _ => uri"$baseUrl/portfolio" + private def handle[R](response: Response[Either[InvestApiResponseError, R]]): Option[R] = { + response.body match { + case Right(r) => Some(r) + case Left(error) => { + errorHandler(error) + None + } } + } + + def portfolio( + brokerAccountId: Option[BrokerAccountId] = None + ): F[Option[PortfolioResponse]] = { + val uri = brokerAccountId + .map(id => uri"$baseUrl/portfolio?brokerAccountId=$id") + .getOrElse(uri"$baseUrl/portfolio") basicRequest .get(uri) .auth.bearer(token) .response(asJsonEither[InvestApiError, PortfolioResponse]) .send(backend) - .flatMap { r => - errorHandler.handle[InvestApiError, PortfolioResponse](r.body) - } + .map(handle) } } + +object InvestApiSttpClient { + + type InvestApiResponseError = ResponseException[InvestApiError, circe.Error] + +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala deleted file mode 100644 index e2a72ce..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/DefaultErrorHandler.scala +++ /dev/null @@ -1,23 +0,0 @@ -package github.ainr.tinvest4s.http.error - -import cats.Monad -import cats.implicits._ -import github.ainr.tinvest4s.domain.{InvestApiError, schemas} -import io.circe -import sttp.client3.{DeserializationException, HttpError, ResponseException} - -final class DefaultErrorHandler[F[_]: Monad] extends ErrorHandler[F] { - - override def handle[E, R <: schemas.Response] - ( - response: Either[ResponseException[InvestApiError, circe.Error], R] - ): F[R] = { - response match { - case Right(success) => success.pure[F] - case Left(DeserializationException(body, error)) => ??? // error deserializing spotify response - case Left(HttpError(body, statusCode)) => ??? // http error - case Left(error) => ??? // other error - } - } - -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala index 0481d9d..f899ddd 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala @@ -5,5 +5,5 @@ import github.ainr.tinvest4s.domain.schemas.Response import sttp.client3.ResponseException trait ErrorHandler[F[_]] { - def handle[E, R <: Response](response: Either[ResponseException[InvestApiError, io.circe.Error], R]): F[R] + def handle[R <: Response](response: Either[ResponseException[InvestApiError, Throwable], R], defaultValue: R): F[R] } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala index 279d4e9..99d32a3 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala @@ -5,7 +5,7 @@ import cats.effect.{Concurrent, ContextShift, ExitCode, IO, IOApp, Resource, Syn import github.ainr.tinvest4s.config.access.InvestAccessConfig import github.ainr.tinvest4s.http.client.InvestApiClient import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient -import github.ainr.tinvest4s.http.error.DefaultErrorHandler +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend object CatsBackendExample extends IOApp { @@ -21,9 +21,9 @@ object CatsBackendExample extends IOApp { def createClient[F[_]: Sync: Monad: Concurrent: ContextShift](): Resource[F, InvestApiClient[F]] = { val config = InvestAccessConfig(token = "t.kkxK5DlAIBodw5moQBIDF1zKSMD-Ov4Kfr5hrBSrTRaxOcRTaeSVKYIdiZXsbSuakLyq9fUK0NUe672oItp6xA") - val errorHandler = new DefaultErrorHandler[F]() + val errorHandler: InvestApiResponseError => Unit = _ => () AsyncHttpClientCatsBackend.resource().map { - backend => new InvestApiSttpClient[F](config, backend, errorHandler) + backend => new InvestApiSttpClient[F](config, backend)(errorHandler) } } } \ No newline at end of file diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala index 1c8dddd..cf5eb82 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala @@ -3,7 +3,6 @@ package github.ainr.tinvest4s.test import cats.implicits.catsStdShowForString import github.ainr.tinvest4s.config.access.InvestAccessConfig import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient -import github.ainr.tinvest4s.http.error.DefaultErrorHandler import sttp.client3.httpclient.zio.HttpClientZioBackend import zio._ import zio.interop.catz._ diff --git a/modules/tests/src/main/scala/InvestRequestImplSpec.scala b/modules/tests/src/main/scala/InvestRequestImplSpec.scala deleted file mode 100644 index bcab958..0000000 --- a/modules/tests/src/main/scala/InvestRequestImplSpec.scala +++ /dev/null @@ -1,60 +0,0 @@ -import github.ainr.tinvest4s.config.access.InvestAccessConfig -import github.ainr.tinvest4s.domain.schemas.{Portfolio, PortfolioPosition, PortfolioResponse} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import sttp.client3.testing.SttpBackendStub - -class InvestRequestImplSpec extends AnyFlatSpec with Matchers { - - val testingBackend = SttpBackendStub - .synchronous - .whenRequestMatches(_ => true) - .thenRespond( - """ - |{ - | "trackingId": "b16ed655d2c0205c", - | "payload": { - | "positions": [ - | { - | "figi": "BBG005HLSZ23", - | "ticker": "FXUS", - | "isin": "IE00BD3QHZ91", - | "instrumentType": "Etf", - | "balance": 155, - | "blocked": 0, - | "lots": 155, - | "name": "FinEx Акции американских компаний" - | } - | ] - | }, - | "status": "Ok" - |} - |""".stripMargin) - - val config = InvestAccessConfig("") - //val request = new InvestRequestImpl(config) - val result = - PortfolioResponse( - "b16ed655d2c0205c", - Portfolio( - List( - PortfolioPosition( - "BBG005HLSZ23", - Some("FXUS"), - Some("IE00BD3QHZ91"), - "Etf", - 155.0, - Some(0.0), - None, - 155, - None,None, - "FinEx Акции американских компаний") - ) - ), - "Ok" - ) - - //"InvestRequestImpl" must "work" in { - //testingBackend.send(request.portfolio).body shouldBe Right(result) - //} -} diff --git a/modules/tests/src/main/scala/v1/api/PortfolioSpec.scala b/modules/tests/src/main/scala/v1/api/PortfolioSpec.scala new file mode 100644 index 0000000..0e44ceb --- /dev/null +++ b/modules/tests/src/main/scala/v1/api/PortfolioSpec.scala @@ -0,0 +1,102 @@ +package v1.api + +import github.ainr.tinvest4s.domain.{InvestApiError, InvestApiErrorPayload} +import github.ainr.tinvest4s.domain.schemas.{Portfolio, PortfolioPosition, PortfolioResponse} +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.client3.HttpError +import sttp.client3.testing.SttpBackendStub +import sttp.model.StatusCode +import v1.api.client.TestInvestApiClient + +class PortfolioSpec extends AnyFlatSpec with Matchers with TestInvestApiClient { + + behavior of "InvestApiClient" + + it should "return portfolio" in { + val backend = SttpBackendStub + .synchronous + .whenRequestMatches(_ => true) + .thenRespond( + """ + |{ + | "trackingId": "b16ed655d2c0205c", + | "payload": { + | "positions": [ + | { + | "figi": "BBG005HLSZ23", + | "ticker": "FXUS", + | "isin": "IE00BD3QHZ91", + | "instrumentType": "Etf", + | "balance": 155, + | "blocked": 0, + | "lots": 155, + | "name": "FinEx Акции американских компаний" + | } + | ] + | }, + | "status": "Ok" + |} + |""".stripMargin) + + + val portfolioResponse = + PortfolioResponse( + "b16ed655d2c0205c", + Portfolio( + List( + PortfolioPosition( + figi = "BBG005HLSZ23", + ticker = Some("FXUS"), + isin = Some("IE00BD3QHZ91"), + instrumentType = "Etf", + balance = 155.0, + blocked = Some(0.0), + expectedYield = None, + lots = 155, + averagePositionPrice = None, + averagePositionPriceNoNkd = None, + name = "FinEx Акции американских компаний") + ) + ), + "Ok" + ) + + val errorHandler: InvestApiResponseError => Unit = _ => () + val client = createClient(backend, errorHandler) + + val portfolio = client.portfolio() + + portfolio shouldBe Some(portfolioResponse) + } + + it should "return error" in { + val backend = SttpBackendStub + .synchronous + .whenRequestMatches(_ => true) + .thenRespond( + """ + |{ + | "trackingId": "b16ed655d2c0205c", + | "payload": { + | "message": "Some error", + | "code": "42" + | }, + | "status": "Ok" + |} + |""".stripMargin, StatusCode(500)) + + def errorHandler(error: InvestApiResponseError): Unit = { + error shouldBe HttpError( + InvestApiError("b16ed655d2c0205c", "Ok", InvestApiErrorPayload(Some("Some error"), Some("42"))), + StatusCode(500) + ) + } + val client = createClient(backend, errorHandler) + + val portfolio = client.portfolio() + + portfolio shouldBe None + } +} diff --git a/modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala b/modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala new file mode 100644 index 0000000..bae6fe7 --- /dev/null +++ b/modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala @@ -0,0 +1,19 @@ +package v1.api.client + +import github.ainr.tinvest4s.config.access.InvestAccessConfig +import github.ainr.tinvest4s.http.client.InvestApiClient +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient +import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +import sttp.capabilities.WebSockets +import sttp.client3.Identity +import sttp.client3.testing.SttpBackendStub + +trait TestInvestApiClient { + def createClient( + backend: SttpBackendStub[Identity, WebSockets], + errorHandler: InvestApiResponseError => Unit + ): InvestApiClient[Identity] = { + val config = InvestAccessConfig("token") + new InvestApiSttpClient[Identity](config, backend)(errorHandler) + } +} From ddcc93c34bf48b3586aa20587c98e0a34518227e Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Sun, 16 May 2021 23:50:24 +0500 Subject: [PATCH 07/10] WIP limit and market orders --- .../ainr/tinvest4s/domain/schemas.scala | 87 +++++++++++-------- .../http/client/InvestApiClient.scala | 18 +++- .../interpreters/InvestApiSttpClient.scala | 64 +++++++++++--- .../github/ainr/tinvest4s/http/json.scala | 30 +++++-- .../tinvest4s/test/CatsBackendExample.scala | 13 ++- .../tinvest4s/test/ZioBackendExample.scala | 37 -------- project/Dependencies.scala | 2 +- 7 files changed, 155 insertions(+), 96 deletions(-) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala index 6e6e7b1..bde4cb2 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala @@ -12,52 +12,63 @@ object schemas { type Lots = Int; sealed trait Response - final class EmptyResponse() extends Response case class MoneyAmount(currency: String, value: Double) sealed trait Currency { def name: String } - case class RUB() extends Currency() { override def name: String = "RUB" } - case class USD() extends Currency() { override def name: String = "USD" } - case class EUR() extends Currency() { override def name: String = "EUR" } - case class GBP() extends Currency() { override def name: String = "GBP" } - case class HKD() extends Currency() { override def name: String = "HKD" } - case class CHF() extends Currency() { override def name: String = "CHF" } - case class JPY() extends Currency() { override def name: String = "JPY" } - case class CNY() extends Currency() { override def name: String = "CNY" } - case class TRY() extends Currency() { override def name: String = "TRY" } - case class UnknownCurrency() extends Currency() { override def name: String = "UnknownCurrency" } + object Currency { + case class RUB() extends Currency() { override def name: String = "RUB" } + case class USD() extends Currency() { override def name: String = "USD" } + case class EUR() extends Currency() { override def name: String = "EUR" } + case class GBP() extends Currency() { override def name: String = "GBP" } + case class HKD() extends Currency() { override def name: String = "HKD" } + case class CHF() extends Currency() { override def name: String = "CHF" } + case class JPY() extends Currency() { override def name: String = "JPY" } + case class CNY() extends Currency() { override def name: String = "CNY" } + case class TRY() extends Currency() { override def name: String = "TRY" } + case class UnknownCurrency() extends Currency() { override def name: String = "UnknownCurrency" } + } - sealed trait Operation { def name: String } - case class Buy() extends Operation { override def name: String = "Buy" } + sealed trait Operation { + def name: String + override def toString: String = name + } + object Operation { + case class Buy() extends Operation { override def name: String = "Buy" } + case class Sell() extends Operation { override def name: String = "Sell" } + } - case class Sell() extends Operation { override def name: String = "Sell" } - case class OrderStatus(value: String) object OrderStatus { - val New = "New" - val PartiallyFill = "PartiallyFill" - val Fill = "Fill" - val Cancelled = "Cancelled" - val Replaced = "Replaced" - val PendingCancel = "PendingCancel" - val Rejected = "Rejected" - val PendingReplace = "PendingReplace" - val PendingNew = "PendingNew" + case class New() extends OrderStatus { override def name: String = "New" } + case class PartiallyFill() extends OrderStatus { override def name: String = "PartiallyFill" } + case class Fill() extends OrderStatus { override def name: String = "Fill" } + case class Cancelled() extends OrderStatus { override def name: String = "Cancelled" } + case class Replaced() extends OrderStatus { override def name: String = "Replaced" } + case class PendingCancel() extends OrderStatus { override def name: String = "PendingCancel" } + case class Rejected() extends OrderStatus { override def name: String = "Rejected" } + case class PendingReplace() extends OrderStatus { override def name: String = "PendingReplace" } + case class PendingNew() extends OrderStatus { override def name: String = "PendingNew" } + } + sealed trait OrderStatus { + def name: String + override def toString: String = name } - trait CandleResolution { def value: String } - case class `1min` () extends CandleResolution { override def value: String = "1min" } - case class `2min` () extends CandleResolution { override def value: String = "2min" } - case class `3min` () extends CandleResolution { override def value: String = "3min" } - case class `5min` () extends CandleResolution { override def value: String = "5min" } - case class `10min`() extends CandleResolution { override def value: String = "10min" } - case class `15min`() extends CandleResolution { override def value: String = "15min" } - case class `30min`() extends CandleResolution { override def value: String = "30min" } - case class hour () extends CandleResolution { override def value: String = "hour" } - case class day () extends CandleResolution { override def value: String = "day" } - case class week () extends CandleResolution { override def value: String = "week" } - case class month () extends CandleResolution { override def value: String = "month" } + object CandleResolution { + case class `1min` () extends CandleResolution { override def value: String = "1min" } + case class `2min` () extends CandleResolution { override def value: String = "2min" } + case class `3min` () extends CandleResolution { override def value: String = "3min" } + case class `5min` () extends CandleResolution { override def value: String = "5min" } + case class `10min`() extends CandleResolution { override def value: String = "10min" } + case class `15min`() extends CandleResolution { override def value: String = "15min" } + case class `30min`() extends CandleResolution { override def value: String = "30min" } + case class hour () extends CandleResolution { override def value: String = "hour" } + case class day () extends CandleResolution { override def value: String = "day" } + case class week () extends CandleResolution { override def value: String = "week" } + case class month () extends CandleResolution { override def value: String = "month" } + } + sealed trait CandleResolution { def value: String } case class PortfolioResponse(trackingId: TrackingId, payload: Portfolio, status: String) extends Response @@ -79,7 +90,7 @@ object schemas { case class MarketOrderRequest(lots: Lots, operation: Operation) case class LimitOrderRequest(lots: Lots, operation: Operation, price: Price) - case class OrdersResponse(trackingId: String, status: String, payload: PlacedOrder) extends Response + case class OrderResponse(trackingId: String, status: String, payload: PlacedOrder) extends Response case class PlacedOrder( orderId: OrderId, @@ -95,7 +106,7 @@ object schemas { case class Order( orderId: OrderId, figi: FIGI, - operation: Operation, + //operation: Operation, status: OrderStatus, requestedLots: Lots, executedLots: Lots, diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala index 53404bc..3cad86f 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala @@ -1,7 +1,23 @@ package github.ainr.tinvest4s.http.client -import github.ainr.tinvest4s.domain.schemas.{BrokerAccountId, PortfolioResponse} +import github.ainr.tinvest4s.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]] } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala index acc4870..607ffe7 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala @@ -1,25 +1,27 @@ package github.ainr.tinvest4s.http.client.interpreters import cats.Monad -import cats.implicits._ +import cats.syntax.all._ import github.ainr.tinvest4s.config.access.{InvestAccessConfig, Token} -import github.ainr.tinvest4s.domain.InvestApiError -import github.ainr.tinvest4s.domain.schemas.{BrokerAccountId, PortfolioResponse} +import github.ainr.tinvest4s.domain.schemas.{Response => _, _} +import github.ainr.tinvest4s.domain.{InvestApiError, schemas} import github.ainr.tinvest4s.http.client.InvestApiClient import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError import github.ainr.tinvest4s.http.json._ import io.circe -import sttp.client3.circe.asJsonEither -import sttp.client3.{ResponseException, SttpBackend, _} +import sttp.client3._ +import sttp.client3.circe._ /* You should create custom error handler For example: ```scala -error match { - case Left(DeserializationException(body, error)) => ??? // error deserializing response - case Left(HttpError(body, statusCode)) => ??? // http error - case Left(error) => ??? // other error +def errorHandler(error: InvestApiResponseError): Unit = { + error match { + case DeserializationException(body, error) => ??? // error deserializing response + case HttpError(body, statusCode) => ??? // http error + case error => ??? // other error + } } ``` */ @@ -65,10 +67,52 @@ final class InvestApiSttpClient[F[_]: Monad] .send(backend) .map(handle) } + + override def limitOrder( + figi: FIGI, + lots: Lots, + operation: Operation, + price: Price, + brokerAccountId: Option[BrokerAccountId] + ): F[Option[OrderResponse]] = { + val uri = brokerAccountId + .map(id => uri"$baseUrl/orders/limit-order?figi=$figi&brokerAccountId=$id") + .getOrElse(uri"$baseUrl/orders/limit-order?figi=$figi") + + val requestBody = LimitOrderRequest(lots, operation, price) + + basicRequest + .post(uri) + .auth.bearer(token) + .body(requestBody) + .response(asJsonEither[InvestApiError, OrderResponse]) + .send(backend) + .map(handle) + } + + override def marketOrder( + figi: FIGI, + lots: Lots, + operation: Operation, + brokerAccountId: Option[BrokerAccountId] + ): F[Option[OrderResponse]] = { + val uri = brokerAccountId + .map(id => uri"$baseUrl/orders/market-order?figi=$figi&brokerAccountId=$id") + .getOrElse(uri"$baseUrl/orders/market-order?figi=$figi") + + val requestBody = MarketOrderRequest(lots, operation) + + basicRequest + .post(uri) + .auth.bearer(token) + .body(requestBody) + .response(asJsonEither[InvestApiError, OrderResponse]) + .send(backend) + .map(handle) + } } object InvestApiSttpClient { type InvestApiResponseError = ResponseException[InvestApiError, circe.Error] - } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala index d290164..bd84543 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala @@ -1,9 +1,9 @@ package github.ainr.tinvest4s.http +import github.ainr.tinvest4s.domain.schemas.{Operation, _} import github.ainr.tinvest4s.domain.{InvestApiError, InvestApiErrorPayload} -import github.ainr.tinvest4s.domain.schemas.{Currency, MoneyAmount, Portfolio, PortfolioPosition, PortfolioResponse, TrackingId} -import io.circe.generic.semiauto._ -import io.circe._ +import io.circe.{Decoder, Encoder} +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} object json extends JsonCodecs { @@ -11,8 +11,9 @@ object json extends JsonCodecs { private[http] trait JsonCodecs { - implicit val currencyAmountDecoder: Decoder[Currency] = deriveDecoder - implicit val currencyAmountEncoder: Encoder[Currency] = deriveEncoder + + implicit val currencyAmountDecoder: Decoder[Currency] = deriveDecoder[Currency] + implicit val currencyAmountEncoder: Encoder[Currency] = deriveEncoder[Currency] implicit val moneyAmountDecoder: Decoder[MoneyAmount] = deriveDecoder implicit val moneyAmountEncoder: Encoder[MoneyAmount] = deriveEncoder @@ -23,10 +24,27 @@ private[http] trait JsonCodecs { implicit val portfolioDecoder: Decoder[Portfolio] = deriveDecoder implicit val portfolioEncoder: Encoder[Portfolio] = deriveEncoder + implicit val operationDecoder: Decoder[Operation] = deriveDecoder + implicit val operationEncoder: Encoder[Operation] = deriveEncoder + implicit val portfolioResponseDecoder: Decoder[PortfolioResponse] = deriveDecoder implicit val portfolioResponseEncoder: Encoder[PortfolioResponse] = deriveEncoder + implicit val orderStatusDecoder: Decoder[OrderStatus] = deriveDecoder + implicit val orderStatusEncoder: Encoder[OrderStatus] = deriveEncoder + + implicit val placedOrderDecoder: Decoder[PlacedOrder] = deriveDecoder + implicit val placedOrderEncoder: Encoder[PlacedOrder] = deriveEncoder + + implicit val orderResponseDecoder: Decoder[OrderResponse] = deriveDecoder + implicit val orderResponseEncoder: Encoder[OrderResponse] = deriveEncoder + + implicit val limitOrderRequestDecoder: Decoder[LimitOrderRequest] = deriveDecoder + implicit val limitOrderRequestEncoder: Encoder[LimitOrderRequest] = deriveEncoder + + implicit val marketOrderRequestDecoder: Decoder[MarketOrderRequest] = deriveDecoder + implicit val marketOrderRequestEncoder: Encoder[MarketOrderRequest] = deriveEncoder + implicit val investApiErrorPayloadDecoder: Decoder[InvestApiErrorPayload] = deriveDecoder implicit val investClientErrorDecoder: Decoder[InvestApiError] = deriveDecoder - } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala index 99d32a3..ceb09bc 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala @@ -3,6 +3,7 @@ package github.ainr.tinvest4s.test import cats.Monad import cats.effect.{Concurrent, ContextShift, ExitCode, IO, IOApp, Resource, Sync} import github.ainr.tinvest4s.config.access.InvestAccessConfig +import github.ainr.tinvest4s.domain.schemas.Operation import github.ainr.tinvest4s.http.client.InvestApiClient import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError @@ -13,15 +14,21 @@ object CatsBackendExample extends IOApp { override def run(args: List[String]): IO[ExitCode] = { createClient[IO]() .use { investApi => for { - portfolio <- investApi.portfolio() - _ <- IO.delay(println(portfolio)) + 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.Buy()) + _ <- 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") - val errorHandler: InvestApiResponseError => Unit = _ => () + def errorHandler(error: InvestApiResponseError): Unit = { + println(error) + } AsyncHttpClientCatsBackend.resource().map { backend => new InvestApiSttpClient[F](config, backend)(errorHandler) } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala index cf5eb82..eda2a7b 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala @@ -1,38 +1 @@ package github.ainr.tinvest4s.test - -import cats.implicits.catsStdShowForString -import github.ainr.tinvest4s.config.access.InvestAccessConfig -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient -import sttp.client3.httpclient.zio.HttpClientZioBackend -import zio._ -import zio.interop.catz._ - -// TODO: ??? -/* -object ZioBackendExample extends zio.App { - - val layer = HttpClientZioBackend().flatMap { - backend => - val config = InvestAccessConfig(token = "t.kkxK5DlAIBodw5moQBIDF1zKSMD-Ov4Kfr5hrBSrTRaxOcRTaeSVKYIdiZXsbSuakLyq9fUK0NUe672oItp6xA") - val errorHandler = new DefaultErrorHandler() - val client = new InvestApiSttpClient[Task](config, backend, errorHandler) - - for { - _ <- console.putStrLn("hui") - p = client.portfolio() - _ <- console.putStrLn(p.toString) - } yield () - }.toLayer - - - override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { - def z = ZIO.runtime[zio.ZEnv].provideLayer(layer) - - for { - _ <- z - } yield () - - ??? - } -} - */ \ No newline at end of file diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 4ba5fff..9b61422 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,7 +10,7 @@ object Dependencies { val newtype = "0.4.3" val scalatest = "3.2.0" val scalamock = "5.1.0" - val sttp = "3.1.9" + val sttp = "3.3.3" val zio_interop_cats = "3.0.2.0" val `async-http-client-backend-cats-ce2` = "3.3.3" } From 7eb7a1f7a30dd02daf58da50bc6b4642080c0a88 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Mon, 17 May 2021 23:31:51 +0500 Subject: [PATCH 08/10] Limit and market order methods --- .../ainr/tinvest4s/domain/schemas.scala | 115 ----------------- .../tinvest4s/test/ZioBackendExample.scala | 1 - .../tinvest4s/{ => v1}/config/access.scala | 2 +- .../tinvest4s/{ => v1}/domain/Error.scala | 2 +- .../tinvest4s/{ => v1}/domain/Market.scala | 6 +- .../{ => v1}/domain/TradeStatus.scala | 2 +- .../ainr/tinvest4s/v1/domain/schemas.scala | 116 ++++++++++++++++++ .../http/client/InvestApiClient.scala | 4 +- .../interpreters/InvestApiSttpClient.scala | 14 +-- .../{ => v1}/http/error/ErrorHandler.scala | 6 +- .../ainr/tinvest4s/{ => v1}/http/json.scala | 18 +-- .../{ => v1}/test/CatsBackendExample.scala | 16 +-- .../tinvest4s/v1/test/ZioBackendExample.scala | 1 + .../scala/v1/api/LimitOrderMethodSpec.scala | 89 ++++++++++++++ .../scala/v1/api/MarketOrderMethodSpec.scala | 89 ++++++++++++++ ...ioSpec.scala => PortfolioMethodSpec.scala} | 8 +- .../v1/api/client/TestInvestApiClient.scala | 8 +- 17 files changed, 333 insertions(+), 164 deletions(-) delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/config/access.scala (75%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/domain/Error.scala (89%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/domain/Market.scala (93%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/domain/TradeStatus.scala (79%) create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/schemas.scala rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/http/client/InvestApiClient.scala (72%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/http/client/interpreters/InvestApiSttpClient.scala (85%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/http/error/ErrorHandler.scala (54%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/http/json.scala (72%) rename modules/core/src/main/scala/github/ainr/tinvest4s/{ => v1}/test/CatsBackendExample.scala (71%) create mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala create mode 100644 modules/tests/src/main/scala/v1/api/LimitOrderMethodSpec.scala create mode 100644 modules/tests/src/main/scala/v1/api/MarketOrderMethodSpec.scala rename modules/tests/src/main/scala/v1/api/{PortfolioSpec.scala => PortfolioMethodSpec.scala} (88%) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala deleted file mode 100644 index bde4cb2..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/schemas.scala +++ /dev/null @@ -1,115 +0,0 @@ -package github.ainr.tinvest4s.domain - -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) - - sealed trait Currency { def name: String } - object Currency { - case class RUB() extends Currency() { override def name: String = "RUB" } - case class USD() extends Currency() { override def name: String = "USD" } - case class EUR() extends Currency() { override def name: String = "EUR" } - case class GBP() extends Currency() { override def name: String = "GBP" } - case class HKD() extends Currency() { override def name: String = "HKD" } - case class CHF() extends Currency() { override def name: String = "CHF" } - case class JPY() extends Currency() { override def name: String = "JPY" } - case class CNY() extends Currency() { override def name: String = "CNY" } - case class TRY() extends Currency() { override def name: String = "TRY" } - case class UnknownCurrency() extends Currency() { override def name: String = "UnknownCurrency" } - } - - sealed trait Operation { - def name: String - override def toString: String = name - } - object Operation { - case class Buy() extends Operation { override def name: String = "Buy" } - case class Sell() extends Operation { override def name: String = "Sell" } - } - - - object OrderStatus { - case class New() extends OrderStatus { override def name: String = "New" } - case class PartiallyFill() extends OrderStatus { override def name: String = "PartiallyFill" } - case class Fill() extends OrderStatus { override def name: String = "Fill" } - case class Cancelled() extends OrderStatus { override def name: String = "Cancelled" } - case class Replaced() extends OrderStatus { override def name: String = "Replaced" } - case class PendingCancel() extends OrderStatus { override def name: String = "PendingCancel" } - case class Rejected() extends OrderStatus { override def name: String = "Rejected" } - case class PendingReplace() extends OrderStatus { override def name: String = "PendingReplace" } - case class PendingNew() extends OrderStatus { override def name: String = "PendingNew" } - } - sealed trait OrderStatus { - def name: String - override def toString: String = name - } - - object CandleResolution { - case class `1min` () extends CandleResolution { override def value: String = "1min" } - case class `2min` () extends CandleResolution { override def value: String = "2min" } - case class `3min` () extends CandleResolution { override def value: String = "3min" } - case class `5min` () extends CandleResolution { override def value: String = "5min" } - case class `10min`() extends CandleResolution { override def value: String = "10min" } - case class `15min`() extends CandleResolution { override def value: String = "15min" } - case class `30min`() extends CandleResolution { override def value: String = "30min" } - case class hour () extends CandleResolution { override def value: String = "hour" } - case class day () extends CandleResolution { override def value: String = "day" } - case class week () extends CandleResolution { override def value: String = "week" } - case class month () extends CandleResolution { override def value: String = "month" } - } - sealed trait CandleResolution { def value: String } - - 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 - ) -} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala deleted file mode 100644 index eda2a7b..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/test/ZioBackendExample.scala +++ /dev/null @@ -1 +0,0 @@ -package github.ainr.tinvest4s.test diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/config/access.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/config/access.scala similarity index 75% rename from modules/core/src/main/scala/github/ainr/tinvest4s/config/access.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/config/access.scala index 145b097..016d7c3 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/config/access.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/config/access.scala @@ -1,4 +1,4 @@ -package github.ainr.tinvest4s.config +package github.ainr.tinvest4s.v1.config object access { type Token = String diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/Error.scala similarity index 89% rename from modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/Error.scala index 6c8b580..f8fa531 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Error.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/Error.scala @@ -1,4 +1,4 @@ -package github.ainr.tinvest4s.domain +package github.ainr.tinvest4s.v1.domain /** * @param trackingId diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Market.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/Market.scala similarity index 93% rename from modules/core/src/main/scala/github/ainr/tinvest4s/domain/Market.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/Market.scala index 0119e8a..89cf546 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/Market.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/Market.scala @@ -1,7 +1,7 @@ -package github.ainr.tinvest4s.domain +package github.ainr.tinvest4s.v1.domain -import github.ainr.tinvest4s.domain.TradeStatus.TradeStatus -import github.ainr.tinvest4s.domain.schemas.{CandleResolution, FIGI, Price, TrackingId} +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) diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/TradeStatus.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/TradeStatus.scala similarity index 79% rename from modules/core/src/main/scala/github/ainr/tinvest4s/domain/TradeStatus.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/TradeStatus.scala index 2a696cc..bbb5ce2 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/domain/TradeStatus.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/TradeStatus.scala @@ -1,4 +1,4 @@ -package github.ainr.tinvest4s.domain +package github.ainr.tinvest4s.v1.domain /** * diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/schemas.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/schemas.scala new file mode 100644 index 0000000..537df8a --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/domain/schemas.scala @@ -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 + ) +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/client/InvestApiClient.scala similarity index 72% rename from modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/client/InvestApiClient.scala index 3cad86f..567d953 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/InvestApiClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/client/InvestApiClient.scala @@ -1,6 +1,6 @@ -package github.ainr.tinvest4s.http.client +package github.ainr.tinvest4s.v1.http.client -import github.ainr.tinvest4s.domain.schemas.{BrokerAccountId, FIGI, Lots, Operation, OrderResponse, PortfolioResponse, Price} +import github.ainr.tinvest4s.v1.domain.schemas.{BrokerAccountId, FIGI, Lots, Operation, OrderResponse, PortfolioResponse, Price} trait InvestApiClient[F[_]] { diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/client/interpreters/InvestApiSttpClient.scala similarity index 85% rename from modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/client/interpreters/InvestApiSttpClient.scala index 607ffe7..1bde389 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/client/interpreters/InvestApiSttpClient.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/client/interpreters/InvestApiSttpClient.scala @@ -1,13 +1,13 @@ -package github.ainr.tinvest4s.http.client.interpreters +package github.ainr.tinvest4s.v1.http.client.interpreters import cats.Monad import cats.syntax.all._ -import github.ainr.tinvest4s.config.access.{InvestAccessConfig, Token} -import github.ainr.tinvest4s.domain.schemas.{Response => _, _} -import github.ainr.tinvest4s.domain.{InvestApiError, schemas} -import github.ainr.tinvest4s.http.client.InvestApiClient -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError -import github.ainr.tinvest4s.http.json._ +import github.ainr.tinvest4s.v1.config.access.{InvestAccessConfig, Token} +import github.ainr.tinvest4s.v1.domain.schemas.{Response => _, _} +import github.ainr.tinvest4s.v1.domain.{InvestApiError, schemas} +import github.ainr.tinvest4s.v1.http.client.InvestApiClient +import github.ainr.tinvest4s.v1.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +import github.ainr.tinvest4s.v1.http.json._ import io.circe import sttp.client3._ import sttp.client3.circe._ diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/error/ErrorHandler.scala similarity index 54% rename from modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/error/ErrorHandler.scala index f899ddd..007bf3c 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/error/ErrorHandler.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/error/ErrorHandler.scala @@ -1,7 +1,7 @@ -package github.ainr.tinvest4s.http.error +package github.ainr.tinvest4s.v1.http.error -import github.ainr.tinvest4s.domain.InvestApiError -import github.ainr.tinvest4s.domain.schemas.Response +import github.ainr.tinvest4s.v1.domain.InvestApiError +import github.ainr.tinvest4s.v1.domain.schemas.Response import sttp.client3.ResponseException trait ErrorHandler[F[_]] { diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/json.scala similarity index 72% rename from modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/json.scala index bd84543..38a9095 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/http/json.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/http/json.scala @@ -1,9 +1,9 @@ -package github.ainr.tinvest4s.http +package github.ainr.tinvest4s.v1.http -import github.ainr.tinvest4s.domain.schemas.{Operation, _} -import github.ainr.tinvest4s.domain.{InvestApiError, InvestApiErrorPayload} -import io.circe.{Decoder, Encoder} +import github.ainr.tinvest4s.v1.domain.schemas._ +import github.ainr.tinvest4s.v1.domain.{InvestApiError, InvestApiErrorPayload} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} object json extends JsonCodecs { @@ -11,10 +11,6 @@ object json extends JsonCodecs { private[http] trait JsonCodecs { - - implicit val currencyAmountDecoder: Decoder[Currency] = deriveDecoder[Currency] - implicit val currencyAmountEncoder: Encoder[Currency] = deriveEncoder[Currency] - implicit val moneyAmountDecoder: Decoder[MoneyAmount] = deriveDecoder implicit val moneyAmountEncoder: Encoder[MoneyAmount] = deriveEncoder @@ -24,15 +20,9 @@ private[http] trait JsonCodecs { implicit val portfolioDecoder: Decoder[Portfolio] = deriveDecoder implicit val portfolioEncoder: Encoder[Portfolio] = deriveEncoder - implicit val operationDecoder: Decoder[Operation] = deriveDecoder - implicit val operationEncoder: Encoder[Operation] = deriveEncoder - implicit val portfolioResponseDecoder: Decoder[PortfolioResponse] = deriveDecoder implicit val portfolioResponseEncoder: Encoder[PortfolioResponse] = deriveEncoder - implicit val orderStatusDecoder: Decoder[OrderStatus] = deriveDecoder - implicit val orderStatusEncoder: Encoder[OrderStatus] = deriveEncoder - implicit val placedOrderDecoder: Decoder[PlacedOrder] = deriveDecoder implicit val placedOrderEncoder: Encoder[PlacedOrder] = deriveEncoder diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/CatsBackendExample.scala similarity index 71% rename from modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/CatsBackendExample.scala index ceb09bc..81032e0 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/test/CatsBackendExample.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/CatsBackendExample.scala @@ -1,12 +1,12 @@ -package github.ainr.tinvest4s.test +package github.ainr.tinvest4s.v1.test import cats.Monad import cats.effect.{Concurrent, ContextShift, ExitCode, IO, IOApp, Resource, Sync} -import github.ainr.tinvest4s.config.access.InvestAccessConfig -import github.ainr.tinvest4s.domain.schemas.Operation -import github.ainr.tinvest4s.http.client.InvestApiClient -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +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 { @@ -16,9 +16,9 @@ object CatsBackendExample extends IOApp { .use { investApi => for { portfolio <- investApi.portfolio() _ <- IO.delay(println(portfolio)) - limitOrderResult <- investApi.limitOrder("BBG005HLSZ23", 1, Operation.Buy(), 10) + limitOrderResult <- investApi.limitOrder("BBG005HLSZ23", 1, Operation.Buy, 10) _ <- IO.delay(println(limitOrderResult)) - marketOrderResult <- investApi.marketOrder("BBG005HLSZ23", 1, Operation.Buy()) + marketOrderResult <- investApi.marketOrder("BBG005HLSZ23", 1, Operation.Sell) _ <- IO.delay(println(marketOrderResult)) } yield ExitCode.Success } diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala new file mode 100644 index 0000000..767d090 --- /dev/null +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala @@ -0,0 +1 @@ +package github.ainr.tinvest4s.v1.test diff --git a/modules/tests/src/main/scala/v1/api/LimitOrderMethodSpec.scala b/modules/tests/src/main/scala/v1/api/LimitOrderMethodSpec.scala new file mode 100644 index 0000000..f103810 --- /dev/null +++ b/modules/tests/src/main/scala/v1/api/LimitOrderMethodSpec.scala @@ -0,0 +1,89 @@ +package v1.api + +import github.ainr.tinvest4s.v1.domain.schemas.{Operation, OrderResponse, PlacedOrder} +import github.ainr.tinvest4s.v1.domain.{InvestApiError, InvestApiErrorPayload} +import github.ainr.tinvest4s.v1.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.client3.HttpError +import sttp.client3.testing.SttpBackendStub +import sttp.model.StatusCode +import v1.api.client.TestInvestApiClient + +class LimitOrderMethodSpec extends AnyFlatSpec with Matchers with TestInvestApiClient { + + behavior of "InvestApiClient" + + it should "create limit order" in { + val backend = SttpBackendStub + .synchronous + .whenRequestMatches(_ => true) + .thenRespond( + """ + |{ + | "trackingId": "bfbc0bd6607b8d14", + | "payload": { + | "orderId": "b7278cc7-8d88-412a-a85c-984d5a0c2c4d", + | "operation": "Buy", + | "status": "Fill", + | "requestedLots": "1", + | "executedLots": "1" + | }, + | "status": "Ok" + |} + |""".stripMargin) + + + val limitOrderResponse = + OrderResponse( + trackingId = "bfbc0bd6607b8d14", + status = "Ok", + payload = PlacedOrder( + orderId = "b7278cc7-8d88-412a-a85c-984d5a0c2c4d", + operation = "Buy", + status = "Fill", + rejectReason = None, + message = None, + requestedLots = 1, + executedLots = 1, + commission = None + ) + ) + + val errorHandler: InvestApiResponseError => Unit = _ => () + val client = createClient(backend, errorHandler) + + val response = client.limitOrder("BBG005HLSZ23", 1, Operation.Sell, 10) + + response shouldBe Some(limitOrderResponse) + } + + it should "return error" in { + val backend = SttpBackendStub + .synchronous + .whenRequestMatches(_ => true) + .thenRespond( + """ + |{ + | "trackingId": "b16ed655d2c0205c", + | "payload": { + | "message": "Some error", + | "code": "42" + | }, + | "status": "Ok" + |} + |""".stripMargin, StatusCode(500)) + + def errorHandler(error: InvestApiResponseError): Unit = { + error shouldBe HttpError( + InvestApiError("b16ed655d2c0205c", "Ok", InvestApiErrorPayload(Some("Some error"), Some("42"))), + StatusCode(500) + ) + } + val client = createClient(backend, errorHandler) + + val response = client.limitOrder("BBG005HLSZ23", 1, Operation.Sell, 10) + + response shouldBe None + } +} diff --git a/modules/tests/src/main/scala/v1/api/MarketOrderMethodSpec.scala b/modules/tests/src/main/scala/v1/api/MarketOrderMethodSpec.scala new file mode 100644 index 0000000..1cfa336 --- /dev/null +++ b/modules/tests/src/main/scala/v1/api/MarketOrderMethodSpec.scala @@ -0,0 +1,89 @@ +package v1.api + +import github.ainr.tinvest4s.v1.domain.schemas.{Operation, OrderResponse, PlacedOrder} +import github.ainr.tinvest4s.v1.domain.{InvestApiError, InvestApiErrorPayload} +import github.ainr.tinvest4s.v1.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.client3.HttpError +import sttp.client3.testing.SttpBackendStub +import sttp.model.StatusCode +import v1.api.client.TestInvestApiClient + +class MarketOrderMethodSpec extends AnyFlatSpec with Matchers with TestInvestApiClient { + + behavior of "InvestApiClient" + + it should "create market order" in { + val backend = SttpBackendStub + .synchronous + .whenRequestMatches(_ => true) + .thenRespond( + """ + |{ + | "trackingId": "bfbc0bd6607b8d14", + | "payload": { + | "orderId": "b7278cc7-8d88-412a-a85c-984d5a0c2c4d", + | "operation": "Buy", + | "status": "Fill", + | "requestedLots": "1", + | "executedLots": "1" + | }, + | "status": "Ok" + |} + |""".stripMargin) + + + val limitOrderResponse = + OrderResponse( + trackingId = "bfbc0bd6607b8d14", + status = "Ok", + payload = PlacedOrder( + orderId = "b7278cc7-8d88-412a-a85c-984d5a0c2c4d", + operation = "Buy", + status = "Fill", + rejectReason = None, + message = None, + requestedLots = 1, + executedLots = 1, + commission = None + ) + ) + + val errorHandler: InvestApiResponseError => Unit = _ => () + val client = createClient(backend, errorHandler) + + val response = client.marketOrder("BBG005HLSZ23", 1, Operation.Sell) + + response shouldBe Some(limitOrderResponse) + } + + it should "return error" in { + val backend = SttpBackendStub + .synchronous + .whenRequestMatches(_ => true) + .thenRespond( + """ + |{ + | "trackingId": "b16ed655d2c0205c", + | "payload": { + | "message": "Some error", + | "code": "42" + | }, + | "status": "Ok" + |} + |""".stripMargin, StatusCode(500)) + + def errorHandler(error: InvestApiResponseError): Unit = { + error shouldBe HttpError( + InvestApiError("b16ed655d2c0205c", "Ok", InvestApiErrorPayload(Some("Some error"), Some("42"))), + StatusCode(500) + ) + } + val client = createClient(backend, errorHandler) + + val response = client.marketOrder("BBG005HLSZ23", 1, Operation.Sell) + + response shouldBe None + } +} diff --git a/modules/tests/src/main/scala/v1/api/PortfolioSpec.scala b/modules/tests/src/main/scala/v1/api/PortfolioMethodSpec.scala similarity index 88% rename from modules/tests/src/main/scala/v1/api/PortfolioSpec.scala rename to modules/tests/src/main/scala/v1/api/PortfolioMethodSpec.scala index 0e44ceb..772a4b0 100644 --- a/modules/tests/src/main/scala/v1/api/PortfolioSpec.scala +++ b/modules/tests/src/main/scala/v1/api/PortfolioMethodSpec.scala @@ -1,8 +1,8 @@ package v1.api -import github.ainr.tinvest4s.domain.{InvestApiError, InvestApiErrorPayload} -import github.ainr.tinvest4s.domain.schemas.{Portfolio, PortfolioPosition, PortfolioResponse} -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +import github.ainr.tinvest4s.v1.domain.{InvestApiError, InvestApiErrorPayload} +import github.ainr.tinvest4s.v1.domain.schemas.{Portfolio, PortfolioPosition, PortfolioResponse} +import github.ainr.tinvest4s.v1.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import sttp.client3.HttpError @@ -10,7 +10,7 @@ import sttp.client3.testing.SttpBackendStub import sttp.model.StatusCode import v1.api.client.TestInvestApiClient -class PortfolioSpec extends AnyFlatSpec with Matchers with TestInvestApiClient { +class PortfolioMethodSpec extends AnyFlatSpec with Matchers with TestInvestApiClient { behavior of "InvestApiClient" diff --git a/modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala b/modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala index bae6fe7..3926e07 100644 --- a/modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala +++ b/modules/tests/src/main/scala/v1/api/client/TestInvestApiClient.scala @@ -1,9 +1,9 @@ package v1.api.client -import github.ainr.tinvest4s.config.access.InvestAccessConfig -import github.ainr.tinvest4s.http.client.InvestApiClient -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient -import github.ainr.tinvest4s.http.client.interpreters.InvestApiSttpClient.InvestApiResponseError +import github.ainr.tinvest4s.v1.config.access.InvestAccessConfig +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.capabilities.WebSockets import sttp.client3.Identity import sttp.client3.testing.SttpBackendStub From e24fabe0078f5c91a0fd8927922713ae3a073298 Mon Sep 17 00:00:00 2001 From: a-khakimov Date: Mon, 17 May 2021 23:41:49 +0500 Subject: [PATCH 09/10] Add logo to readme --- README.md | 4 +++- images/tinvest4s.logo.ico | Bin 0 -> 177470 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 images/tinvest4s.logo.ico diff --git a/README.md b/README.md index 38871aa..167dd2d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ +# 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 + Библиотека предназначена для взаимодействия с [ОpenAPI Тинькофф Инвестиций](https://tinkoffcreditsystems.github.io/invest-openapi/). diff --git a/images/tinvest4s.logo.ico b/images/tinvest4s.logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..3e506e3542334d0f269b12defdeea0a8eb37fff1 GIT binary patch literal 177470 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 Date: Mon, 17 May 2021 23:59:19 +0500 Subject: [PATCH 10/10] Move examples to examples package --- README.md | 10 +++++++ .../CatsBackendExample.scala | 27 ++++++++++--------- .../tinvest4s/v1/test/ZioBackendExample.scala | 1 - 3 files changed, 25 insertions(+), 13 deletions(-) rename modules/core/src/main/scala/github/ainr/tinvest4s/v1/{test => examples}/CatsBackendExample.scala (60%) delete mode 100644 modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala diff --git a/README.md b/README.md index 167dd2d..46d63cb 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,16 @@ Для работы с библиотекой потребуется изучить [документацию](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 | | + ## Подключение библиотеки к проекту В данный момент возможен вариант подключения библиотеки в качестве внешнего проекта. diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/CatsBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/examples/CatsBackendExample.scala similarity index 60% rename from modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/CatsBackendExample.scala rename to modules/core/src/main/scala/github/ainr/tinvest4s/v1/examples/CatsBackendExample.scala index 81032e0..5ae9a1a 100644 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/CatsBackendExample.scala +++ b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/examples/CatsBackendExample.scala @@ -1,4 +1,4 @@ -package github.ainr.tinvest4s.v1.test +package github.ainr.tinvest4s.v1.examples import cats.Monad import cats.effect.{Concurrent, ContextShift, ExitCode, IO, IOApp, Resource, Sync} @@ -13,24 +13,27 @@ 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 - } + .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]] = { + 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) } } -} \ No newline at end of file +} diff --git a/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala b/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala deleted file mode 100644 index 767d090..0000000 --- a/modules/core/src/main/scala/github/ainr/tinvest4s/v1/test/ZioBackendExample.scala +++ /dev/null @@ -1 +0,0 @@ -package github.ainr.tinvest4s.v1.test