├── .gitignore ├── .travis.yml ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ └── microsite │ │ ├── data │ │ └── menu.yml │ │ └── img │ │ ├── first_icon.png │ │ ├── first_icon2x.png │ │ ├── jumbotron_pattern.png │ │ ├── navbar_brand.png │ │ ├── navbar_brand2x.png │ │ ├── second_icon.png │ │ ├── second_icon2x.png │ │ ├── sidebar_brand.png │ │ └── sidebar_brand2x.png ├── scala │ └── alpaca │ │ ├── Alpaca.scala │ │ ├── client │ │ ├── AlpacaClient.scala │ │ ├── AlpacaStreamingClient.scala │ │ ├── BaseStreamingClient.scala │ │ ├── PolygonClient.scala │ │ ├── PolygonStreamingClient.scala │ │ └── StreamingClient.scala │ │ ├── dto │ │ ├── Account.scala │ │ ├── AlpacaAccountConfig.scala │ │ ├── Assets.scala │ │ ├── Calendar.scala │ │ ├── Clock.scala │ │ ├── OHLC.scala │ │ ├── Orders.scala │ │ ├── Position.scala │ │ ├── algebra.scala │ │ ├── polygon │ │ │ ├── HistoricalAggregates.scala │ │ │ ├── Tick.scala │ │ │ ├── Trade.scala │ │ │ └── TradeMap.scala │ │ ├── request │ │ │ └── OrderRequest.scala │ │ └── streaming │ │ │ ├── Authentication.scala │ │ │ ├── StreamingMessage.scala │ │ │ └── request │ │ │ ├── AuthenticationRequest.scala │ │ │ └── StreamingRequest.scala │ │ ├── modules │ │ └── MainModule.scala │ │ └── service │ │ ├── ConfigService.scala │ │ ├── HammockService.scala │ │ └── StreamingService.scala └── tut │ ├── docs │ ├── account.md │ ├── assets.md │ ├── calendar.md │ ├── clock.md │ ├── index.md │ ├── market-data.md │ ├── orders.md │ ├── polygon.md │ ├── positions.md │ └── streaming.md │ └── index.md └── test └── scala └── alpaca ├── AlpacaIntegrationTests.scala ├── AlpacaTest.scala ├── client └── AlpacaClientTest.scala └── service ├── ConfigServiceTest.scala └── HammockServiceTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/sbt,scala,intellij 3 | # Edit at https://www.gitignore.io/?templates=sbt,scala,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | ### Intellij Patch ### 74 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 75 | 76 | # *.iml 77 | # modules.xml 78 | # .idea/misc.xml 79 | # *.ipr 80 | 81 | # Sonarlint plugin 82 | .idea/sonarlint 83 | 84 | ### SBT ### 85 | # Simple Build Tool 86 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 87 | 88 | dist/* 89 | target/ 90 | lib_managed/ 91 | src_managed/ 92 | project/boot/ 93 | project/target/ 94 | project/plugins/project/ 95 | .history 96 | .cache 97 | .lib/ 98 | 99 | ### Scala ### 100 | *.class 101 | *.log 102 | 103 | # End of https://www.gitignore.io/api/sbt,scala,intellij 104 | /runTest.sh 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.12.8 5 | 6 | script: 7 | - sbt clean coverage test 8 | 9 | after_success: 10 | - sbt coverageReport coveralls -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alpaca Scala 2 | 3 | A scala library that interfaces to [alpaca.markets](https://alpaca.markets) 4 | 5 | ![Scala](https://img.shields.io/badge/scala-made--red.svg?logo=scala&style=for-the-badge) 6 | 7 | 8 | [![Build Status](https://travis-ci.org/OUeasley/alpaca-scala.svg?branch=master)](https://travis-ci.org/OUeasley/alpaca-scala) [![Coverage Status](https://coveralls.io/repos/github/OUeasley/alpaca-scala/badge.svg?branch=master)](https://coveralls.io/github/OUeasley/alpaca-scala?branch=master) 9 | 10 | ### Documentation 11 | 12 | Visit [http://www.cynance.com/alpaca-scala/](http://www.cynance.com/alpaca-scala/) for usage. 13 | 14 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "alpaca-scala" 2 | 3 | version := "3.0.0" 4 | 5 | // POM settings for Sonatype 6 | import xerial.sbt.Sonatype._ 7 | organization := "com.cynance" 8 | homepage := Some(url("https://github.com/cynance/alpaca-scala")) 9 | sonatypeProjectHosting := Some(GitHubHosting("cynance", "alpaca-scala", "devs@cynance.com")) 10 | scmInfo := Some(ScmInfo(url("https://github.com/cynance/alpaca-scala"),"git@github.com:cynance/alpaca-scala.git")) 11 | developers := List(Developer("cynance", 12 | "cynance", 13 | "devs@cynance.com", 14 | url("https://github.com/cynance"))) 15 | licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0")) 16 | publishMavenStyle := true 17 | 18 | enablePlugins(MicrositesPlugin) 19 | 20 | // Add sonatype repository settings 21 | publishTo := sonatypePublishTo.value 22 | 23 | scalaVersion := "2.12.8" 24 | 25 | val circeVersion = "0.10.0" 26 | val hammockVersion = "0.8.6" 27 | 28 | libraryDependencies ++= Seq( 29 | "com.pepegar" %% "hammock-core" % hammockVersion, 30 | "com.pepegar" %% "hammock-circe" % hammockVersion, 31 | "com.github.pureconfig" %% "pureconfig" % "0.10.1", 32 | "io.circe" %% "circe-core" % circeVersion, 33 | "io.circe" %% "circe-generic" % circeVersion, 34 | "io.circe" %% "circe-parser" % circeVersion, 35 | "org.scalactic" %% "scalactic" % "3.0.5", 36 | "org.scalatest" %% "scalatest" % "3.0.5" % "test", 37 | "org.scalamock" %% "scalamock" % "4.1.0" % Test, 38 | "com.typesafe.akka" %% "akka-http" % "10.1.7", 39 | "com.typesafe.akka" %% "akka-stream" % "2.5.19", // or whatever the latest version is, 40 | "io.nats" % "jnats" % "2.2.0", 41 | "org.typelevel" %% "cats-core" % "1.5.0", 42 | "ch.qos.logback" % "logback-classic" % "1.2.3", 43 | "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2", 44 | "com.softwaremill.macwire" %% "macros" % "2.3.3" % "provided", 45 | "com.softwaremill.macwire" %% "macrosakka" % "2.3.3" % "provided", 46 | "com.softwaremill.macwire" %% "util" % "2.3.3", 47 | "com.softwaremill.macwire" %% "proxy" % "2.3.3", 48 | "com.beachape" %% "enumeratum" % "1.5.13", 49 | "com.beachape" %% "enumeratum-circe" % "1.5.21", 50 | "org.mockito" %% "mockito-scala-scalatest" % "1.5.12" 51 | ) 52 | 53 | 54 | libraryDependencies += { 55 | val version = scalaBinaryVersion.value match { 56 | case "2.10" => "1.0.3" 57 | case _ ⇒ "1.6.2" 58 | } 59 | "com.lihaoyi" % "ammonite" % version % "test" cross CrossVersion.full 60 | } 61 | 62 | sourceGenerators in Test += Def.task { 63 | val file = (sourceManaged in Test).value / "amm.scala" 64 | IO.write(file, """object amm extends App { ammonite.Main.main(args) }""") 65 | Seq(file) 66 | }.taskValue 67 | 68 | coverageExcludedPackages := ".*ConfigService.*;.*Config.*;alpaca\\.client\\..*" 69 | 70 | coverageEnabled := true 71 | 72 | 73 | //Microsite details 74 | micrositeName := "Alpaca Scala" 75 | micrositeDescription := "A Scala library for alpaca.markets" 76 | micrositeAuthor := "Cynance" 77 | micrositeBaseUrl := "/alpaca-scala" 78 | micrositeDocumentationUrl := "/alpaca-scala/docs" 79 | 80 | micrositePalette := Map( 81 | "brand-primary" -> "#000", 82 | "brand-secondary" -> "#000", 83 | "brand-tertiary" -> "#fcd600", 84 | "gray-dark" -> "#453E46", 85 | "gray" -> "#837F84", 86 | "gray-light" -> "#E3E2E3", 87 | "gray-lighter" -> "#F4F3F4", 88 | "white-color" -> "#FFFFFF") 89 | 90 | micrositePushSiteWith := GitHub4s 91 | 92 | micrositeGithubToken := sys.env.get("GITHUB_TOKEN") 93 | 94 | micrositeGithubOwner := "cynance" 95 | micrositeGithubRepo := "alpaca-scala" -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.2.8 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.5") 2 | 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") 4 | 5 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0") 6 | 7 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.7") 8 | 9 | addSbtPlugin("com.47deg" % "sbt-microsites" % "0.9.2") 10 | -------------------------------------------------------------------------------- /src/main/resources/microsite/data/menu.yml: -------------------------------------------------------------------------------- 1 | options: 2 | - title: Getting Started 3 | url: docs/index.html 4 | 5 | - title: Account 6 | url: docs/account.html 7 | 8 | - title: Orders 9 | url: docs/orders.html 10 | 11 | - title: Positions 12 | url: docs/positions.html 13 | 14 | - title: Assets 15 | url: docs/assets.html 16 | 17 | - title: Calendar 18 | url: docs/calendar.html 19 | 20 | - title: Clock 21 | url: docs/clock.html 22 | 23 | - title: Streaming 24 | url: docs/streaming.html 25 | 26 | - title: Market Data 27 | url: docs/market-data.html 28 | 29 | - title: Polygon 30 | url: docs/polygon.html -------------------------------------------------------------------------------- /src/main/resources/microsite/img/first_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/first_icon.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/first_icon2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/first_icon2x.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/jumbotron_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/jumbotron_pattern.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/navbar_brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/navbar_brand.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/navbar_brand2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/navbar_brand2x.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/second_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/second_icon.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/second_icon2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/second_icon2x.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/sidebar_brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/sidebar_brand.png -------------------------------------------------------------------------------- /src/main/resources/microsite/img/sidebar_brand2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cynance/alpaca-scala/d0dbdff9dbf01c336472b774df8fd8bd5e048c82/src/main/resources/microsite/img/sidebar_brand2x.png -------------------------------------------------------------------------------- /src/main/scala/alpaca/Alpaca.scala: -------------------------------------------------------------------------------- 1 | package alpaca 2 | 3 | import alpaca.client.{AlpacaClient, PolygonClient, StreamingClient} 4 | import alpaca.dto._ 5 | import alpaca.dto.algebra.Bars 6 | import alpaca.dto.polygon.{HistoricalAggregates, Trade} 7 | import alpaca.dto.request.OrderRequest 8 | import alpaca.modules.MainModule 9 | import alpaca.service.ConfigService 10 | import cats.effect.IO 11 | 12 | case class Alpaca(isPaper: Option[Boolean] = None, 13 | accountKey: Option[String] = None, 14 | accountSecret: Option[String] = None) 15 | extends MainModule { 16 | 17 | configService.loadConfig(isPaper, accountKey, accountSecret) 18 | 19 | def getAccount: IO[Account] = { 20 | alpacaClient.getAccount 21 | } 22 | 23 | def getAssets(status: Option[String] = None, 24 | asset_class: Option[String] = None): IO[List[Assets]] = { 25 | alpacaClient.getAssets(status, asset_class) 26 | } 27 | 28 | def getAsset(symbol: String): IO[Assets] = { 29 | alpacaClient.getAsset(symbol) 30 | } 31 | 32 | def getBars(timeframe: String, 33 | symbols: List[String], 34 | limit: Option[String] = None, 35 | start: Option[String] = None, 36 | end: Option[String] = None, 37 | after: Option[String] = None, 38 | until: Option[String] = None): IO[Bars] = { 39 | alpacaClient.getBars(timeframe, symbols, limit, start, end, after, until) 40 | } 41 | 42 | def getCalendar(start: Option[String] = None, 43 | end: Option[String] = None): IO[List[Calendar]] = { 44 | alpacaClient.getCalendar(start, end) 45 | } 46 | 47 | def getClock: IO[Clock] = { 48 | alpacaClient.getClock 49 | } 50 | 51 | def cancelOrder(orderId: String): Unit = { 52 | alpacaClient.cancelOrder(orderId) 53 | } 54 | 55 | def cancelAllOrders(): Unit = { 56 | alpacaClient.cancelAllOrders 57 | } 58 | 59 | def getOrder(orderId: String): IO[Orders] = { 60 | alpacaClient.getOrder(orderId) 61 | } 62 | 63 | def getOrders: IO[List[Orders]] = { 64 | alpacaClient.getOrders 65 | } 66 | 67 | def placeOrder(orderRequest: OrderRequest): IO[Orders] = { 68 | alpacaClient.placeOrder(orderRequest) 69 | } 70 | 71 | def getPositions: IO[List[Position]] = { 72 | alpacaClient.getPositions 73 | } 74 | 75 | def closePosition(symbol: String): IO[Orders] = { 76 | alpacaClient.closePosition(symbol) 77 | } 78 | 79 | def closeAllPositions: IO[Unit] = { 80 | alpacaClient.closeAllPositions() 81 | } 82 | 83 | def getPosition(symbol: String): IO[Position] = { 84 | alpacaClient.getPosition(symbol) 85 | } 86 | 87 | def getHistoricalTrades(symbol: String, 88 | date: String, 89 | offset: Option[Long] = None, 90 | limit: Option[Int] = None): IO[Trade] = { 91 | polygonClient.getHistoricalTrades(symbol, date, offset, limit) 92 | } 93 | 94 | def getHistoricalTradesAggregate( 95 | symbol: String, 96 | size: String, 97 | from: Option[String] = None, 98 | to: Option[String] = None, 99 | limit: Option[Int] = None, 100 | unadjusted: Option[Boolean] = None): IO[HistoricalAggregates] = { 101 | polygonClient.getHistoricalTradesAggregate(symbol, 102 | size, 103 | from, 104 | to, 105 | limit, 106 | unadjusted) 107 | } 108 | 109 | def getStream: StreamingClient = { 110 | streamingClient 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/client/AlpacaClient.scala: -------------------------------------------------------------------------------- 1 | package alpaca.client 2 | 3 | import alpaca.dto._ 4 | import alpaca.dto.algebra.Bars 5 | import alpaca.dto.request.OrderRequest 6 | import alpaca.service.{ConfigService, HammockService, Parameter} 7 | import cats.effect.IO 8 | import com.typesafe.scalalogging.Logger 9 | import hammock._ 10 | import hammock.circe.implicits._ 11 | import hammock.jvm.Interpreter 12 | import hammock.marshalling._ 13 | import io.circe.generic.auto._ 14 | 15 | private[alpaca] class AlpacaClient(configService: ConfigService, 16 | hammockService: HammockService) { 17 | 18 | val logger = Logger(classOf[AlpacaClient]) 19 | 20 | def getAccount: IO[Account] = { 21 | hammockService.execute[Account, Unit]( 22 | Method.GET, 23 | configService.getConfig.value.account_url) 24 | } 25 | 26 | def getAsset(symbol: String): IO[Assets] = { 27 | hammockService.execute[Assets, Unit]( 28 | Method.GET, 29 | s"${configService.getConfig.value.assets_url}/$symbol") 30 | } 31 | 32 | def getAssets(status: Option[String] = None, 33 | asset_class: Option[String] = None): IO[List[Assets]] = { 34 | hammockService.execute[List[Assets], Unit]( 35 | Method.GET, 36 | configService.getConfig.value.assets_url, 37 | None, 38 | hammockService.createTuples(Parameter("status", status), 39 | Parameter("asset_class", asset_class))) 40 | } 41 | 42 | def getBars(timeframe: String, 43 | symbols: List[String], 44 | limit: Option[String] = None, 45 | start: Option[String] = None, 46 | end: Option[String] = None, 47 | after: Option[String] = None, 48 | until: Option[String] = None): IO[Bars] = { 49 | 50 | val url = s"${configService.getConfig.value.bars_url}/$timeframe" 51 | 52 | hammockService.execute[Bars, Unit]( 53 | Method.GET, 54 | url, 55 | None, 56 | hammockService.createTuples( 57 | Parameter("symbols", Some(symbols.mkString(","))), 58 | Parameter("limit", limit), 59 | Parameter("start", start), 60 | Parameter("end", end), 61 | Parameter("after", after), 62 | Parameter("until", until) 63 | ) 64 | ) 65 | } 66 | 67 | def getCalendar(start: Option[String] = None, 68 | end: Option[String] = None): IO[List[Calendar]] = { 69 | hammockService.execute[List[Calendar], Unit]( 70 | Method.GET, 71 | configService.getConfig.value.calendar_url, 72 | None, 73 | hammockService.createTuples(Parameter("start", start), 74 | Parameter("end", end))) 75 | } 76 | 77 | def getClock: IO[Clock] = { 78 | hammockService 79 | .execute[Clock, Unit](Method.GET, configService.getConfig.value.clock_url) 80 | } 81 | 82 | def getOrder(orderId: String): IO[Orders] = { 83 | hammockService.execute[Orders, Unit]( 84 | Method.GET, 85 | s"${configService.getConfig.value.order_url}/$orderId") 86 | } 87 | 88 | def cancelOrder(orderId: String): Unit = { 89 | hammockService.execute[String, Unit]( 90 | Method.DELETE, 91 | s"${configService.getConfig.value.order_url}/$orderId") 92 | } 93 | 94 | def cancelAllOrders = { 95 | hammockService.execute[String, Unit]( 96 | Method.DELETE, 97 | s"${configService.getConfig.value.order_url}") 98 | } 99 | 100 | def getOrders: IO[List[Orders]] = { 101 | hammockService.execute[List[Orders], Unit]( 102 | Method.GET, 103 | configService.getConfig.value.order_url) 104 | } 105 | 106 | def placeOrder(orderRequest: OrderRequest): IO[Orders] = { 107 | hammockService.execute[Orders, OrderRequest]( 108 | Method.POST, 109 | configService.getConfig.value.order_url, 110 | Some(orderRequest)) 111 | } 112 | 113 | def getPositions: IO[List[Position]] = { 114 | hammockService.execute[List[Position], Unit]( 115 | Method.GET, 116 | configService.getConfig.value.positions_url) 117 | } 118 | 119 | def getPosition(symbol: String): IO[Position] = { 120 | hammockService.execute[Position, Unit]( 121 | Method.GET, 122 | s"${configService.getConfig.value.positions_url}/$symbol") 123 | } 124 | 125 | def closePosition(symbol: String): IO[Orders] = { 126 | hammockService.execute[Orders, Unit]( 127 | Method.DELETE, 128 | s"${configService.getConfig.value.positions_url}/$symbol") 129 | } 130 | 131 | def closeAllPositions(): IO[Unit] = { 132 | hammockService.execute[Unit, Unit]( 133 | Method.DELETE, 134 | s"${configService.getConfig.value.positions_url}") 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/client/AlpacaStreamingClient.scala: -------------------------------------------------------------------------------- 1 | package alpaca.client 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.ws.{ 6 | BinaryMessage, 7 | TextMessage, 8 | WebSocketRequest, 9 | WebSocketUpgradeResponse, 10 | Message => WSMessage 11 | } 12 | import akka.stream.scaladsl.{Flow, Sink, Source, SourceQueueWithComplete} 13 | import akka.stream.{ActorMaterializer, OverflowStrategy} 14 | import akka.{Done, NotUsed} 15 | import alpaca.dto.streaming.{ 16 | ClientStreamMessage, 17 | StreamMessage, 18 | StreamingMessage 19 | } 20 | import alpaca.service.{ConfigService, StreamingService} 21 | import com.typesafe.scalalogging.Logger 22 | import io.circe.generic.auto._ 23 | import io.circe.syntax._ 24 | import io.circe._ 25 | import io.circe.parser._ 26 | import io.nats.client._ 27 | import cats._ 28 | import cats.implicits._ 29 | import alpaca.dto.streaming.Alpaca._ 30 | 31 | import scala.concurrent.{Future, Promise} 32 | import scala.util.{Failure, Success} 33 | import scala.concurrent.ExecutionContext.Implicits.global 34 | 35 | class AlpacaStreamingClient(configService: ConfigService, 36 | streamingService: StreamingService) 37 | extends BaseStreamingClient { 38 | 39 | val logger = Logger(classOf[AlpacaStreamingClient]) 40 | 41 | private val messageList = 42 | scala.collection.mutable.ListBuffer.empty[AlpacaClientStreamMessage] 43 | 44 | override def wsUrl: String = 45 | configService.getConfig.value.base_url 46 | .replace("https", "wss") 47 | .replace("http", "wss") + "/stream" 48 | 49 | private val incoming: Sink[WSMessage, Future[Done]] = 50 | Sink.foreach[WSMessage] { 51 | case message: BinaryMessage.Strict => 52 | val msg = message.data.utf8String 53 | logger.info(msg) 54 | for { 55 | parsed <- parse(message.data.utf8String) 56 | alpacaMessage <- parsed.as[AlpacaAckMessage] 57 | decodedMessage <- streamingService.decodeAlpacaMessage(parsed, 58 | alpacaMessage) 59 | _ <- checkAuthentication(List(decodedMessage)) 60 | _ <- offerMessage(List(decodedMessage)) 61 | } yield decodedMessage 62 | case message: TextMessage.Strict => 63 | logger.info(message.toString()) 64 | 65 | } 66 | 67 | private val clientSource: SourceQueueWithComplete[WSMessage] = 68 | streamingService.createClientSource(wsUrl, incoming) 69 | 70 | authPromise.future.onComplete { 71 | case Failure(exception) => 72 | logger.error(exception.toString) 73 | case Success(value) => 74 | messageList.foreach(msg => { 75 | import alpaca.dto.streaming.Alpaca._ 76 | logger.debug(msg.toString) 77 | clientSource.offer(TextMessage(msg.asJson.noSpaces)) 78 | }) 79 | } 80 | 81 | private def checkAuthentication(message: List[AlpacaStreamMessage]) 82 | : Either[String, List[AlpacaStreamMessage]] = { 83 | if (!authPromise.isCompleted) { 84 | message.head match { 85 | case polygonStreamAuthenticationMessage: AlpacaAuthorizationMessage => 86 | if (polygonStreamAuthenticationMessage.data.status.equalsIgnoreCase( 87 | "authorized")) { 88 | authPromise.completeWith(Future.successful(true)) 89 | } 90 | case _ => 91 | } 92 | } 93 | message.asRight 94 | } 95 | 96 | def subscribe( 97 | subject: AlpacaClientStreamMessage): (SourceQueueWithComplete[ 98 | StreamMessage], 99 | Source[StreamMessage, NotUsed]) = { 100 | 101 | if (authPromise.isCompleted) { 102 | clientSource.offer(TextMessage(subject.asJson.noSpaces)) 103 | } else { 104 | val str = AlpacaAuthenticate( 105 | configService.getConfig.value.accountKey, 106 | configService.getConfig.value.accountSecret).asJson.noSpaces 107 | clientSource.offer(TextMessage(str)) 108 | messageList += subject 109 | } 110 | 111 | source 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/client/BaseStreamingClient.scala: -------------------------------------------------------------------------------- 1 | package alpaca.client 2 | 3 | import akka.NotUsed 4 | import akka.actor.ActorSystem 5 | import akka.stream.{ActorMaterializer, OverflowStrategy} 6 | import akka.stream.scaladsl.{Source, SourceQueueWithComplete} 7 | import alpaca.dto.streaming.Polygon.PolygonClientStreamMessage 8 | import alpaca.dto.streaming.{ 9 | ClientStreamMessage, 10 | StreamMessage, 11 | StreamingMessage 12 | } 13 | import enumeratum.{CirceEnum, Enum, EnumEntry} 14 | import cats._ 15 | import cats.implicits._ 16 | 17 | import scala.concurrent.Promise 18 | 19 | trait BaseStreamingClient { 20 | implicit val system: ActorSystem = ActorSystem() 21 | implicit val materializer: ActorMaterializer = ActorMaterializer() 22 | val authPromise: Promise[Boolean] = Promise[Boolean]() 23 | 24 | def wsUrl: String = { 25 | "wss://alpaca.socket.polygon.io/stocks" 26 | } 27 | 28 | val source 29 | : (SourceQueueWithComplete[StreamMessage], Source[StreamMessage, NotUsed]) = 30 | Source 31 | .queue[StreamMessage](bufferSize = 1000, OverflowStrategy.backpressure) 32 | .log("error logging") 33 | .preMaterialize() 34 | 35 | def offerMessage( 36 | message: List[StreamMessage]): Either[String, List[StreamMessage]] = { 37 | message.foreach(source._1.offer) 38 | message.asRight 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/client/PolygonClient.scala: -------------------------------------------------------------------------------- 1 | package alpaca.client 2 | 3 | import alpaca.dto.polygon.{HistoricalAggregates, Trade} 4 | import alpaca.service.{ConfigService, HammockService, Parameter} 5 | import cats.effect.IO 6 | import hammock.circe.implicits._ 7 | import hammock.jvm.Interpreter 8 | import hammock.marshalling._ 9 | import hammock.{Hammock, Method, UriInterpolator, _} 10 | import io.circe.generic.auto._ 11 | import cats._ 12 | import cats.implicits._ 13 | 14 | import scala.collection.mutable.ListBuffer 15 | 16 | class PolygonClient(configService: ConfigService, 17 | hammockService: HammockService) { 18 | private implicit val interpreter: Interpreter[IO] = Interpreter[IO] 19 | 20 | def getHistoricalTrades(symbol: String, 21 | date: String, 22 | offset: Option[Long] = None, 23 | limit: Option[Int] = None): IO[Trade] = { 24 | 25 | hammockService.execute[Trade, Unit]( 26 | Method.GET, 27 | s"${configService.getConfig.value.basePolygonUrl}/v1/historic/trades/$symbol/$date", 28 | None, 29 | hammockService.createTuples( 30 | Parameter("apiKey", configService.getConfig.value.accountKey.some), 31 | Parameter("offset", offset), 32 | Parameter("limit", limit)) 33 | ) 34 | } 35 | 36 | def getHistoricalTradesAggregate( 37 | symbol: String, 38 | size: String, 39 | from: Option[String] = None, 40 | to: Option[String] = None, 41 | limit: Option[Int] = None, 42 | unadjusted: Option[Boolean] = None): IO[HistoricalAggregates] = { 43 | hammockService.execute[HistoricalAggregates, Unit]( 44 | Method.GET, 45 | s"${configService.getConfig.value.basePolygonUrl}/v1/historic/agg/$size/$symbol", 46 | None, 47 | hammockService.createTuples( 48 | Parameter("apiKey", configService.getConfig.value.accountKey.some), 49 | Parameter("from", from), 50 | Parameter("to", to), 51 | Parameter("limit", limit)) 52 | ) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/client/PolygonStreamingClient.scala: -------------------------------------------------------------------------------- 1 | package alpaca.client 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.ws.{ 6 | BinaryMessage, 7 | TextMessage, 8 | WebSocketRequest, 9 | WebSocketUpgradeResponse, 10 | Message => WSMessage 11 | } 12 | import akka.stream.scaladsl.{Flow, Sink, Source, SourceQueueWithComplete} 13 | import akka.stream.{ActorMaterializer, OverflowStrategy} 14 | import akka.{Done, NotUsed} 15 | import alpaca.dto.streaming.{ 16 | ClientStreamMessage, 17 | StreamMessage, 18 | StreamingMessage 19 | } 20 | import alpaca.dto.streaming.Polygon._ 21 | import alpaca.dto.streaming.request.{ 22 | AuthenticationRequest, 23 | AuthenticationRequestData, 24 | StreamingData, 25 | StreamingRequest 26 | } 27 | import alpaca.service.{ConfigService, StreamingService} 28 | import com.typesafe.scalalogging.Logger 29 | import io.circe.generic.auto._ 30 | import io.circe.syntax._ 31 | import io.circe._ 32 | import io.circe.parser._ 33 | import io.nats.client._ 34 | import cats._ 35 | import cats.implicits._ 36 | 37 | import scala.concurrent.{Future, Promise} 38 | import scala.util.{Failure, Success} 39 | import scala.concurrent.ExecutionContext.Implicits.global 40 | 41 | class PolygonStreamingClient(configService: ConfigService, 42 | streamingService: StreamingService) 43 | extends BaseStreamingClient { 44 | 45 | val logger = Logger(classOf[PolygonStreamingClient]) 46 | 47 | val messageList = 48 | scala.collection.mutable.ListBuffer.empty[PolygonClientStreamMessage] 49 | 50 | val incoming: Sink[WSMessage, Future[Done]] = 51 | Sink.foreach[WSMessage] { 52 | case message: TextMessage.Strict => 53 | for { 54 | parsedJson <- parse(message.text) 55 | polygonStreamMessage <- parsedJson 56 | .as[List[PolygonStreamBasicMessage]] 57 | decodeBasicMessage <- streamingService.decodePolygonMessage( 58 | parsedJson, 59 | polygonStreamMessage.head) 60 | _ <- checkAuthentication(decodeBasicMessage) 61 | _ <- offerMessage(decodeBasicMessage) 62 | } yield decodeBasicMessage 63 | } 64 | 65 | val clientSource: SourceQueueWithComplete[WSMessage] = 66 | streamingService.createClientSource(wsUrl, incoming) 67 | 68 | authPromise.future.onComplete { 69 | case Failure(exception) => 70 | logger.error(exception.toString) 71 | case Success(value) => 72 | messageList.foreach(msg => { 73 | logger.debug(msg.toString) 74 | clientSource.offer(TextMessage(msg.asJson.noSpaces)) 75 | }) 76 | } 77 | 78 | private def checkAuthentication(message: List[PolygonStreamMessage]) 79 | : Either[String, List[PolygonStreamMessage]] = { 80 | if (!authPromise.isCompleted) { 81 | message.head match { 82 | case polygonStreamAuthenticationMessage: PolygonStreamAuthenticationMessage => 83 | if (polygonStreamAuthenticationMessage.status.equalsIgnoreCase( 84 | "auth_success")) { 85 | authPromise.completeWith(Future.successful(true)) 86 | } 87 | case _ => 88 | } 89 | } 90 | message.asRight 91 | } 92 | 93 | def subscribe( 94 | subject: PolygonClientStreamMessage): (SourceQueueWithComplete[ 95 | StreamMessage], 96 | Source[StreamMessage, NotUsed]) = { 97 | 98 | if (authPromise.isCompleted) { 99 | clientSource.offer(TextMessage(subject.asJson.noSpaces)) 100 | } else { 101 | clientSource.offer(TextMessage(PolygonAuthMessage( 102 | configService.getConfig.value.accountKey).asJson.noSpaces)) 103 | messageList += subject 104 | } 105 | 106 | source 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/client/StreamingClient.scala: -------------------------------------------------------------------------------- 1 | package alpaca.client 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.ws.{ 6 | BinaryMessage, 7 | TextMessage, 8 | WebSocketRequest, 9 | WebSocketUpgradeResponse, 10 | Message => WSMessage 11 | } 12 | import akka.stream.scaladsl.{Flow, Sink, Source, SourceQueueWithComplete} 13 | import akka.stream.{ActorMaterializer, OverflowStrategy} 14 | import akka.{Done, NotUsed} 15 | import alpaca.dto.streaming.Polygon.PolygonAuthMessage 16 | import alpaca.dto.streaming.{ 17 | Alpaca, 18 | ClientStreamMessage, 19 | Polygon, 20 | StreamingMessage 21 | } 22 | import alpaca.dto.streaming.request.{ 23 | AuthenticationRequest, 24 | AuthenticationRequestData, 25 | StreamingData, 26 | StreamingRequest 27 | } 28 | import alpaca.service.ConfigService 29 | import io.circe.generic.auto._ 30 | import io.circe.syntax._ 31 | import io.nats.client._ 32 | 33 | import scala.concurrent.Future 34 | 35 | class StreamingClient(polygonStreamingClient: PolygonStreamingClient, 36 | alpacaStreamingClient: AlpacaStreamingClient) { 37 | 38 | def subscribe(list: ClientStreamMessage*) = { 39 | list.map { 40 | case message: Polygon.PolygonClientStreamMessage => 41 | polygonStreamingClient.subscribe(message) 42 | case message: Alpaca.AlpacaClientStreamMessage => 43 | alpacaStreamingClient.subscribe(message) 44 | } 45 | } 46 | 47 | def sub(list: List[String]): Map[String, 48 | (SourceQueueWithComplete[StreamingMessage], 49 | Source[StreamingMessage, NotUsed])] = { 50 | null 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/Account.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | case class Account(id: String, 4 | status: String, 5 | currency: String, 6 | buying_power: String, 7 | cash: String, 8 | portfolio_value: String, 9 | pattern_day_trader: Boolean, 10 | trading_blocked: Boolean, 11 | transfers_blocked: Boolean, 12 | account_blocked: Boolean, 13 | created_at: String, 14 | trade_suspended_by_user: Boolean, 15 | shorting_enabled: Boolean, 16 | multiplier: String, 17 | long_market_value: String, 18 | short_market_value: String, 19 | equity: String, 20 | last_equity: String, 21 | initial_margin: String, 22 | maintenance_margin: String, 23 | daytrade_count: Int, 24 | sma: String) 25 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/AlpacaAccountConfig.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | case class AlpacaAccountConfig(accountKey: String, 4 | accountSecret: String, 5 | isPaper: Boolean) 6 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/Assets.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | case class Assets( 4 | id: String, 5 | asset_class: Option[String], 6 | exchange: String, 7 | symbol: String, 8 | status: String, 9 | tradable: Boolean, 10 | marginable: Boolean, 11 | shortable: Boolean, 12 | easy_to_borrow: Boolean, 13 | ) 14 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/Calendar.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | case class Calendar( 4 | date: String, 5 | open: String, 6 | close: String 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/Clock.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | case class Clock( 4 | timestamp: String, 5 | is_open: Boolean, 6 | next_open: String, 7 | next_close: String 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/OHLC.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | case class OHLC(t: Double, 4 | o: Double, 5 | h: Double, 6 | l: Double, 7 | c: Double, 8 | v: Double) 9 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/Orders.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | import enumeratum._ 4 | 5 | import scala.collection.immutable 6 | 7 | sealed trait Side extends EnumEntry 8 | 9 | case object Side extends Enum[Side] with CirceEnum[Side] { 10 | case object buy extends Side 11 | case object sell extends Side 12 | val values: immutable.IndexedSeq[Side] = findValues 13 | } 14 | 15 | sealed trait Type extends EnumEntry 16 | 17 | case object Type extends Enum[Type] with CirceEnum[Type] { 18 | case object market extends Type 19 | case object limit extends Type 20 | case object stop extends Type 21 | case object stop_limit extends Type 22 | val values: immutable.IndexedSeq[Type] = findValues 23 | } 24 | 25 | /** 26 | * Alpaca supports the following Time-In-Force designations: 27 | * 28 | * day 29 | * A day order is eligible for execution only on the day it is live. By default, the order is only valid during Regular Trading Hours (9:30am - 4:00pm ET). If unfilled after the closing auction, it is automatically canceled. If submitted after the close, it is queued and submitted the following trading day. However, if marked as eligible for extended hours, the order can also execute during supported extended hours. 30 | * gtc 31 | * The order is good until canceled. Non-marketable GTC limit orders are subject to price adjustments to offset corporate actions affecting the issue. We do not currently support Do Not Reduce(DNR) orders to opt out of such price adjustments. 32 | * opg 33 | * Use this TIF with a market/limit order type to submit “market on open” (MOO) and “limit on open” (LOO) orders. This order is eligible to execute only in the market opening auction. Any unfilled orders after the open will be cancelled. OPG orders submitted after 9:28am but before 7:00pm ET will be rejected. OPG orders submitted after 7:00pm will be queued and routed to the following day’s opening auction. 34 | * cls 35 | * Use this TIF with a market/limit order type to submit “market on close” (MOC) and “limit on close” (LOC) orders. This order is eligible to execute only in the market closing auction. Any unfilled orders after the close will be cancelled. CLS orders submitted after 3:50pm but before 7:00pm ET will be rejected. CLS orders submitted after 7:00pm will be queued and routed to the following day’s closing auction. Only available with API v2. 36 | * ioc 37 | * An Immediate Or Cancel (IOC) order requires all or part of the order to be executed immediately. Any unfilled portion of the order is canceled. Only available with API v2. 38 | * fok 39 | * A Fill or Kill (FOK) order is only executed if the entire order quantity can be filled, otherwise the order is canceled. Only available with API v2. 40 | */ 41 | sealed trait TimeInForce extends EnumEntry 42 | 43 | case object TimeInForce extends Enum[TimeInForce] with CirceEnum[TimeInForce] { 44 | case object day extends TimeInForce 45 | case object gtc extends TimeInForce 46 | case object opg extends TimeInForce 47 | case object cls extends TimeInForce 48 | case object ioc extends TimeInForce 49 | case object fok extends TimeInForce 50 | val values: immutable.IndexedSeq[TimeInForce] = findValues 51 | } 52 | 53 | sealed trait OrderStatus extends EnumEntry 54 | 55 | case object OrderStatus extends Enum[OrderStatus] with CirceEnum[OrderStatus] { 56 | case object `new` extends OrderStatus 57 | case object partially_filled extends OrderStatus 58 | case object filled extends OrderStatus 59 | case object done_for_day extends OrderStatus 60 | case object canceled extends OrderStatus 61 | case object expired extends OrderStatus 62 | case object accepted extends OrderStatus 63 | case object pending_new extends OrderStatus 64 | case object accepted_for_bidding extends OrderStatus 65 | case object pending_cancel extends OrderStatus 66 | case object stopped extends OrderStatus 67 | case object rejected extends OrderStatus 68 | case object suspended extends OrderStatus 69 | case object calculated extends OrderStatus 70 | val values: immutable.IndexedSeq[OrderStatus] = findValues 71 | } 72 | 73 | case class Orders( 74 | id: String, 75 | client_order_id: String, 76 | created_at: String, 77 | updated_at: String, 78 | submitted_at: String, 79 | filled_at: Option[String], 80 | expired_at: Option[String], 81 | canceled_at: Option[String], 82 | failed_at: Option[String], 83 | asset_id: String, 84 | symbol: String, 85 | exchange: Option[String], 86 | asset_class: String, 87 | qty: String, 88 | filled_qty: String, 89 | `type`: Type, 90 | side: Side, 91 | time_in_force: TimeInForce, 92 | limit_price: Option[String], 93 | stop_price: Option[String], 94 | filled_avg_price: Option[String], 95 | status: OrderStatus 96 | ) 97 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/Position.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | 3 | case class Position( 4 | asset_id: String, 5 | symbol: String, 6 | exchange: String, 7 | asset_class: String, 8 | avg_entry_price: String, 9 | qty: String, 10 | side: String, 11 | market_value: String, 12 | cost_basis: String, 13 | unrealized_pl: String, 14 | unrealized_plpc: String, 15 | unrealized_intraday_pl: String, 16 | unrealized_intraday_plpc: String, 17 | current_price: String, 18 | lastday_price: String, 19 | change_today: String 20 | ) 21 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/algebra.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto 2 | import cats.syntax.functor._ 3 | import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder} 4 | import io.circe.generic.auto._ 5 | import io.circe.syntax._ 6 | 7 | object algebra { 8 | type Bars = Map[String, List[OHLC]] 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/polygon/HistoricalAggregates.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.polygon 2 | 3 | case class HistoricalAggregates(map: HistoricalAggregateMap, 4 | status: Option[String], 5 | aggType: String, 6 | symbol: String, 7 | ticks: List[HistoricalOHLC]) 8 | case class HistoricalOHLC(o: Double, 9 | c: Double, 10 | h: Double, 11 | l: Double, 12 | v: Int, 13 | k: Option[Int], 14 | t: Long, 15 | d: Long) 16 | case class HistoricalAggregateMap(a: Option[String], 17 | c: String, 18 | h: String, 19 | k: Option[String], 20 | l: String, 21 | o: String, 22 | d: String, 23 | t: String, 24 | v: String) 25 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/polygon/Tick.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.polygon 2 | 3 | case class Tick(c1: Int, 4 | c2: Int, 5 | c3: Int, 6 | c4: Int, 7 | e: String, 8 | p: Double, 9 | t: Long) {} 10 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/polygon/Trade.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.polygon 2 | 3 | case class Trade(day: String, 4 | map: TradeMap, 5 | msLatency: Int, 6 | status: String, 7 | symbol: String, 8 | ticks: List[Tick], 9 | `type`: String) {} 10 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/polygon/TradeMap.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.polygon 2 | 3 | case class TradeMap(c1: String, 4 | c2: String, 5 | c3: String, 6 | c4: String, 7 | e: String, 8 | p: String, 9 | s: String, 10 | t: String) {} 11 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/request/OrderRequest.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.request 2 | 3 | case class OrderRequest(symbol: String, 4 | qty: String, 5 | side: String, 6 | `type`: String, 7 | time_in_force: String, 8 | limit_price: Option[String] = None, 9 | stop_price: Option[String] = None, 10 | client_order_id: Option[String] = None) 11 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/streaming/Authentication.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.streaming 2 | 3 | case class AuthenticationData(status: String, action: String) 4 | case class Authentication(stream: String, data: AuthenticationData) 5 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/streaming/StreamingMessage.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.streaming 2 | 3 | import alpaca.dto.Orders 4 | import alpaca.dto.streaming.Polygon.{ 5 | PolygonAggregatePerSecondSubscribe, 6 | PolygonClientStreamMessage 7 | } 8 | import enumeratum._ 9 | import io.circe.{Encoder, Json} 10 | import io.circe.syntax._ 11 | import io.circe._ 12 | import io.circe.parser._ 13 | 14 | sealed trait StreamMessage 15 | 16 | sealed trait ClientStreamMessage 17 | 18 | case class StreamingMessage(subject: String, data: String) 19 | 20 | object Polygon { 21 | sealed trait Ev extends EnumEntry 22 | 23 | case object Ev extends Enum[Ev] with CirceEnum[Ev] { 24 | 25 | case object T extends Ev 26 | case object Q extends Ev 27 | case object A extends Ev 28 | case object AM extends Ev 29 | case object status extends Ev 30 | 31 | val values = findValues 32 | 33 | } 34 | 35 | sealed trait PolygonStreamMessage extends StreamMessage 36 | case class PolygonStreamTradeMessage(ev: Ev, 37 | sym: String, 38 | x: Int, 39 | p: Double, 40 | s: Int, 41 | c: Array[Int], 42 | t: Long) 43 | extends PolygonStreamMessage 44 | 45 | case class PolygonStreamQuoteMessage( 46 | ev: String, // Event Type 47 | sym: String, // Symbol Ticker 48 | bx: Int, // Bix Exchange ID 49 | bp: Double, // Bid Price 50 | bs: Int, // Bid Size 51 | ax: Int, // Ask Exchange ID 52 | ap: Double, // Ask Price 53 | as: Int, // Ask Size 54 | c: Int, // Quote Condition 55 | t: Long // Quote Timestamp ( Unix MS ) 56 | ) extends PolygonStreamMessage 57 | 58 | case class PolygonStreamAggregatePerMinute( 59 | ev: String, // Event Type ( A = Second Agg, AM = Minute Agg ) 60 | sym: String, // Symbol Ticker 61 | v: Int, // Tick Volume 62 | av: Int, // Accumlated Volume ( Today ) 63 | op: Double, // Todays official opening price 64 | vw: Double, // VWAP (Volume Weighted Average Price) 65 | o: Double, // Tick Open Price 66 | c: Double, // Tick Close Price 67 | h: Double, // Tick High Price 68 | l: Double, // Tick Low Price 69 | a: Double, // Tick Average / VWAP Price 70 | s: Long, // Tick Start Timestamp ( Unix MS ) 71 | e: Long // Tick End Timestamp ( Unix MS )) {} 72 | ) extends PolygonStreamMessage 73 | 74 | case class PolygonStreamAggregatePerSecond( 75 | ev: String, // Event Type ( A = Second Agg, AM = Minute Agg ) 76 | sym: String, // Symbol Ticker 77 | v: Int, // Tick Volume 78 | av: Int, // Accumlated Volume ( Today ) 79 | op: Double, // Todays official opening price 80 | vw: Double, // VWAP (Volume Weighted Average Price) 81 | o: Double, // Tick Open Price 82 | c: Double, // Tick Close Price 83 | h: Double, // Tick High Price 84 | l: Double, // Tick Low Price 85 | a: Double, // Tick Average / VWAP Price 86 | s: Long, // Tick Start Timestamp ( Unix MS ) 87 | e: Long // Tick End Timestamp ( Unix MS )) {} 88 | ) extends PolygonStreamMessage 89 | 90 | case class PolygonStreamBasicMessage(ev: Ev) extends PolygonStreamMessage 91 | 92 | case class PolygonStreamAuthenticationMessage(ev: Ev, 93 | status: String, 94 | message: String) 95 | extends PolygonStreamMessage 96 | 97 | //outgoing messages 98 | sealed trait PolygonClientStreamMessage extends ClientStreamMessage 99 | case class PolygonTradeSubscribe(symbol: String) 100 | extends PolygonClientStreamMessage 101 | case class PolygonQuoteSubscribe(symbol: String) 102 | extends PolygonClientStreamMessage 103 | case class PolygonAggregatePerMinuteSubscribe(symbol: String) 104 | extends PolygonClientStreamMessage 105 | case class PolygonAggregatePerSecondSubscribe(symbol: String) 106 | extends PolygonClientStreamMessage 107 | case class PolygonAuthMessage(key: String) extends PolygonClientStreamMessage 108 | 109 | implicit val encodePolygonTradeSubscribe: Encoder[PolygonTradeSubscribe] = 110 | new Encoder[PolygonTradeSubscribe] { 111 | override def apply(a: PolygonTradeSubscribe): Json = Json.obj( 112 | ("action", Json.fromString("subscribe")), 113 | ("params", Json.fromString(s"T.${a.symbol}")) 114 | ) 115 | } 116 | 117 | implicit val encodePolygonQuoteSubscribe: Encoder[PolygonQuoteSubscribe] = 118 | new Encoder[PolygonQuoteSubscribe] { 119 | override def apply(a: PolygonQuoteSubscribe): Json = Json.obj( 120 | ("action", Json.fromString("subscribe")), 121 | ("params", Json.fromString(s"Q.${a.symbol}")) 122 | ) 123 | } 124 | 125 | implicit val encodePolygonAggregatePerMinuteSubscribe 126 | : Encoder[PolygonAggregatePerMinuteSubscribe] = 127 | new Encoder[PolygonAggregatePerMinuteSubscribe] { 128 | override def apply(a: PolygonAggregatePerMinuteSubscribe): Json = 129 | Json.obj( 130 | ("action", Json.fromString("subscribe")), 131 | ("params", Json.fromString(s"AM.${a.symbol}")) 132 | ) 133 | } 134 | 135 | implicit val encodePolygonAggregatePerSecondSubscribe 136 | : Encoder[PolygonAggregatePerSecondSubscribe] = 137 | new Encoder[PolygonAggregatePerSecondSubscribe] { 138 | override def apply(a: PolygonAggregatePerSecondSubscribe): Json = 139 | Json.obj( 140 | ("action", Json.fromString("subscribe")), 141 | ("params", Json.fromString(s"A.${a.symbol}")) 142 | ) 143 | } 144 | 145 | implicit val encodePolygonAuthMessage: Encoder[PolygonAuthMessage] = 146 | new Encoder[PolygonAuthMessage] { 147 | override def apply(a: PolygonAuthMessage): Json = 148 | Json.obj( 149 | ("action", Json.fromString("auth")), 150 | ("params", Json.fromString(a.key)) 151 | ) 152 | } 153 | 154 | implicit val PolygonClientStreamMessage: Encoder[PolygonClientStreamMessage] = 155 | new Encoder[PolygonClientStreamMessage] { 156 | override def apply(a: PolygonClientStreamMessage): Json = { 157 | a match { 158 | case t: PolygonTradeSubscribe => t.asJson 159 | case q: PolygonQuoteSubscribe => q.asJson 160 | case am: PolygonAggregatePerMinuteSubscribe => am.asJson 161 | case a: PolygonAggregatePerSecondSubscribe => a.asJson 162 | case aum: PolygonAuthMessage => aum.asJson 163 | } 164 | } 165 | } 166 | } 167 | 168 | object Alpaca { 169 | 170 | sealed trait Stream extends EnumEntry 171 | 172 | case object Stream extends Enum[Stream] with CirceEnum[Stream] { 173 | 174 | case object account_updates extends Stream 175 | case object trade_updates extends Stream 176 | case object listening extends Stream 177 | case object authorization extends Stream 178 | 179 | val values = findValues 180 | 181 | } 182 | 183 | //traits 184 | sealed trait AlpacaClientStreamMessage extends ClientStreamMessage 185 | sealed trait AlpacaStreamMessage extends StreamMessage 186 | 187 | //incoming messages 188 | 189 | case class AlpacaStreamArray(streams: Array[String]) 190 | case class AlpacaAckMessage(stream: Stream) extends AlpacaStreamMessage 191 | case class AlpacaTradeUpdateData(event: String, 192 | qty: String, 193 | price: String, 194 | timestamp: String, 195 | order: Orders) 196 | case class AlpacaAccountUpdateDate(id: String, 197 | created_at: String, 198 | updated_at: String, 199 | deleted_at: String, 200 | status: String, 201 | currency: String, 202 | cash: String, 203 | cash_withdrawable: String) 204 | case class AlpacaAccountUpdate(stream: String, data: AlpacaAccountUpdateDate) 205 | extends AlpacaStreamMessage 206 | case class AlpacaTradeUpdate(stream: String, data: AlpacaTradeUpdateData) 207 | extends AlpacaStreamMessage 208 | case class AlpacaAuthorizationData(status: String, action: String) 209 | case class AlpacaAuthorizationMessage(stream: String, 210 | data: AlpacaAuthorizationData) 211 | extends AlpacaStreamMessage 212 | case class AlpacaListenMessageData(streams: Array[String]) 213 | case class AlpacaListenMessage(stream: String, data: AlpacaListenMessageData) 214 | extends AlpacaStreamMessage 215 | 216 | //outgoing messages 217 | case class AlpacaAccountUpdatesSubscribe() extends AlpacaClientStreamMessage 218 | case class AlpacaTradeUpdatesSubscribe() extends AlpacaClientStreamMessage 219 | case class AlpacaAccountAndTradeUpdates() extends AlpacaClientStreamMessage 220 | case class AlpacaAuthenticate(key_id: String, secret_key: String) 221 | extends AlpacaClientStreamMessage 222 | 223 | implicit val encodeAlpacaAuthenticate: Encoder[AlpacaAuthenticate] = 224 | new Encoder[AlpacaAuthenticate] { 225 | override def apply(a: AlpacaAuthenticate): Json = 226 | Json.obj( 227 | ("action", Json.fromString("authenticate")), 228 | ("data", 229 | Json.obj( 230 | ("key_id", Json.fromString(a.key_id)), 231 | ("secret_key", Json.fromString(a.secret_key)) 232 | )) 233 | ) 234 | } 235 | 236 | implicit val encodeAlpacaTradeUpdatesSubscribe 237 | : Encoder[AlpacaTradeUpdatesSubscribe] = 238 | new Encoder[AlpacaTradeUpdatesSubscribe] { 239 | override def apply(a: AlpacaTradeUpdatesSubscribe): Json = 240 | Json.obj( 241 | ("action", Json.fromString("listen")), 242 | ("data", 243 | Json.obj( 244 | ("streams", Json.arr(Json.fromString("trade_updates"))) 245 | )) 246 | ) 247 | } 248 | 249 | implicit val encodeAlpacaAccountUpdatesSubscribe 250 | : Encoder[AlpacaAccountUpdatesSubscribe] = 251 | new Encoder[AlpacaAccountUpdatesSubscribe] { 252 | override def apply(a: AlpacaAccountUpdatesSubscribe): Json = 253 | Json.obj( 254 | ("action", Json.fromString("listen")), 255 | ("data", 256 | Json.obj( 257 | ("streams", Json.arr(Json.fromString("account_updates"))) 258 | )) 259 | ) 260 | } 261 | 262 | implicit val encodeAlpacaAccountAndTradeUpdates 263 | : Encoder[AlpacaAccountAndTradeUpdates] = 264 | new Encoder[AlpacaAccountAndTradeUpdates] { 265 | override def apply(a: AlpacaAccountAndTradeUpdates): Json = 266 | Json.obj( 267 | ("action", Json.fromString("listen")), 268 | ("data", 269 | Json.obj( 270 | ("streams", 271 | Json.arr(Json.fromString("account_updates"), 272 | Json.fromString("trade_updates"))) 273 | )) 274 | ) 275 | } 276 | 277 | implicit val AlpacaClientStreamMessage: Encoder[AlpacaClientStreamMessage] = 278 | new Encoder[AlpacaClientStreamMessage] { 279 | override def apply(a: AlpacaClientStreamMessage): Json = { 280 | a match { 281 | case msg: AlpacaAccountUpdatesSubscribe => msg.asJson 282 | case msg: AlpacaTradeUpdatesSubscribe => msg.asJson 283 | case msg: AlpacaAccountAndTradeUpdates => msg.asJson 284 | case msg: AlpacaAuthenticate => msg.asJson 285 | } 286 | } 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/streaming/request/AuthenticationRequest.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.streaming.request 2 | 3 | case class AuthenticationRequestData(key_id: String, secret_key: String) 4 | case class AuthenticationRequest(action: String = "authenticate", 5 | data: AuthenticationRequestData) 6 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/dto/streaming/request/StreamingRequest.scala: -------------------------------------------------------------------------------- 1 | package alpaca.dto.streaming.request 2 | 3 | case class StreamingData(streams: Array[String]) 4 | case class StreamingRequest(action: String, data: StreamingData) 5 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/modules/MainModule.scala: -------------------------------------------------------------------------------- 1 | package alpaca.modules 2 | 3 | import alpaca.client.{ 4 | AlpacaClient, 5 | AlpacaStreamingClient, 6 | PolygonClient, 7 | PolygonStreamingClient, 8 | StreamingClient 9 | } 10 | import alpaca.service.{ConfigService, HammockService, StreamingService} 11 | 12 | trait MainModule { 13 | import com.softwaremill.macwire._ 14 | //Services 15 | lazy val configService: ConfigService = wire[ConfigService] 16 | lazy val hammockService: HammockService = wire[HammockService] 17 | lazy val streamingService: StreamingService = wire[StreamingService] 18 | 19 | //Clients 20 | lazy val polygonClient: PolygonClient = wire[PolygonClient] 21 | lazy val alpacaClient: AlpacaClient = wire[AlpacaClient] 22 | lazy val alpacaStreamingClient: AlpacaStreamingClient = 23 | wire[AlpacaStreamingClient] 24 | lazy val polygonStreamingClient: PolygonStreamingClient = 25 | wire[PolygonStreamingClient] 26 | lazy val streamingClient: StreamingClient = wire[StreamingClient] 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/service/ConfigService.scala: -------------------------------------------------------------------------------- 1 | package alpaca.service 2 | import alpaca.dto.AlpacaAccountConfig 3 | import pureconfig.error.{ConfigReaderFailure, ConfigReaderFailures} 4 | import pureconfig.generic.auto._ 5 | import cats._ 6 | import cats.implicits._ 7 | import com.typesafe.scalalogging.Logger 8 | 9 | private[alpaca] class ConfigService { 10 | val logger = Logger(classOf[ConfigService]) 11 | 12 | var getConfig: Eval[Config] = _ 13 | 14 | def loadConfig(isPaper: Option[Boolean] = None, 15 | accountKey: Option[String] = None, 16 | accountSecret: Option[String] = None): Eval[Config] = { 17 | val alpacaAccountConfig = for { 18 | accountKey <- accountKey 19 | accountSecret <- accountSecret 20 | paperAccount <- isPaper 21 | } yield Config(accountKey, accountSecret, paperAccount) 22 | 23 | getConfig = Eval.now { 24 | alpacaAccountConfig 25 | .orElse(loadConfigFromFile()) 26 | .orElse(loadConfigFromEnv()) 27 | .getOrElse(Config("", "", isPaper = true)) 28 | } 29 | 30 | getConfig 31 | } 32 | 33 | private def loadConfigFromFile() = { 34 | for { 35 | config <- pureconfig.loadConfig[AlpacaConfig].toOption 36 | accountKey <- config.alpacaAuth.accountKey 37 | accountSecret <- config.alpacaAuth.accountSecret 38 | paperAccount <- config.alpacaAuth.isPaper 39 | } yield Config(accountKey, accountSecret, paperAccount) 40 | } 41 | 42 | private def loadConfigFromEnv() = { 43 | for { 44 | accountKey <- sys.env.get("accountKey") 45 | accountSecret <- sys.env.get("accountSecret") 46 | isPaper <- sys.env.get("isPaper") 47 | } yield Config(accountKey, accountSecret, isPaper.equalsIgnoreCase("true")) 48 | } 49 | 50 | } 51 | 52 | case class Config(accountKey: String, accountSecret: String, isPaper: Boolean) { 53 | var base_url: String = 54 | "https://api.alpaca.markets" 55 | var paper_url: String = "https://paper-api.alpaca.markets" 56 | var data_url: String = "https://data.alpaca.markets" 57 | val apiVersion = "v2" 58 | 59 | def getBaseUrl: String = { 60 | if (isPaper) { 61 | paper_url 62 | } else { 63 | base_url 64 | } 65 | } 66 | 67 | lazy val account_url = s"$getBaseUrl/$apiVersion/account" 68 | lazy val assets_url = s"$getBaseUrl/$apiVersion/assets" 69 | lazy val bars_url = s"$data_url/v1/bars" 70 | lazy val calendar_url = s"$getBaseUrl/$apiVersion/calendar" 71 | lazy val clock_url = s"$getBaseUrl/$apiVersion/clock" 72 | lazy val order_url = s"$getBaseUrl/$apiVersion/orders" 73 | lazy val positions_url = s"$getBaseUrl/$apiVersion/positions" 74 | 75 | lazy val basePolygonUrl = "https://api.polygon.io" 76 | 77 | } 78 | 79 | private case class AlpacaConfig(alpacaAuth: AlpacaAuth) 80 | private case class AlpacaAuth(accountKey: Option[String], 81 | accountSecret: Option[String], 82 | isPaper: Option[Boolean]) 83 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/service/HammockService.scala: -------------------------------------------------------------------------------- 1 | package alpaca.service 2 | 3 | import cats.effect.IO 4 | import hammock.UriInterpolator 5 | import hammock._ 6 | import cats.effect.IO 7 | import hammock.circe.implicits._ 8 | import hammock.jvm.Interpreter 9 | import hammock.marshalling._ 10 | import hammock.{Hammock, Method, UriInterpolator, _} 11 | import io.circe.generic.auto._ 12 | import cats._ 13 | import cats.implicits._ 14 | 15 | class HammockService(configService: ConfigService) { 16 | private implicit val interpreter: Interpreter[IO] = Interpreter[IO] 17 | 18 | def createTuples(arguments: Parameter*): Option[Array[(String, String)]] = { 19 | val args = arguments.flatMap(parameter => { 20 | parameter.value.map { parameterValue => 21 | Tuple2.apply(parameter.name, parameterValue.toString) 22 | } 23 | }) 24 | 25 | if (args.isEmpty) { 26 | None 27 | } else { 28 | Some(args.toArray) 29 | } 30 | } 31 | 32 | def buildURI(url: String, urlParams: Option[Array[(String, String)]] = None) 33 | : UriInterpolator.Output = { 34 | 35 | val withParams = if (urlParams.isDefined) { 36 | uri"${url}".params(urlParams.get: _*) 37 | } else { 38 | uri"$url" 39 | } 40 | 41 | withParams 42 | } 43 | 44 | def execute[A, B](method: Method, 45 | url: String, 46 | body: Option[B] = None, 47 | queryParams: Option[Array[(String, String)]] = None)( 48 | implicit hammockEvidence: hammock.Decoder[A], 49 | hammockEvidenceEncoder: hammock.Encoder[B]): IO[A] = { 50 | val trueUrl = buildURI(url, queryParams) 51 | 52 | Hammock 53 | .request( 54 | method, 55 | trueUrl, 56 | Map( 57 | "APCA-API-KEY-ID" -> configService.getConfig.value.accountKey, 58 | "APCA-API-SECRET-KEY" -> configService.getConfig.value.accountSecret), 59 | body 60 | ) // In the `request` method, you describe your HTTP request 61 | .as[A] 62 | .exec[IO] 63 | } 64 | } 65 | 66 | case class Parameter(name: String, value: Option[_]) 67 | -------------------------------------------------------------------------------- /src/main/scala/alpaca/service/StreamingService.scala: -------------------------------------------------------------------------------- 1 | package alpaca.service 2 | 3 | import akka.Done 4 | import akka.actor.ActorSystem 5 | import akka.http.scaladsl.Http 6 | import akka.http.scaladsl.model.ws.WebSocketRequest 7 | import akka.stream.{ActorMaterializer, OverflowStrategy} 8 | import akka.stream.scaladsl.{Sink, Source, SourceQueueWithComplete} 9 | import alpaca.dto.streaming.Polygon._ 10 | import io.circe.{DecodingFailure, Json} 11 | import io.circe.generic.auto._ 12 | import io.circe.syntax._ 13 | import io.circe._ 14 | import io.circe.parser._ 15 | import akka.http.scaladsl.model.ws.{ 16 | BinaryMessage, 17 | TextMessage, 18 | WebSocketRequest, 19 | WebSocketUpgradeResponse, 20 | Message => WSMessage 21 | } 22 | import alpaca.dto.streaming.Alpaca.{ 23 | AlpacaAccountUpdate, 24 | AlpacaAckMessage, 25 | AlpacaAuthorizationMessage, 26 | AlpacaListenMessage, 27 | AlpacaStreamMessage, 28 | AlpacaTradeUpdate, 29 | Stream 30 | } 31 | 32 | import scala.concurrent.Future 33 | 34 | class StreamingService { 35 | implicit val system: ActorSystem = ActorSystem() 36 | implicit val materializer: ActorMaterializer = ActorMaterializer() 37 | 38 | def decodePolygonMessage(json: Json, 39 | polygonStreamBasicMessage: PolygonStreamBasicMessage) 40 | : Either[DecodingFailure, List[PolygonStreamMessage]] = { 41 | polygonStreamBasicMessage.ev match { 42 | case Ev.T => json.as[List[PolygonStreamTradeMessage]] 43 | case Ev.Q => json.as[List[PolygonStreamQuoteMessage]] 44 | case Ev.A => json.as[List[PolygonStreamAggregatePerSecond]] 45 | case Ev.AM => json.as[List[PolygonStreamAggregatePerMinute]] 46 | case Ev.status => json.as[List[PolygonStreamAuthenticationMessage]] 47 | } 48 | 49 | } 50 | 51 | def decodeAlpacaMessage(json: Json, alpacaAckMessage: AlpacaAckMessage) 52 | : Either[DecodingFailure, AlpacaStreamMessage] = { 53 | alpacaAckMessage.stream match { 54 | case Stream.account_updates => json.as[AlpacaAccountUpdate] 55 | case Stream.trade_updates => json.as[AlpacaTradeUpdate] 56 | case Stream.listening => json.as[AlpacaListenMessage] 57 | case Stream.authorization => json.as[AlpacaAuthorizationMessage] 58 | } 59 | } 60 | 61 | def createClientSource(wsUrl: String, flow: Sink[WSMessage, Future[Done]]) 62 | : SourceQueueWithComplete[WSMessage] = { 63 | Source 64 | .queue[WSMessage](bufferSize = 1000, OverflowStrategy.backpressure) 65 | .via(Http().webSocketClientFlow(WebSocketRequest(wsUrl))) 66 | .to(flow) 67 | .run() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/tut/docs/account.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Account 4 | --- 5 | ## Account 6 | 7 | ### View Account Information 8 | 9 | ```scala 10 | import alpaca.Alpaca 11 | 12 | //Can optionally pass in API_KEY and API_SECRET if it wasn't specified above. 13 | val alpaca : Alpaca = Alpaca 14 | val account = alpaca.getAccount.unsafeToFuture() 15 | account.onComplete { 16 | case Failure(exception) => println("Could not get account information") 17 | case Success(value) => 18 | if (value.account_blocked) { 19 | println("Account is currently restricted from trading.") 20 | } 21 | println(s"${value.buying_power} is available as buying power.") 22 | } 23 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/assets.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Assets 4 | --- 5 | 6 | ## Assets 7 | 8 | ### Get a List of Assets 9 | 10 | ```scala 11 | import alpaca.Alpaca 12 | 13 | val alpaca : Alpaca = Alpaca 14 | val activeAssets = alpaca.getAssets(Some("active")).unsafeToFuture() 15 | activeAssets.onComplete { 16 | case Failure(exception) => println("Could not retrieve assets.") 17 | case Success(values) => 18 | values 19 | .filter(asset => asset.exchange.equalsIgnoreCase("NASDAQ")) 20 | .foreach(println) 21 | } 22 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/calendar.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Calendar 4 | --- 5 | 6 | ## Calendar 7 | 8 | ### Check when the market was open on Dec. 1, 2018. 9 | 10 | ```scala 11 | import alpaca.Alpaca 12 | 13 | val alpaca : Alpaca = Alpaca 14 | val date = "2018-12-01" 15 | val calendar = alpaca.getCalendar(Some(date), Some(date)).unsafeToFuture() 16 | calendar.onComplete { 17 | case Failure(exception) => 18 | println("Could not get calendar." + exception.getMessage) 19 | case Success(value) => 20 | val calendar = value.head 21 | println( 22 | s"The market opened at ${calendar.open} and closed at ${calendar.close} on ${date}.") 23 | } 24 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/clock.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Clock 4 | --- 5 | 6 | ## Clock 7 | 8 | ### See if the Market is Open 9 | 10 | ```scala 11 | import alpaca.Alpaca 12 | 13 | val alpaca : Alpaca = Alpaca 14 | val clock = alpaca.getClock.unsafeToFuture() 15 | clock.onComplete { 16 | case Failure(exception) => println("Could not get clock.") 17 | case Success(value) => 18 | val isOpen = if (value.is_open) { 19 | "open." 20 | } else { 21 | "closed." 22 | } 23 | println(s"The market is $isOpen") 24 | } 25 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Getting Started 4 | --- 5 | 6 | ## Setup 7 | 8 | Add the dependency: 9 | 10 | `libraryDependencies += "com.cynance" %% "alpaca-scala" % "3.0.0"` 11 | 12 | The library requires configuration that consists of these 3 properties 13 | * `accountKey` - The account key from the alpaca account. 14 | * `accountSecret` - The account secret from the alpaca account. 15 | * `isPaper` - Determines whether an account is a paper trading account. 16 | 17 | The order the configuration properties are read are : 18 | * **Class Instantiation** - When the `Alpaca` class is instantiated, it can have the arguments of `accountKey`, `accountSecret` and `isPaper`. 19 | * **Config File** - This library will automatically pick up from an `application.conf` for example: 20 | ```yaml 21 | alpaccaauth { 22 | accountKey : 'blah', 23 | accountSecret : 'asdfv', 24 | isPaper : 'true' 25 | } 26 | ``` 27 | * **Env Variables** - You can also pass in system environment variables and it will pick those up. -------------------------------------------------------------------------------- /src/main/tut/docs/market-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Market Data 4 | --- 5 | 6 | ## Market Data 7 | 8 | ### Get Historical Price and Volume Data 9 | 10 | ```scala 11 | import alpaca.Alpaca 12 | 13 | val alpaca : Alpaca = Alpaca 14 | val bars = alpaca.getBars("day", List("AAPL"), limit = Some("5")) 15 | bars.unsafeToFuture().onComplete { 16 | case Failure(exception) => 17 | println("Could not retrieve bars." + exception.getMessage) 18 | case Success(barset) => 19 | val appl_bars = barset.get("AAPL").get 20 | val week_open = appl_bars.head.o 21 | val week_close = appl_bars.last.c 22 | val percent_change = (week_close - week_open) / week_open 23 | println(s"AAPL moved $percent_change over the last 5 days.") 24 | } 25 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/orders.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Orders 4 | --- 5 | 6 | ## Orders 7 | 8 | ### Place Order 9 | 10 | ```scala 11 | import alpaca.Alpaca 12 | 13 | val alpaca : Alpaca = Alpaca 14 | val order = 15 | Await.result( 16 | alpaca 17 | .placeOrder(OrderRequest("AAPL", "1", "sell", "market", "day")) 18 | .unsafeToFuture(), 19 | 10 seconds) 20 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/polygon.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Polygon 4 | --- 5 | 6 | ## Polygon Usage 7 | 8 | >Polygon API's require a live account. 9 | 10 | ### Historical trade aggregate. 11 | 12 | ```scala 13 | val alpaca = Alpaca() 14 | val ht = 15 | alpaca.getHistoricalTradesAggregate("AAPL", 16 | "minute", 17 | Some("4-1-2018"), 18 | Some("4-12-2018"), 19 | Some(5)) 20 | ht.unsafeToFuture().onComplete { 21 | case Failure(exception) => 22 | println("Could not get trades." + exception.getMessage) 23 | case Success(value) => 24 | println(s"${value}") 25 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/positions.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Positions 4 | --- 5 | 6 | ## Portfolio Examples 7 | 8 | ### View Open Positions of AAPL in Your Portfolio 9 | 10 | ```scala 11 | import alpaca.Alpaca 12 | 13 | val alpaca : Alpaca = Alpaca 14 | val aaplPosition = alpaca.getPosition("AAPL") 15 | aaplPosition.unsafeToFuture().onComplete { 16 | case Failure(exception) => 17 | println("Could not get position." + exception.getMessage) 18 | case Success(value) => 19 | println(s"${value.qty}") 20 | } 21 | ``` 22 | 23 | ### View All Open Positions in Your Portfolio 24 | 25 | ```scala 26 | import alpaca.Alpaca 27 | 28 | val alpaca : Alpaca = Alpaca 29 | alpaca.getPositions.unsafeToFuture().onComplete { 30 | case Failure(exception) => 31 | println("Could not get position." + exception.getMessage) 32 | case Success(value) => 33 | println(s"${value.size}") 34 | } 35 | ``` 36 | 37 | ### Close Positions of AAPL in Your Portfolio 38 | 39 | ```scala 40 | import alpaca.Alpaca 41 | 42 | val alpaca : Alpaca = Alpaca 43 | alpaca.closePosition("AAPL").unsafeToFuture().onComplete { 44 | case Failure(exception) => 45 | println("Could not get position." + exception.getMessage) 46 | case Success(value) => 47 | println(s"${value.size}") 48 | } 49 | ``` 50 | 51 | ### Close All Positions in Your Portfolio 52 | 53 | ```scala 54 | import alpaca.Alpaca 55 | 56 | val alpaca : Alpaca = Alpaca 57 | alpaca.closeAllPositions.unsafeToFuture().onComplete { 58 | case Failure(exception) => 59 | println("Could not get position." + exception.getMessage) 60 | case Success(value) => 61 | println(s"${value.size}") 62 | } 63 | ``` -------------------------------------------------------------------------------- /src/main/tut/docs/streaming.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Streaming 4 | --- 5 | 6 | ## Streaming Usage 7 | 8 | >Note : Streaming requires [akka-streams](https://doc.akka.io/docs/akka/2.5/stream/) for proper usage. 9 | 10 | ### Main DSL for Streams 11 | 12 | #### Polygon 13 | 14 | - `PolygonQuoteSubscribe` - Reads a stream of the polygon quotes based on a symbol. 15 | - `PolygonTradeSubscribe` - Reads a stream of the polygon trades based on a symbol. 16 | - `PolygonAggregatePerMinuteSubscribe` - Reads a stream of the polygon aggregates on a minute basis. 17 | - `PolygonAggregatePerSecondSubscribe` - Reads a stream of the polygon aggregates on a second basis. 18 | 19 | #### Alpaca 20 | 21 | - `AlpacaTradeUpdatesSubscribe` - Reads a stream of trade updates from Alpaca 22 | - `AlpacaAccountUpdatesSubscribe` - Reads a stream of account updates from Alpaca 23 | - `AlpacaAccountAndTradeUpdates` - Combines both streams. 24 | 25 | ### Subscribe to trades from polygon 26 | 27 | ```scala 28 | val alpaca = Alpaca() 29 | val stream: StreamingClient = alpaca.alpaca.polygonStreamingClient 30 | 31 | val str = stream 32 | .subscribe(PolygonQuoteSubscribe("AAPL")) 33 | 34 | str._2 35 | .runWith(Sink.foreach(x => println(x.data))) 36 | ``` 37 | 38 | ### Subscribe to trade updates alpaca 39 | 40 | ```scala 41 | val alpaca = Alpaca() 42 | val stream = alpaca.alpacaStreamingClient.subscribe(AlpacaAccountAndTradeUpdates()) 43 | stream._2.runWith(Sink.foreach(x => { 44 | println(x) 45 | })) 46 | ``` -------------------------------------------------------------------------------- /src/main/tut/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | technologies: 4 | - first: ["Alpaca.Markets", "Alpaca Scala is completely dependent on alpaca.markets"] 5 | - second: ["Scala", "Alpaca Scala is powered by, of course, Scala."] 6 | - third: ["Cynance", "The finance startup that is powering the open-source tools to trade."] 7 | --- 8 | 9 | # alpaca-scala 10 | 11 | [![Build Status](https://travis-ci.org/OUeasley/alpaca-scala.svg?branch=master)](https://travis-ci.org/OUeasley/alpaca-scala) [![Coverage Status](https://coveralls.io/repos/github/OUeasley/alpaca-scala/badge.svg?branch=master)](https://coveralls.io/github/OUeasley/alpaca-scala?branch=master) 12 | 13 | **alpaca-scala** is an scala library that interfaces directly with [alpaca.markets](http://alpaca.markets). 14 | 15 | ## What is alpaca? 16 | 17 | Alpaca API lets you build and trade with real-time market data for free. 18 | 19 | - Commission-Free Stock Trading API. 20 | - Registered Securities Broker. 21 | - Built For Makers In Open source & Community 22 | 23 | 24 | ## Credits 25 | 26 | This library is based on and utilizes other awesome scala libraries: 27 | 28 | * [cats](https://typelevel.org/cats/) 29 | * [hammock](https://github.com/pepegar/hammock) 30 | * [akka-streams](https://doc.akka.io/docs/akka/current/stream/index.html) 31 | 32 | -------------------------------------------------------------------------------- /src/test/scala/alpaca/AlpacaIntegrationTests.scala: -------------------------------------------------------------------------------- 1 | //package alpaca 2 | // 3 | //import akka.NotUsed 4 | //import akka.actor.ActorSystem 5 | //import akka.stream.ActorMaterializer 6 | //import akka.stream.scaladsl.{Sink, Source, SourceQueueWithComplete} 7 | //import alpaca.client.{PolygonStreamingClient, StreamingClient} 8 | //import alpaca.dto.request.OrderRequest 9 | //import alpaca.dto.streaming.Alpaca.{ 10 | // AlpacaAccountAndTradeUpdates, 11 | // AlpacaTradeUpdatesSubscribe 12 | //} 13 | //import alpaca.dto.streaming.Polygon.{ 14 | // PolygonQuoteSubscribe, 15 | // PolygonTradeSubscribe 16 | //} 17 | //import alpaca.dto.streaming.{StreamMessage, StreamingMessage} 18 | //import org.scalatest.{AsyncFunSuite, BeforeAndAfterEach, FunSuite} 19 | // 20 | //import scala.concurrent.{Await, ExecutionContext, Future} 21 | //import scala.concurrent.duration._ 22 | //import scala.util.{Failure, Success} 23 | //import com.typesafe.scalalogging.Logger 24 | //import cats._ 25 | //import cats.implicits._ 26 | // 27 | ////Functional tests. Need a paper api key for these to work. 28 | //class AlpacaIntegrationTests extends AsyncFunSuite with BeforeAndAfterEach { 29 | // override implicit val executionContext = ExecutionContext.Implicits.global 30 | // implicit val sys = ActorSystem() 31 | // implicit val mat = ActorMaterializer() 32 | // val logger = Logger(classOf[AlpacaIntegrationTests]) 33 | // var alpaca: Alpaca = _ 34 | // 35 | // override def beforeEach() { 36 | // alpaca = Alpaca(Some(true), Some(""), Some("")) 37 | // super.beforeEach() // To be stackable, must call super.beforeEach 38 | // } 39 | // 40 | // test("Get account") { 41 | // 42 | // val account = alpaca.getAccount.unsafeToFuture() 43 | // account.onComplete { 44 | // case Failure(exception) => 45 | // logger.error("Could not get account", exception) 46 | // case Success(value) => 47 | // if (value.account_blocked) { 48 | // println("Account is currently restricted from trading.") 49 | // } 50 | // println(s"${value.buying_power} is available as buying power.") 51 | // } 52 | // 53 | // account.map(ac => assert(!ac.pattern_day_trader)) 54 | // } 55 | // 56 | //// test("Get trade updates") { 57 | //// val stream = alpaca.getStream().subscribeAlpaca("trade_updates") 58 | //// stream._2.runWith(Sink.foreach(x => { 59 | //// println(new String(x.data)) 60 | //// println(new String(x.subject)) 61 | //// })) 62 | //// Thread.sleep(5000) 63 | //// 64 | //// val order = 65 | //// Await.result( 66 | //// alpaca 67 | //// .placeOrder(OrderRequest("GOOG", "1", "buy", "market", "day")) 68 | //// .unsafeToFuture(), 69 | //// 10 seconds) 70 | //// 71 | //// Thread.sleep(10000) 72 | //// null 73 | //// } 74 | //// 75 | //// test("Stream -> get quote updates.") { 76 | //// val listOfQuotes = List("T.AAPL", "T.GOOG", "T.SNAP") 77 | //// val stream = alpaca.getStream().sub(listOfQuotes) 78 | //// stream.foreach(x => { 79 | //// x._2._2.runWith(Sink.foreach(x => { 80 | //// println(new String(x.data)) 81 | //// println(new String(x.subject)) 82 | //// })) 83 | //// }) 84 | //// Thread.sleep(10000) 85 | //// } 86 | //// 87 | //// test("Get Assets") { 88 | //// val activeAssets = alpaca.getAssets(Some("active")).unsafeToFuture() 89 | //// activeAssets.onComplete { 90 | //// case Failure(exception) => 91 | //// logger.error("Could not retrieve assets.", exception) 92 | //// case Success(values) => 93 | //// values 94 | //// .filter(asset => asset.exchange.equalsIgnoreCase("NASDAQ")) 95 | //// .foreach(println) 96 | //// } 97 | //// } 98 | //// 99 | // test("Get Bars") { 100 | // val bars = alpaca.getBars("day", List("AAPL"), limit = Some("5")) 101 | // bars.unsafeToFuture().map(bar => assert(bar.head._2.nonEmpty)) 102 | // } 103 | //// 104 | // test("Get Clock") { 105 | // val clock = alpaca.getClock.unsafeToFuture() 106 | // clock.map(x => assert(!x.timestamp.isEmpty)) 107 | // } 108 | //// 109 | //// test("Get Calendar") { 110 | //// val date = "2018-12-01" 111 | //// val calendar = alpaca.getCalendar(Some(date), Some(date)).unsafeToFuture() 112 | //// calendar.onComplete { 113 | //// case Failure(exception) => 114 | //// println("Could not get calendar." + exception.getMessage) 115 | //// case Success(value) => 116 | //// val calendar = value.head 117 | //// println( 118 | //// s"The market opened at ${calendar.open} and closed at ${calendar.close} on ${date}.") 119 | //// } 120 | //// } 121 | //// 122 | //// test("Get Position") { 123 | //// val aaplPosition = alpaca.getPosition("AAPL") 124 | //// aaplPosition.unsafeToFuture().onComplete { 125 | //// case Failure(exception) => 126 | //// println("Could not get position." + exception.getMessage) 127 | //// case Success(value) => 128 | //// println(s"${value.qty}") 129 | //// } 130 | //// } 131 | //// 132 | //// test("Get Positions") { 133 | //// alpaca.getPositions.unsafeToFuture().onComplete { 134 | //// case Failure(exception) => 135 | //// println("Could not get position." + exception.getMessage) 136 | //// case Success(value) => 137 | //// println(s"${value.size}") 138 | //// } 139 | //// } 140 | // 141 | // test("Close Position") { 142 | // for { 143 | // posit <- alpaca.closePosition("AAPL").unsafeToFuture() 144 | // } yield assert(posit.id != null) 145 | // } 146 | // 147 | // test("Close All Position") { 148 | // for { 149 | // posit <- alpaca.closeAllPositions.unsafeToFuture() 150 | // } yield assert(true == true) 151 | // } 152 | //// 153 | // test("Place buy order") { 154 | // val order = 155 | // Await.result( 156 | // alpaca 157 | // .placeOrder(OrderRequest("MSFT", "10", "buy", "market", "day")) 158 | // .unsafeToFuture(), 159 | // 10 seconds) 160 | // assert(order != null) 161 | // 162 | // } 163 | //// 164 | //// test("Place sell order") { 165 | //// val order = 166 | //// Await.result( 167 | //// alpaca 168 | //// .placeOrder(OrderRequest("AAPL", "1", "sell", "market", "day")) 169 | //// .unsafeToFuture(), 170 | //// 10 seconds) 171 | //// assert(order != null) 172 | //// } 173 | //// 174 | // test("Test stream polygon") { 175 | // val stream: PolygonStreamingClient = alpaca.polygonStreamingClient 176 | // 177 | // val str: (SourceQueueWithComplete[StreamMessage], 178 | // Source[StreamMessage, NotUsed]) = stream 179 | // .subscribe(PolygonQuoteSubscribe("AAPL")) 180 | // 181 | // str._2 182 | // .runWith(Sink.foreach(x => println(x))) 183 | // Thread.sleep(15000) 184 | // str._1.complete() 185 | // println("-----------------------------------------") 186 | // str._2 187 | // .runWith(Sink.foreach(x => println(x))) 188 | // Thread.sleep(5000) 189 | // assert(true) 190 | // } 191 | //// 192 | // test("Test stream alpaca") { 193 | // val stream = alpaca.alpacaStreamingClient 194 | // implicit val sys = ActorSystem() 195 | // implicit val mat = ActorMaterializer() 196 | // val str = stream 197 | // .subscribe(AlpacaAccountAndTradeUpdates()) 198 | // str._2 199 | // .runWith(Sink.foreach(x => println(x))) 200 | // 201 | // Await.result( 202 | // alpaca 203 | // .placeOrder(OrderRequest("AAPL", "10", "buy", "market", "day")) 204 | // .unsafeToFuture(), 205 | // 10 seconds) 206 | //// Await.result( 207 | //// alpaca 208 | //// .placeOrder(OrderRequest("GOOG", "10", "buy", "market", "day")) 209 | //// .unsafeToFuture(), 210 | //// 10 seconds) 211 | // Thread.sleep(15000) 212 | // assert(true) 213 | // 214 | // } 215 | //// 216 | //// test("Polygon trade test") { 217 | //// alpaca.getHistoricalTrades("AAPL", "2018-2-2", None, Some(5)) 218 | //// } 219 | //// 220 | //// test("Polygon hist trade test") { 221 | //// val ht = 222 | //// alpaca.getHistoricalTradesAggregate("AAPL", 223 | //// "minute", 224 | //// Some("4-1-2018"), 225 | //// Some("4-12-2018"), 226 | //// Some(5)) 227 | //// ht.unsafeToFuture().onComplete { 228 | //// case Failure(exception) => 229 | //// println("Could not get position." + exception.getMessage) 230 | //// case Success(value) => 231 | //// println(s"${value}") 232 | //// } 233 | //// } 234 | // 235 | //} 236 | -------------------------------------------------------------------------------- /src/test/scala/alpaca/AlpacaTest.scala: -------------------------------------------------------------------------------- 1 | package alpaca 2 | 3 | import alpaca.client.{AlpacaClient, PolygonClient} 4 | import alpaca.dto.request.OrderRequest 5 | import alpaca.modules.MainModule 6 | import org.mockito.{ArgumentMatchersSugar, MockitoSugar} 7 | import org.scalatest.{BeforeAndAfterEach, FunSuite, WordSpec} 8 | import cats._ 9 | import cats.implicits._ 10 | 11 | import scala.util.Failure 12 | 13 | class AlpacaTest extends WordSpec with BeforeAndAfterEach with MockitoSugar { 14 | 15 | trait fakeMainModule extends MainModule { 16 | override lazy val alpacaClient: AlpacaClient = mock[AlpacaClient] 17 | override lazy val polygonClient: PolygonClient = mock[PolygonClient] 18 | } 19 | var alpaca: Alpaca = _ 20 | override def beforeEach() { 21 | alpaca = new Alpaca() with fakeMainModule 22 | super.beforeEach() // To be stackable, must call super.beforeEach 23 | } 24 | 25 | "Alpaca" can { 26 | "getAccount " should { 27 | "invoke AlpacaClient getAccount" in { 28 | alpaca.getAccount 29 | verify(alpaca.alpacaClient).getAccount 30 | } 31 | } 32 | "getAsset " should { 33 | "invoke AlpacaClient getAsset" in { 34 | val symbol = "AAPL" 35 | alpaca.getAsset(symbol) 36 | verify(alpaca.alpacaClient).getAsset(symbol) 37 | } 38 | "invoke AlpacaClient getAsset with class" in { 39 | val symbol = "AAPL".some 40 | val assetClass = "blah".some 41 | alpaca.getAssets(symbol, assetClass) 42 | alpaca.getAsset("blah") 43 | verify(alpaca.alpacaClient).getAssets(symbol, assetClass) 44 | } 45 | } 46 | "getBars" should { 47 | "invoke AlpacaClient getBars" in { 48 | val timeframe = "day" 49 | val symbols = List("AAPL") 50 | alpaca.getBars(timeframe, symbols) 51 | verify(alpaca.alpacaClient).getBars(timeframe, symbols) 52 | } 53 | } 54 | "getCalendar " should { 55 | "invoke AlpacaClient getCalendar" in { 56 | alpaca.getCalendar() 57 | verify(alpaca.alpacaClient).getCalendar() 58 | } 59 | } 60 | "getClock " should { 61 | "invoke AlpacaClient getCalendar" in { 62 | alpaca.getClock 63 | verify(alpaca.alpacaClient).getClock 64 | } 65 | } 66 | "getOrder " should { 67 | "invoke AlpacaClient getOrder" in { 68 | val orderId = "1234" 69 | alpaca.getOrder(orderId) 70 | verify(alpaca.alpacaClient).getOrder(orderId) 71 | } 72 | } 73 | 74 | "cancelOrder " should { 75 | "invoke AlpacaClient cancelOrder" in { 76 | val orderId = "1234" 77 | alpaca.cancelOrder(orderId) 78 | verify(alpaca.alpacaClient).cancelOrder(orderId) 79 | } 80 | } 81 | 82 | "getOrders " should { 83 | "invoke AlpacaClient getOrders" in { 84 | alpaca.getOrders 85 | verify(alpaca.alpacaClient).getOrders 86 | } 87 | } 88 | 89 | "placeOrder " should { 90 | "invoke AlpacaClient placeOrder" in { 91 | val orderRequest = OrderRequest("AAPL", "5", "BUY", "blah", "sure") 92 | alpaca.placeOrder(orderRequest) 93 | verify(alpaca.alpacaClient).placeOrder(orderRequest) 94 | } 95 | } 96 | 97 | "getPositions for symbol" should { 98 | "invoke AlpacaClient placeOrder" in { 99 | val symbol = "AAPL" 100 | alpaca.getPosition(symbol) 101 | verify(alpaca.alpacaClient).getPosition(symbol) 102 | } 103 | } 104 | 105 | "getPositions " should { 106 | "invoke AlpacaClient placeOrder" in { 107 | alpaca.getPositions 108 | verify(alpaca.alpacaClient).getPositions 109 | } 110 | } 111 | 112 | "getHistoricalTrades " should { 113 | "invoke PolygonClient getHistoricalTrades" in { 114 | val symbol = "AAPL" 115 | val date = "06/06/1960" 116 | alpaca.getHistoricalTrades(symbol, date) 117 | verify(alpaca.polygonClient).getHistoricalTrades(symbol, date) 118 | } 119 | } 120 | 121 | "getHistoricalTradesAggregate " should { 122 | "invoke PolygonClient getHistoricalTradesAggregate" in { 123 | val symbol = "AAPL" 124 | val date = "06/06/1960" 125 | alpaca.getHistoricalTradesAggregate(symbol, date) 126 | verify(alpaca.polygonClient).getHistoricalTradesAggregate(symbol, date) 127 | } 128 | } 129 | 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/test/scala/alpaca/client/AlpacaClientTest.scala: -------------------------------------------------------------------------------- 1 | package alpaca.client 2 | import alpaca.Alpaca 3 | import alpaca.client.{AlpacaClient, PolygonClient} 4 | import alpaca.dto.Account 5 | import alpaca.dto.request.OrderRequest 6 | import alpaca.modules.MainModule 7 | import alpaca.service.{Config, ConfigService, HammockService} 8 | import org.mockito.{ArgumentMatchersSugar, MockitoSugar} 9 | import org.scalatest.{BeforeAndAfterEach, FunSuite, WordSpec} 10 | import cats._ 11 | import cats.effect.IO 12 | import cats.implicits._ 13 | import hammock._ 14 | import hammock.circe.implicits._ 15 | import hammock.jvm.Interpreter 16 | import hammock.marshalling._ 17 | import alpaca.dto._ 18 | import alpaca.dto.algebra.Bars 19 | import alpaca.dto.request.OrderRequest 20 | import io.circe.generic.auto._ 21 | 22 | import alpaca.service.{ConfigService, HammockService, Parameter} 23 | 24 | class AlpacaClientTest 25 | extends WordSpec 26 | with BeforeAndAfterEach 27 | with MockitoSugar { 28 | 29 | val hammockService: HammockService = mock[HammockService] 30 | val configService: ConfigService = mock[ConfigService] 31 | val mockConfig: Config = mock[Config] 32 | 33 | var alpacaClient: AlpacaClient = _ 34 | override def beforeEach() { 35 | 36 | when(mockConfig.getBaseUrl).thenReturn("asdfasf") 37 | when(mockConfig.assets_url).thenReturn("asdfsadf") 38 | when(configService.getConfig).thenReturn(Eval.now { 39 | mockConfig 40 | }) 41 | alpacaClient = new AlpacaClient(configService, hammockService) 42 | super.beforeEach() // To be stackable, must call super.beforeEach 43 | } 44 | 45 | "AlpacaClient" can { 46 | "getAccount " should { 47 | "invoke hammockService" in { 48 | alpacaClient.getAccount 49 | } 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/scala/alpaca/service/ConfigServiceTest.scala: -------------------------------------------------------------------------------- 1 | //package alpaca.service 2 | // 3 | //import alpaca.dto.Account 4 | //import cats.Eval 5 | //import cats.effect.IO 6 | //import hammock.Method 7 | //import org.mockito.MockitoSugar 8 | //import org.scalatest.{BeforeAndAfterEach, WordSpec} 9 | // 10 | //class ConfigServiceTest 11 | // extends WordSpec 12 | // with BeforeAndAfterEach 13 | // with MockitoSugar { 14 | // 15 | // var configService: ConfigService = _ 16 | // 17 | // override def beforeEach() { 18 | // configService = new ConfigService 19 | // super.beforeEach() // To be stackable, must call super.beforeEach 20 | // } 21 | // 22 | // "ConfigService" can { 23 | // "loadConfig " should { 24 | // "should use passed in parameters" in { 25 | // configService.loadConfig(Some(true), Some("test"), Some("blah")) 26 | // val config: Config = configService.getConfig.value 27 | // assert(config.isPaper) 28 | // assert(config.accountKey === "test") 29 | // assert(config.accountSecret === "blah") 30 | // } 31 | // } 32 | // } 33 | // 34 | //} 35 | -------------------------------------------------------------------------------- /src/test/scala/alpaca/service/HammockServiceTest.scala: -------------------------------------------------------------------------------- 1 | package alpaca.service 2 | 3 | import alpaca.client.{AlpacaClient, PolygonClient} 4 | import alpaca.dto.Account 5 | import alpaca.dto.request.OrderRequest 6 | import alpaca.modules.MainModule 7 | import org.mockito.{ArgumentMatchersSugar, MockitoSugar} 8 | import org.scalatest.{BeforeAndAfterEach, FunSuite, WordSpec} 9 | import cats._ 10 | import cats.effect.IO 11 | import cats.implicits._ 12 | import hammock.Method 13 | import hammock.jvm.Interpreter 14 | import cats.effect.IO 15 | import hammock.UriInterpolator 16 | import hammock._ 17 | import cats.effect.IO 18 | import hammock.circe.implicits._ 19 | import hammock.jvm.Interpreter 20 | import hammock.marshalling._ 21 | import hammock.{Hammock, Method, UriInterpolator, _} 22 | import io.circe.generic.auto._ 23 | import cats._ 24 | import cats.implicits._ 25 | 26 | class HammockServiceTest 27 | extends WordSpec 28 | with BeforeAndAfterEach 29 | with MockitoSugar { 30 | 31 | var hammockService: HammockService = _ 32 | val configService: ConfigService = mock[ConfigService] 33 | val mockConfig: Config = mock[Config] 34 | 35 | private implicit val interpreter: Interpreter[IO] = Interpreter[IO] 36 | 37 | override def beforeEach() { 38 | 39 | hammockService = new HammockService(configService) 40 | when(mockConfig.getBaseUrl).thenReturn("asdfasf") 41 | when(configService.getConfig).thenReturn(Eval.now { 42 | mockConfig 43 | }) 44 | super.beforeEach() // To be stackable, must call super.beforeEach 45 | } 46 | 47 | "HammockService" can { 48 | "execute " should { 49 | "should invoke execute" in { 50 | val execute = hammockService.execute[Account, Unit](Method.GET, "") 51 | assert(execute.isInstanceOf[IO[Account]]) 52 | } 53 | "should invoke execute with query params" in { 54 | val execute = hammockService.execute[Account, Unit]( 55 | Method.GET, 56 | "", 57 | queryParams = Some(Array(Tuple2.apply("Test", "Test")))) 58 | assert(execute.isInstanceOf[IO[Account]]) 59 | } 60 | } 61 | "createTuples" should { 62 | "create tuples when args are passed " in { 63 | val value = "bblah" 64 | val parameter = Parameter("test", value.some) 65 | val returnParams = hammockService.createTuples(parameter) 66 | assert(returnParams.get(0)._1 == parameter.name) 67 | assert(returnParams.get(0)._2 == value) 68 | } 69 | "return None when no args are passed " in { 70 | val returnParams = hammockService.createTuples() 71 | assert(returnParams.isEmpty) 72 | } 73 | } 74 | } 75 | } 76 | --------------------------------------------------------------------------------