├── .github
└── workflows
│ ├── living-documentation.yml
│ └── maven.yml
├── .gitignore
├── LICENSE
├── bddtrader-app
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── net
│ │ │ └── bddtrader
│ │ │ ├── Application.java
│ │ │ ├── clients
│ │ │ ├── ClientController.java
│ │ │ └── ClientDirectory.java
│ │ │ ├── config
│ │ │ ├── TraderConfiguration.java
│ │ │ └── TradingDataSource.java
│ │ │ ├── exceptions
│ │ │ └── MissingMandatoryFieldsException.java
│ │ │ ├── news
│ │ │ └── NewsController.java
│ │ │ ├── portfolios
│ │ │ ├── InsufficientFundsException.java
│ │ │ ├── Portfolio.java
│ │ │ ├── PortfolioController.java
│ │ │ ├── PortfolioDirectory.java
│ │ │ ├── PortfolioNotFoundException.java
│ │ │ └── PortfolioWithPositions.java
│ │ │ ├── status
│ │ │ └── StatusController.java
│ │ │ ├── stocks
│ │ │ └── StockController.java
│ │ │ └── tradingdata
│ │ │ ├── TradingData.java
│ │ │ ├── TradingDataAPI.java
│ │ │ ├── exceptions
│ │ │ └── IllegalPriceManipulationException.java
│ │ │ └── services
│ │ │ ├── IEXtradingAPI.java
│ │ │ └── StaticAPI.java
│ └── resources
│ │ ├── application.conf
│ │ └── sample_data
│ │ ├── news.json
│ │ └── prices.json
│ └── test
│ ├── java
│ └── net
│ │ └── bddtrader
│ │ ├── acceptancetests
│ │ ├── CucumberTestSuite.java
│ │ ├── actors
│ │ │ ├── CastOfTraders.java
│ │ │ └── Trader.java
│ │ ├── endpoints
│ │ │ └── BDDTraderEndPoints.java
│ │ ├── model
│ │ │ ├── MarketPrice.java
│ │ │ └── PositionSummary.java
│ │ ├── questions
│ │ │ └── ThePortfolio.java
│ │ ├── stepdefinitions
│ │ │ ├── BuyingAndSellingSharesStepDefinitions.java
│ │ │ ├── ClientStepDefinitions.java
│ │ │ ├── MarketNewsStepDefinitions.java
│ │ │ ├── SettingTheStage.java
│ │ │ ├── SetupSteps.java
│ │ │ ├── TestEnvironment.java
│ │ │ └── ViewingPositionsStepDefinitons.java
│ │ └── tasks
│ │ │ ├── PlaceOrder.java
│ │ │ ├── RegisterWithBDDTrader.java
│ │ │ ├── ViewNewsAbout.java
│ │ │ └── dsl
│ │ │ └── PlaceOrderDSL.java
│ │ └── apitests
│ │ ├── clients
│ │ ├── WhenAClientRegistersWithBDDTrader.java
│ │ └── WhenDifferentTypesOfClientsRegister.java
│ │ ├── news
│ │ └── WhenReadingTheNews.java
│ │ ├── portfolio
│ │ ├── WhenClientsUseTheirPortfolios.java
│ │ ├── WhenCreatingAPortfolio.java
│ │ ├── WhenWorkingWithPositions.java
│ │ └── WhenWorkingWithTrades.java
│ │ ├── status
│ │ └── WhenCheckingApplicationStatus.java
│ │ └── stocks
│ │ └── WhenRequestingThePrice.java
│ └── resources
│ ├── BDDTrader.postman_collection
│ ├── features
│ ├── clients
│ │ └── registering_a_new_client.feature
│ ├── portfolios
│ │ ├── buying_and_selling_shares.feature
│ │ └── viewing_positions.feature
│ ├── readme.md
│ └── viewing_market_data
│ │ ├── reading_market_news.feature
│ │ └── readme.md
│ ├── junit-platform.properties
│ └── serenity.conf
├── bddtrader-domain
├── pom.xml
└── src
│ └── main
│ └── java
│ └── net
│ └── bddtrader
│ ├── clients
│ └── Client.java
│ ├── news
│ └── NewsItem.java
│ ├── portfolios
│ ├── MoneyCalculations.java
│ ├── Position.java
│ ├── Positions.java
│ ├── Trade.java
│ ├── TradeBuilder.java
│ ├── TradeDirection.java
│ ├── TradeType.java
│ └── dsl
│ │ ├── AtAPriceOf.java
│ │ ├── CentsEach.java
│ │ ├── DollarsEach.java
│ │ ├── InCurrency.java
│ │ └── SharesOf.java
│ ├── stocks
│ └── TopStock.java
│ └── tradingdata
│ ├── NewsReader.java
│ ├── PriceReader.java
│ └── PriceUpdater.java
├── circle.yml
├── exercises.adoc
├── manifest.yml
├── pom.xml
├── readme.adoc
└── serenity.properties
/.github/workflows/living-documentation.yml:
--------------------------------------------------------------------------------
1 | name: Living Documentation
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up JDK 17
17 | uses: actions/setup-java@v1
18 | with:
19 | distribution: 'zulu'
20 | java-version: '17'
21 |
22 | - name: Cache the Maven packages to speed up build
23 | uses: actions/cache@v1
24 | with:
25 | path: ~/.m2
26 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
27 | restore-keys: ${{ runner.os }}-m2
28 |
29 | - name: Build with Maven
30 | run: mvn verify
31 |
32 | - name: Deploy
33 | uses: JamesIves/github-pages-deploy-action@4.1.1
34 | with:
35 | branch: gh-pages # The branch the action should deploy to.
36 | folder: bddtrader-app/target/site/serenity/ # The folder the action should deploy.
37 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
3 |
4 | name: Java CI with Maven
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up JDK 17
20 | uses: actions/setup-java@v2
21 | with:
22 | java-version: '17'
23 | distribution: 'adopt'
24 |
25 | - name: Build with Maven
26 | run: mvn verify --file pom.xml
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | target
3 | *.iml
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/bddtrader-app/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | net.bddtrader
6 | bddtrader-app
7 | 1.0.0-SNAPSHOT
8 | jar
9 |
10 | The BDD Trader Application
11 |
12 |
13 | net.bddtrader
14 | bddtrader
15 | 1.0.0-SNAPSHOT
16 |
17 |
18 |
19 | 17
20 | UTF-8
21 | UTF-8
22 | 4.1.20
23 |
24 |
25 |
26 |
27 | net.bddtrader
28 | bddtrader-domain
29 | ${version}
30 |
31 |
32 | org.springframework.boot
33 | spring-boot-starter-parent
34 | 3.1.0
35 | pom
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-starter-web
40 | 3.1.0
41 |
42 |
43 | com.typesafe
44 | config
45 | 1.4.3
46 |
47 |
48 | org.projectlombok
49 | lombok
50 | 1.18.34
51 |
52 |
53 | javax.servlet
54 | javax.servlet-api
55 | 4.0.1
56 | provided
57 |
58 |
59 | org.springdoc
60 | springdoc-openapi-ui
61 | 1.7.0
62 |
63 |
64 | com.github.lalyos
65 | jfiglet
66 | 0.0.9
67 | compile
68 |
69 |
70 | org.mockito
71 | mockito-core
72 | 5.12.0
73 | test
74 |
75 |
76 |
77 | net.serenity-bdd
78 | serenity-core
79 | ${serenity.version}
80 | test
81 |
82 |
83 | net.serenity-bdd
84 | serenity-junit
85 | ${serenity.version}
86 | test
87 |
88 |
89 | net.serenity-bdd
90 | serenity-screenplay
91 | ${serenity.version}
92 | test
93 |
94 |
95 | net.serenity-bdd
96 | serenity-screenplay-webdriver
97 | ${serenity.version}
98 | test
99 |
100 |
101 | net.serenity-bdd
102 | serenity-screenplay-rest
103 | ${serenity.version}
104 | test
105 |
106 |
107 | net.serenity-bdd
108 | serenity-cucumber
109 | ${serenity.version}
110 | test
111 |
112 |
113 | net.serenity-bdd
114 | serenity-rest-assured
115 | ${serenity.version}
116 | test
117 |
118 |
119 | io.cucumber
120 | cucumber-junit-platform-engine
121 | 7.18.1
122 | test
123 |
124 |
125 |
126 |
127 |
128 | org.springframework.boot
129 | spring-boot-maven-plugin
130 | 3.3.2
131 |
132 |
133 |
134 | repackage
135 |
136 |
137 |
138 | pre-integration-test
139 |
140 | start
141 |
142 |
143 |
144 | post-integration-test
145 |
146 | stop
147 |
148 |
149 |
150 |
151 |
152 | org.apache.maven.plugins
153 | maven-surefire-plugin
154 | 3.2.5
155 |
156 |
157 | **/When*.java
158 | **/*Spec.java
159 | **/*Test.java
160 |
161 |
162 |
163 |
164 | maven-failsafe-plugin
165 | 3.2.5
166 |
167 |
168 | **/acceptancetests/*.java
169 |
170 |
171 |
172 |
173 |
174 | integration-test
175 |
176 |
177 |
178 |
179 |
180 | org.apache.maven.plugins
181 | maven-compiler-plugin
182 | 3.8.1
183 |
184 | 17
185 | 17
186 |
187 |
188 |
189 | net.serenity-bdd.maven.plugins
190 | serenity-maven-plugin
191 | ${serenity.version}
192 |
193 | single-page-html
194 |
195 |
196 |
197 |
198 | net.serenity-bdd
199 | serenity-single-page-report
200 | ${serenity.version}
201 |
202 |
203 |
204 |
205 | check-gherkin
206 | process-test-resources
207 |
208 | check-gherkin
209 |
210 |
211 |
212 | serenity-reports
213 | post-integration-test
214 |
215 | aggregate
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/Application.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader;
2 |
3 | import com.github.lalyos.jfiglet.FigletFont;
4 | import org.springframework.boot.CommandLineRunner;
5 | import org.springframework.boot.SpringApplication;
6 | import org.springframework.boot.autoconfigure.SpringBootApplication;
7 | import org.springframework.context.ApplicationContext;
8 | import org.springframework.context.annotation.Bean;
9 |
10 | @SpringBootApplication
11 | public class Application {
12 |
13 | public static void main(String[] args) {
14 | SpringApplication.run(Application.class, args);
15 | }
16 |
17 | @Bean
18 | public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
19 | return args -> {
20 | System.out.println(FigletFont.convertOneLine("BDD TRADER"));
21 | };
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/clients/ClientController.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.clients;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import net.bddtrader.portfolios.PortfolioController;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.http.HttpStatus;
8 | import org.springframework.http.ResponseEntity;
9 | import org.springframework.web.bind.annotation.PathVariable;
10 | import org.springframework.web.bind.annotation.RequestBody;
11 | import org.springframework.web.bind.annotation.RequestMapping;
12 | import org.springframework.web.bind.annotation.RestController;
13 |
14 | import java.util.List;
15 | import java.util.Optional;
16 |
17 | import static org.springframework.http.HttpStatus.NOT_FOUND;
18 | import static org.springframework.http.HttpStatus.OK;
19 | import static org.springframework.web.bind.annotation.RequestMethod.*;
20 |
21 | /**
22 | * Clients register with the application by providing their first and last name.
23 | * When clients register, they are given a portfolio with $1000.
24 | */
25 | @RestController
26 | @Tag(name = "Client", description = "Client APIs")
27 | public class ClientController {
28 |
29 | private final ClientDirectory clientDirectory;
30 | private final PortfolioController portfolioController;
31 |
32 | @Autowired
33 | public ClientController(ClientDirectory clientDirectory,
34 | PortfolioController portfolioController) {
35 |
36 | this.clientDirectory = clientDirectory;
37 | this.portfolioController = portfolioController;
38 | }
39 |
40 | @RequestMapping(value = "/api/client", method = POST)
41 | @Operation(summary = "Register a new client")
42 | public Client register(@RequestBody Client newClient) {
43 | Client client = clientDirectory.registerClient(newClient);
44 |
45 | portfolioController.createPortfolioForClient(client.getId());
46 |
47 | return client;
48 | }
49 |
50 | @RequestMapping(value = "/api/client/{clientId}", method = GET)
51 | @Operation(summary = "Get the details of a given client")
52 | public ResponseEntity findClientById(@PathVariable Long clientId) {
53 |
54 | Optional client = clientDirectory.findClientById(clientId);
55 |
56 | return client.map(
57 | clientFound -> new ResponseEntity<>(clientFound, OK))
58 | .orElseGet(() -> new ResponseEntity<>(NOT_FOUND));
59 | }
60 |
61 | @RequestMapping(value = "/api/client/{clientId}", method = PUT)
62 | @Operation(summary = "Update the details of a given client")
63 | public ResponseEntity updateClientById(@PathVariable Long clientId, @RequestBody Client newClient) {
64 |
65 | Optional client = clientDirectory.findClientById(clientId);
66 |
67 | if (client.isPresent()) {
68 | clientDirectory.updateClient(clientId, newClient);
69 | return new ResponseEntity<>(newClient, OK);
70 | } else {
71 | return new ResponseEntity<>(NOT_FOUND);
72 | }
73 | }
74 |
75 | @RequestMapping(value = "/api/client/{clientId}", method = DELETE)
76 | @Operation(summary = "Delete a client")
77 | public ResponseEntity deleteClientById(@PathVariable Long clientId) {
78 |
79 | clientDirectory.deleteClientById(clientId);
80 | return new ResponseEntity<>(HttpStatus.NO_CONTENT);
81 | }
82 |
83 | @RequestMapping(value = "/api/clients", method = GET)
84 | @Operation(summary = "Get all the currently registered clients")
85 | public List findAll() {
86 | return clientDirectory.findAll();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/clients/ClientDirectory.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.clients;
2 |
3 | import net.bddtrader.exceptions.MissingMandatoryFieldsException;
4 | import org.springframework.stereotype.Component;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import java.util.Optional;
9 | import java.util.concurrent.CopyOnWriteArrayList;
10 | import java.util.concurrent.atomic.AtomicLong;
11 | import java.util.stream.Collectors;
12 |
13 | import static org.springframework.util.StringUtils.isEmpty;
14 |
15 | /**
16 | * An in-memory directory of registered clients.
17 | */
18 | @Component
19 | public class ClientDirectory {
20 |
21 | private final List registeredClients = new CopyOnWriteArrayList<>();
22 | private final AtomicLong clientCount = new AtomicLong(1);
23 |
24 | public Client registerClient(Client newClient) {
25 |
26 | ensureMandatoryFieldsArePresentFor(newClient);
27 |
28 | Client registeredClient = new Client(clientCount.getAndIncrement(),
29 | newClient.getFirstName(),
30 | newClient.getLastName(),
31 | newClient.getEmail());
32 | registeredClients.add(registeredClient);
33 | return registeredClient;
34 | }
35 |
36 | public void updateClient(Long clientId, Client client) {
37 | deleteClientById(clientId);
38 | Client updatedClient = new Client(clientId,
39 | client.getFirstName(),
40 | client.getLastName(),
41 | client.getEmail());
42 | registeredClients.add(updatedClient);
43 | }
44 |
45 | private void ensureMandatoryFieldsArePresentFor(Client newClient) {
46 |
47 | List missingMandatoryFields = new ArrayList<>();
48 | if (isEmpty(newClient.getFirstName())) {
49 | missingMandatoryFields.add("firstName");
50 | }
51 | if (isEmpty(newClient.getLastName())) {
52 | missingMandatoryFields.add("lastName");
53 | }
54 | if (isEmpty(newClient.getEmail())) {
55 | missingMandatoryFields.add("email");
56 | }
57 |
58 | if (!missingMandatoryFields.isEmpty()) {
59 | throw new MissingMandatoryFieldsException("Missing mandatory fields for client: "
60 | + missingMandatoryFields.stream().collect(Collectors.joining(", ")));
61 | }
62 | }
63 |
64 | public Optional findClientById(long id) {
65 | return registeredClients.stream()
66 | .filter( client -> client.getId() == id)
67 | .findFirst();
68 | }
69 |
70 | public void deleteClientById(long id) {
71 | Optional existingClient = findClientById(id);
72 | existingClient.ifPresent(
73 | registeredClients::remove
74 | );
75 | }
76 |
77 | public List findAll() {
78 | return new ArrayList(registeredClients);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/config/TraderConfiguration.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.config;
2 |
3 | import com.typesafe.config.Config;
4 | import com.typesafe.config.ConfigFactory;
5 | import org.springframework.stereotype.Component;
6 |
7 | @Component
8 | public class TraderConfiguration {
9 | private final Config config = ConfigFactory.load();
10 |
11 | public TradingDataSource getTradingDataSource() {
12 | return config.getEnum(TradingDataSource.class, "data.source");
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/config/TradingDataSource.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.config;
2 |
3 | public enum TradingDataSource { DEV }
4 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/exceptions/MissingMandatoryFieldsException.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.exceptions;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(value = HttpStatus.PRECONDITION_FAILED)
7 | public class MissingMandatoryFieldsException extends RuntimeException {
8 | public MissingMandatoryFieldsException(String message) {
9 | super(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/news/NewsController.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.news;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import net.bddtrader.config.TraderConfiguration;
6 | import net.bddtrader.config.TradingDataSource;
7 | import net.bddtrader.tradingdata.TradingData;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.web.bind.annotation.PathVariable;
10 | import org.springframework.web.bind.annotation.RequestMapping;
11 | import org.springframework.web.bind.annotation.RestController;
12 |
13 | import java.util.List;
14 |
15 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
16 |
17 | @RestController
18 | @Tag(name = "news")
19 | public class NewsController {
20 |
21 | private final TradingDataSource tradingDataSource;
22 |
23 | public NewsController(TradingDataSource tradingDataSource) {
24 | this.tradingDataSource = tradingDataSource;
25 | }
26 | @Autowired
27 | public NewsController(TraderConfiguration traderConfiguration) {
28 | this(traderConfiguration.getTradingDataSource());
29 | }
30 |
31 | @RequestMapping(value="/api/stock/{stockid}/news", method = GET)
32 | @Operation(summary = "Get news articles about a given stock",
33 | description="Use 'market' to get market-wide news.")
34 | public List newsFor(@PathVariable String stockid) {
35 |
36 | return TradingData.instanceFor(tradingDataSource).getNewsFor(stockid);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/portfolios/InsufficientFundsException.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.portfolios;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(value = HttpStatus.PAYMENT_REQUIRED)
7 | public class InsufficientFundsException extends RuntimeException {
8 | InsufficientFundsException(String message) {
9 | super(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/portfolios/Portfolio.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.portfolios;
2 |
3 | import net.bddtrader.tradingdata.PriceReader;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.Map;
8 | import java.util.Optional;
9 | import java.util.concurrent.CopyOnWriteArrayList;
10 |
11 | import static net.bddtrader.portfolios.Trade.deposit;
12 |
13 | /**
14 | * A Portfolio records the financial position of a client, as well as the history of their trades.
15 | */
16 | public class Portfolio {
17 |
18 | private static Long INITIAL_DEPOSIT_IN_DOLLARS = 1000L;
19 |
20 | private final Long portfolioId;
21 | private final Long clientId;
22 | private final List history;
23 |
24 | public Portfolio(Long portfolioId, Long clientId) {
25 | this.portfolioId = portfolioId;
26 | this.clientId = clientId;
27 | this.history = new CopyOnWriteArrayList<>();
28 | placeOrder(deposit(INITIAL_DEPOSIT_IN_DOLLARS).dollars());
29 | }
30 |
31 | protected Portfolio(Long portfolioId, Long clientId, List history) {
32 | this.portfolioId = portfolioId;
33 | this.clientId = clientId;
34 | this.history = new CopyOnWriteArrayList<>(history);
35 | }
36 |
37 | public Long getPortfolioId() {
38 | return portfolioId;
39 | }
40 |
41 | public Long getClientId() {
42 | return clientId;
43 | }
44 |
45 | public double getCash() {
46 | return getCashPosition().orElse(Position.EMPTY_CASH_POSITION).getTotalValueInDollars();
47 | }
48 |
49 | private Long getCashInCents() {
50 | return (long) (getCashPosition().orElse(Position.EMPTY_CASH_POSITION).getTotalValueInDollars() * 100);
51 | }
52 |
53 | public void placeOrder(Trade trade) {
54 |
55 | ensureSufficientFundsAreAvailableFor(trade);
56 |
57 | trade.cashTransation().ifPresent( history::add );
58 |
59 | history.add(trade);
60 | }
61 |
62 | public OrderPlacement placeOrderUsingPricesFrom(PriceReader priceReader) {
63 | return new OrderPlacement(priceReader);
64 | }
65 |
66 | public Portfolio withMarketPricesFrom(PriceReader priceReader) {
67 | return new PortfolioWithPositions(portfolioId, clientId, history, priceReader);
68 | }
69 |
70 | public class OrderPlacement {
71 | private PriceReader priceReader;
72 |
73 | OrderPlacement(PriceReader priceReader) {
74 | this.priceReader = priceReader;
75 | }
76 |
77 | public void forTrade(Trade trade) {
78 | if (shouldFindMarketPriceFor(trade)) {
79 | trade = trade.atPrice(priceReader.getPriceFor(trade.getSecurityCode()));
80 | }
81 |
82 | placeOrder(trade);
83 | }
84 | }
85 |
86 | private boolean shouldFindMarketPriceFor(Trade trade) {
87 | return trade.getPriceInCents() == 0;
88 | }
89 |
90 | private void ensureSufficientFundsAreAvailableFor(Trade trade) {
91 |
92 | if (!hasSufficientFundsFor(trade)){
93 | throw new InsufficientFundsException("Insufficient funds: " + getCash() + " for purchase of " + trade.getTotalInCents() / 100);
94 | }
95 | }
96 |
97 | public boolean hasSufficientFundsFor(Trade trade) {
98 | return (trade.getType() == TradeType.Deposit) || (trade.getType() == TradeType.Sell) || ((getCashInCents() >= trade.getTotalInCents()));
99 |
100 | }
101 |
102 | public Map calculatePositionsUsing(PriceReader priceReader) {
103 |
104 | Positions positions = getPositions();
105 |
106 | positions.updateMarketPricesUsing(priceReader);
107 |
108 | return positions.getPositions();
109 | }
110 |
111 | public Double calculateProfitUsing(PriceReader priceReader) {
112 |
113 | Positions positions = getPositions();
114 |
115 | positions.updateMarketPricesUsing(priceReader);
116 |
117 | return calculateProfitOn(positions);
118 | }
119 |
120 | private Optional getCashPosition() {
121 | return Optional.ofNullable(getPositions().getPositions().get(Trade.CASH_ACCOUNT));
122 | }
123 |
124 | public List getHistory() {
125 | return new ArrayList<>(history);
126 | }
127 |
128 |
129 | private Positions getPositions() {
130 | Positions positions = new Positions();
131 |
132 | for (Trade trade : history) {
133 | positions.apply(trade);
134 | }
135 | return positions;
136 | }
137 |
138 | private Double calculateProfitOn(Positions positions) {
139 | return positions.getPositions().values().stream()
140 | .filter(position -> (!position.getSecurityCode().equals(Trade.CASH_ACCOUNT)))
141 | .mapToDouble(Position::getProfit)
142 | .sum();
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/portfolios/PortfolioController.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.portfolios;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import io.swagger.v3.oas.annotations.Operation;
6 | import io.swagger.v3.oas.annotations.tags.Tag;
7 |
8 | import net.bddtrader.config.TraderConfiguration;
9 | import net.bddtrader.config.TradingDataSource;
10 | import net.bddtrader.tradingdata.TradingData;
11 | import net.bddtrader.tradingdata.TradingDataAPI;
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.http.ResponseEntity;
14 | import org.springframework.web.bind.annotation.PathVariable;
15 | import org.springframework.web.bind.annotation.RequestBody;
16 | import org.springframework.web.bind.annotation.RequestMapping;
17 | import org.springframework.web.bind.annotation.RestController;
18 |
19 | import java.util.*;
20 | import java.util.stream.Collectors;
21 |
22 | import static java.util.Comparator.comparing;
23 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
24 | import static org.springframework.web.bind.annotation.RequestMethod.POST;
25 |
26 | @RestController
27 | @Tag(name = "portfolio")
28 | public class PortfolioController {
29 |
30 | private final PortfolioDirectory portfolioDirectory;
31 | private final TradingDataAPI tradingDataAPI;
32 |
33 | public PortfolioController(TradingDataSource tradingDataSource, PortfolioDirectory portfolioDirectory) {
34 | this.portfolioDirectory = portfolioDirectory;
35 | this.tradingDataAPI = TradingData.instanceFor(tradingDataSource);
36 | }
37 |
38 | @Autowired
39 | public PortfolioController(TraderConfiguration traderConfiguration, PortfolioDirectory portfolioDirectory) {
40 | this(traderConfiguration.getTradingDataSource(), portfolioDirectory);
41 | }
42 |
43 | @RequestMapping(value = "/api/client/{clientId}/portfolio", method = POST)
44 | @Operation(summary = "Create a portfolio for a client")
45 | public Portfolio createPortfolioForClient(@PathVariable Long clientId) {
46 | return portfolioDirectory.addPortfolioFor(clientId);
47 | }
48 |
49 | @RequestMapping(value = "/api/client/{clientId}/portfolio", method = GET)
50 | @Operation(summary = "View the portfolio of a client")
51 | public Portfolio viewPortfolioForClient(@PathVariable Long clientId) {
52 |
53 | Portfolio portfolioFound = portfolioDirectory.findByClientId(clientId)
54 | .orElseThrow(() -> new PortfolioNotFoundException("No portfolio found for client id " + clientId));
55 |
56 | return portfolioFound.withMarketPricesFrom(tradingDataAPI);
57 | }
58 |
59 | @RequestMapping(value = "/api/client/{clientId}/portfolio/positions", method = GET)
60 | @Operation(summary = "View the portfolio positions of a client")
61 | public List viewPortfolioPositionsForClient(@PathVariable Long clientId) {
62 |
63 | Portfolio portfolioFound = portfolioDirectory.findByClientId(clientId)
64 | .orElseThrow(() -> new PortfolioNotFoundException("No portfolio found for client id " + clientId));
65 |
66 | return getPositions(portfolioFound.getPortfolioId());
67 | }
68 |
69 |
70 | @RequestMapping(value = "/api/portfolio/{portfolioId}", method = GET)
71 | @Operation(summary = "View a portfolio with a given ID")
72 | public Portfolio viewPortfolio(@PathVariable Long portfolioId) {
73 |
74 | return findById(portfolioId).withMarketPricesFrom(tradingDataAPI);
75 | }
76 |
77 | @RequestMapping(value = "/api/portfolio/{portfolioId}/order", method = POST)
78 | @Operation(summary = "Place an order for a trade in a given portfolio",
79 | description="Use the special CASH security code to deposit more money into the portfolio")
80 | public Portfolio placeOrder(@PathVariable Long portfolioId,
81 | @RequestBody Trade trade) {
82 |
83 | Portfolio foundPortfolio = findById(portfolioId);
84 |
85 | foundPortfolio.placeOrderUsingPricesFrom(tradingDataAPI).forTrade(trade);
86 |
87 | return foundPortfolio;
88 | }
89 |
90 | @RequestMapping(value = "/api/portfolio/{portfolioId}/history", method = GET)
91 | @Operation(summary = "See the history of all the trades made in this portfolio")
92 | public List getHistory(@PathVariable Long portfolioId) {
93 |
94 | return findById(portfolioId).getHistory();
95 | }
96 |
97 | @RequestMapping(value = "/api/portfolio/{portfolioId}/positions", method = GET)
98 | @Operation(summary = "Get the current positions for a given portfolio")
99 | public List getPositions(@PathVariable Long portfolioId) {
100 | return getIndexedPositions(portfolioId).values()
101 | .stream()
102 | .sorted(comparing(Position::getSecurityCode))
103 | .collect(Collectors.toList());
104 | }
105 |
106 | @RequestMapping(value = "/api/portfolio/{portfolioId}/indexed-positions", method = GET)
107 | @Operation(summary = "Get the current positions for a given portfolio as a Map")
108 | public Map getIndexedPositions(@PathVariable Long portfolioId) {
109 | return findById(portfolioId).calculatePositionsUsing(tradingDataAPI);
110 | }
111 |
112 | @RequestMapping(value = "/api/portfolio/{portfolioId}/profit", method = GET)
113 | @Operation(summary = "Get the overall profit or loss value for a given portfolio")
114 | public Double getProfitAndLoss(@PathVariable Long portfolioId) {
115 |
116 | return findById(portfolioId).calculateProfitUsing(tradingDataAPI);
117 | }
118 |
119 | private Portfolio findById(@PathVariable Long portfolioId) {
120 | return portfolioDirectory.findById(portfolioId)
121 | .orElseThrow(() -> new PortfolioNotFoundException("No portfolio found for id " + portfolioId));
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/portfolios/PortfolioDirectory.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.portfolios;
2 |
3 | import org.springframework.stereotype.Component;
4 |
5 | import java.util.List;
6 | import java.util.Optional;
7 | import java.util.concurrent.CopyOnWriteArrayList;
8 | import java.util.concurrent.atomic.AtomicLong;
9 |
10 | /**
11 | * An in-memory directory of registered clients.
12 | */
13 | @Component
14 | public class PortfolioDirectory {
15 |
16 | private final List portfolios = new CopyOnWriteArrayList<>();
17 | private final AtomicLong portfolioCount = new AtomicLong(1);
18 |
19 | public Portfolio addPortfolioFor(Long clientId) {
20 | Portfolio portfolio = new Portfolio(portfolioCount.getAndIncrement(), clientId);
21 | portfolios.add(portfolio);
22 | return portfolio;
23 | }
24 |
25 | public Optional findByClientId(Long clientId) {
26 | return portfolios.stream()
27 | .filter( portfolio -> portfolio.getClientId().equals(clientId))
28 | .findFirst();
29 | }
30 |
31 | public Optional findById(Long portfolioId) {
32 | return portfolios.stream()
33 | .filter( portfolio -> portfolio.getPortfolioId().equals(portfolioId))
34 | .findFirst();
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/portfolios/PortfolioNotFoundException.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.portfolios;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(value = HttpStatus.NOT_FOUND)
7 | public class PortfolioNotFoundException extends RuntimeException {
8 | PortfolioNotFoundException(String message) {
9 | super(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/portfolios/PortfolioWithPositions.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.portfolios;
2 |
3 | import net.bddtrader.tradingdata.PriceReader;
4 |
5 | import java.util.List;
6 | import java.util.Map;
7 |
8 | public class PortfolioWithPositions extends Portfolio {
9 |
10 | private Map marketPositions;
11 | private Double profit;
12 |
13 | public PortfolioWithPositions(Long portfolioId, Long clientId, List history, PriceReader priceReader) {
14 | super(portfolioId, clientId, history);
15 |
16 | marketPositions = calculatePositionsUsing(priceReader);
17 | profit = calculateProfitUsing(priceReader);
18 | }
19 |
20 | public Map getMarketPositions() {
21 | return marketPositions;
22 | }
23 |
24 | public Double getProfit() {
25 | return profit;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/status/StatusController.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.status;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import net.bddtrader.config.TraderConfiguration;
6 | import net.bddtrader.config.TradingDataSource;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.web.bind.annotation.RequestMapping;
9 | import org.springframework.web.bind.annotation.RestController;
10 |
11 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
12 |
13 | @RestController
14 | @Tag(name = "status")
15 | public class StatusController {
16 |
17 | private final TradingDataSource tradingDataSource;
18 |
19 | public StatusController(TradingDataSource tradingDataSource) {
20 | this.tradingDataSource = tradingDataSource;
21 | }
22 |
23 | @Autowired
24 | public StatusController(TraderConfiguration traderConfiguration) {
25 | this(traderConfiguration.getTradingDataSource());
26 | }
27 |
28 | @RequestMapping(value = "/api/status", method = GET)
29 | @Operation(summary = "Check the status of the API")
30 | public String status() {
31 | return "BDDTrader running against " + tradingDataSource;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/stocks/StockController.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.stocks;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import net.bddtrader.config.TraderConfiguration;
6 | import net.bddtrader.config.TradingDataSource;
7 | import net.bddtrader.tradingdata.TradingData;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.web.bind.annotation.*;
10 |
11 | import java.util.List;
12 |
13 | import static org.springframework.web.bind.annotation.RequestMethod.GET;
14 |
15 | @RestController
16 | @Tag(name = "stock")
17 | public class StockController {
18 |
19 | private final TradingDataSource tradingDataSource;
20 |
21 | public StockController(TradingDataSource tradingDataSource) {
22 | this.tradingDataSource = tradingDataSource;
23 | }
24 |
25 | @Autowired
26 | public StockController(TraderConfiguration traderConfiguration) {
27 | this(traderConfiguration.getTradingDataSource());
28 | }
29 |
30 | @RequestMapping(value="/api/stock/{stockid}/price", method = GET)
31 | @Operation(summary = "Find the price of a given stock")
32 | public Double priceFor(@PathVariable String stockid) {
33 | return TradingData.instanceFor(tradingDataSource).getPriceFor(stockid);
34 | }
35 |
36 | @RequestMapping(value = "/api/stock/{stockid}/price", method = RequestMethod.POST)
37 | @Operation(summary = "Update the price of a given stock in a test environment")
38 | public void updatePriceFor(@PathVariable String stockid, @RequestBody Double currentPrice) {
39 | TradingData.instanceFor(tradingDataSource).updatePriceFor(stockid, currentPrice);
40 | }
41 |
42 | @RequestMapping(value = "/api/stocks/reset", method = RequestMethod.POST)
43 | @Operation(summary = "Reset the prices to default values the stock in a test environment")
44 | public void resetTestPrices() {
45 | TradingData.instanceFor(tradingDataSource).reset();
46 | }
47 |
48 | @RequestMapping(value = "/api/stock/popular", method = RequestMethod.GET)
49 | @Operation(summary = "List high volume stocks")
50 | public List getPopularStocks() {
51 | return TradingData.instanceFor(tradingDataSource).getPopularStocks();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/tradingdata/TradingData.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.tradingdata;
2 |
3 | import net.bddtrader.config.TradingDataSource;
4 | import net.bddtrader.tradingdata.services.IEXtradingAPI;
5 | import net.bddtrader.tradingdata.services.StaticAPI;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 |
11 | /**
12 | * Return trading data either from a remote web service, or from static local data for testing.
13 | * The source of that trading data is defined by the data.source system property defined in the
14 | * application.conf file.
15 | * This value can be:
16 | * * IEX - The IEX trading API (https://iextrading.com/developer/docs/)
17 | * * DEV - Test data
18 | */
19 | public class TradingData {
20 |
21 | private static Map TRADING_DATA_SOURCE_API = new HashMap<>();
22 | static {
23 | // TRADING_DATA_SOURCE_API.put(TradingDataSource.IEX, new IEXtradingAPI());
24 | TRADING_DATA_SOURCE_API.put(TradingDataSource.DEV, new StaticAPI());
25 | }
26 |
27 | private final static TradingDataAPI DEFAULT_DATA_SOURCE = new StaticAPI();
28 |
29 | public static TradingDataAPI instanceFor(TradingDataSource dataSource) {
30 | return TRADING_DATA_SOURCE_API.getOrDefault(dataSource, DEFAULT_DATA_SOURCE);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/tradingdata/TradingDataAPI.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.tradingdata;
2 |
3 | public interface TradingDataAPI extends PriceReader, PriceUpdater, NewsReader {
4 | void reset();
5 | }
6 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/tradingdata/exceptions/IllegalPriceManipulationException.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.tradingdata.exceptions;
2 |
3 | public class IllegalPriceManipulationException extends RuntimeException {
4 | public IllegalPriceManipulationException(String message) {
5 | super(message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/tradingdata/services/IEXtradingAPI.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.tradingdata.services;
2 |
3 | import net.bddtrader.news.NewsItem;
4 | import net.bddtrader.portfolios.Trade;
5 | import net.bddtrader.stocks.TopStock;
6 | import net.bddtrader.tradingdata.TradingDataAPI;
7 | import net.bddtrader.tradingdata.exceptions.IllegalPriceManipulationException;
8 | import org.springframework.core.ParameterizedTypeReference;
9 | import org.springframework.http.ResponseEntity;
10 | import org.springframework.web.client.RestTemplate;
11 |
12 | import java.util.List;
13 | import java.util.stream.Collectors;
14 |
15 | import static java.util.Objects.requireNonNull;
16 | import static org.springframework.http.HttpMethod.GET;
17 |
18 | public class IEXtradingAPI implements TradingDataAPI {
19 |
20 | private final RestTemplate restTemplate = new RestTemplate();
21 |
22 | @Override
23 | public List getNewsFor(String stockid) {
24 | return new StaticAPI().getNewsFor(stockid);
25 | }
26 |
27 | @Override
28 | public Double getPriceFor(String stockid) {
29 |
30 | if (stockid.equals(Trade.CASH_ACCOUNT)) {
31 | return 0.01;
32 | }
33 |
34 | ResponseEntity response =
35 | restTemplate.exchange("https://api.iextrading.com/1.0/stock/{stockid}/price",
36 | GET, null,
37 | new ParameterizedTypeReference() {},
38 | stockid);
39 | return response.getBody();
40 | }
41 |
42 | @Override
43 | public List getPopularStocks() {
44 |
45 | ResponseEntity> response =
46 | restTemplate.exchange("https://api.iextrading.com/1.0/tops",
47 | GET, null,
48 | new ParameterizedTypeReference>() {});
49 |
50 | return requireNonNull(response.getBody()).stream()
51 | .map(TopStock::getSymbol)
52 | .collect(Collectors.toList());
53 | }
54 |
55 | @Override
56 | public void updatePriceFor(String stockid, Double currentPrice) {
57 | throw new IllegalPriceManipulationException("Attempt to update prices in production");
58 | }
59 |
60 | @Override
61 | public void reset() {}
62 | }
63 |
--------------------------------------------------------------------------------
/bddtrader-app/src/main/java/net/bddtrader/tradingdata/services/StaticAPI.java:
--------------------------------------------------------------------------------
1 | package net.bddtrader.tradingdata.services;
2 |
3 | import com.fasterxml.jackson.core.type.TypeReference;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
6 | import net.bddtrader.news.NewsItem;
7 | import net.bddtrader.portfolios.Trade;
8 | import net.bddtrader.tradingdata.TradingDataAPI;
9 | import org.springframework.web.client.HttpClientErrorException;
10 |
11 | import java.io.File;
12 | import java.io.IOException;
13 | import java.util.*;
14 | import java.util.stream.Collectors;
15 |
16 | public class StaticAPI implements TradingDataAPI {
17 |
18 | private final ObjectMapper mapper;
19 | Map stockPrices;
20 |
21 | public StaticAPI() {
22 | mapper = new ObjectMapper();
23 | mapper.registerModule(new JavaTimeModule());
24 |
25 | stockPrices = loadSamplePrices();
26 | }
27 |
28 | @Override
29 | public List getNewsFor(String stockid) {
30 | File jsonInput = testDataFrom("news.json");
31 | try {
32 | List items = mapper.readValue(jsonInput, new TypeReference>(){});
33 | return items.stream()
34 | .filter( item -> item.getRelated().contains(stockid))
35 | .collect(Collectors.toList());
36 |
37 |
38 | } catch (IOException e) {
39 | e.printStackTrace();
40 | return new ArrayList<>();
41 | }
42 | }
43 |
44 |
45 | private Map loadSamplePrices() {
46 | File jsonInput = testDataFrom("prices.json");
47 | try {
48 | Map> samplePrices = mapper.readValue(jsonInput, new TypeReference