diff --git a/.gitignore b/.gitignore index fdea67d..bb9c3ce 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,26 @@ nb-configuration.xml ## OS X ############################## .DS_Store + +############################## +## Build output +############################## +/target/ +/build/ +/dist/ +/out/ + +############################## +## Log files +############################## +*.log +logs/ + +############################## +## User-specific configuration +############################## +*.properties +*.yml +*.yaml +.env +credentials.* diff --git a/README.md b/README.md index babcc67..6562f01 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,97 @@ -# SmartAPI 2.2.0 Java client +# SmartAPI 2.2.6 Java client The official Java client for communicating with [SmartAPI Connect API](https://smartapi.angelbroking.com). SmartAPI is a set of REST-like APIs that expose many capabilities required to build a complete investment and trading platform. Execute orders in real-time, manage user portfolios, stream live market data (WebSockets), and more, with the simple HTTP API collection. +This project also includes a comprehensive **JavaFX GUI application** that provides a visual interface for most API features, including charting and strategy backtesting. + ## Documentation -- [SmartAPI - HTTP API documentation] (https://smartapi.angelbroking.com/docs) +- [SmartAPI - HTTP API documentation](https://smartapi.angelbroking.com/docs) - [Java library documentation](https://smartapi.angelbroking.com/docs) -## Usage +## Usage of the Core Library - [Download SmartAPI jar file](https://github.com/angel-one/smartapi-java/blob/main/dist/) and include it in your build path. -- Include com.angelbroking.smartapi into build path from maven. Use version 2.2.0 +- Include com.angelbroking.smartapi into build path from maven. Use version 2.2.6 + +--- + +## SmartAPI Java GUI + +This repository includes a powerful, feature-rich desktop application built with JavaFX. It provides a user-friendly interface to interact with the SmartAPI, visualize data, and backtest trading strategies. + +### Features +- **Secure Login:** Authenticate using your Angel One credentials and TOTP. +- **Dashboard:** View real-time funds, positions, and holdings. +- **Live Order Book:** See all your open, executed, and rejected orders with options to modify or cancel. +- **Interactive Charting:** + - View live and historical candlestick charts for any instrument. + - Detach charts into separate windows for multi-monitor setups. +- **Strategy Backtesting:** + - Test trading strategies (e.g., RSI, MACD) against historical data. + - View detailed performance reports and equity curves. + - Visualize trade entry/exit points directly on the chart. +- **Place & Modify Orders:** A simple interface to place new orders or modify existing ones. +- **Light & Dark Themes:** Switch between themes for comfortable viewing. + +### Screenshots + + + +**Login Window** + +![Screenshot 2025-06-18 001113](https://github.com/user-attachments/assets/0def4c50-30fc-4c3e-8ca1-2511f64f61f0) + + +**Dashboard** +![Screenshot 2025-06-18 001852](https://github.com/user-attachments/assets/bb822727-957a-4a12-9d2a-ac03aff4bf92) + + +### How to Run the GUI + +#### Option 1: Run from Source (Recommended for Developers) +This is the quickest way to run the application directly from the source code using Maven, without needing to package it first. + +1. Ensure you have JDK 11 or newer and Maven installed. +2. Open a terminal in the project's root directory and run: + ```bash + mvn javafx:run + ``` + The application will compile and launch automatically. + +#### Option 2: Running the Executable JAR +After building the project with Maven, you can run the application directly from the command line. + +1. Build the project to create a "fat JAR": + ```bash + mvn clean package + ``` +2. Run the JAR file (ensure you are using JDK 11 or newer): + ```bash + java -jar target/smartapi-java-2.2.6.jar + ``` + +#### Option 3: Building a Native Windows Executable (.exe) +You can create a standalone `.exe` file that bundles the Java runtime, so users don't need to have Java installed. + +1. Ensure you have JDK 14 or newer and that it's added to your system's PATH. +2. Build the fat JAR using Maven: + ```bash + mvn clean package + ``` +3. Run `jpackage` to create the executable. Open a new terminal and run: + ```powershell + jpackage --type exe ` + --input target/ ` + --name "Smart API GUI" ` + --main-jar smartapi-java-2.2.6.jar ` + --main-class com.angelbroking.smartapi.gui.SmartApiGui ` + --dest dist ` + --win-console + ``` +4. The final application will be located in the `dist/Smart API GUI` folder. You can zip this folder and distribute it. + +--- ## API usage ```java @@ -510,8 +591,7 @@ For more details, take a look at Examples.java in the sample directory. log.info("order update data {} ",data); - }}); - + }}); } ``` diff --git a/error.log b/error.log index e69de29..9226d2b 100644 --- a/error.log +++ b/error.log @@ -0,0 +1,379 @@ +2025-06-16 02:18:59.248 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in GET request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/user/v1/getProfile, Request Headers: User-Agent: javasmartapiconnect/3.0.0 +Authorization: ██ +Content-Type: application/json +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +,Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:18:59.279 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:19:00.779 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in POST request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/order/v1/placeOrder, Request Headers: Content-Type: application/json +Authorization: ██ +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +, Request Body: {"duration":"DAY","quantity":1,"triggerprice":"209","variety":"STOPLOSS","price":122.2,"tradingsymbol":"ITC-EQ","exchange":"NSE","transactiontype":"BUY","symboltoken":"1660","producttype":"INTRADAY","ordertype":"STOPLOSS_LIMIT"},Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:19:00.790 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:19:01.939 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in POST request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/order/v1/modifyOrder, Request Headers: Content-Type: application/json +Authorization: ██ +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +, Request Body: {"duration":"DAY","quantity":1,"variety":"NORMAL","orderid":"201216000755110","price":122.2,"tradingsymbol":"ASHOKLEY","exchange":"NSE","symboltoken":"3045","producttype":"DELIVERY","ordertype":"LIMIT"},Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:19:01.942 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:19:02.905 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in POST request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/order/v1/cancelOrder, Request Headers: Content-Type: application/json +Authorization: ██ +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +, Request Body: {"variety":"NORMAL","orderid":"201009000000015"},Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:19:02.909 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:19:02.912 [main] ERROR c.a.smartapi.sample.APITest - Exception: null +2025-06-16 02:43:55.639 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in GET request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/user/v1/getProfile, Request Headers: User-Agent: javasmartapiconnect/3.0.0 +Authorization: ██ +Content-Type: application/json +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +,Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:43:55.645 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:43:55.736 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in POST request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/order/v1/placeOrder, Request Headers: Content-Type: application/json +Authorization: ██ +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +, Request Body: {"duration":"DAY","quantity":1,"triggerprice":"209","variety":"STOPLOSS","price":122.2,"tradingsymbol":"ITC-EQ","exchange":"NSE","transactiontype":"BUY","symboltoken":"1660","producttype":"INTRADAY","ordertype":"STOPLOSS_LIMIT"},Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:43:55.750 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:43:55.820 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in POST request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/order/v1/modifyOrder, Request Headers: Content-Type: application/json +Authorization: ██ +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +, Request Body: {"duration":"DAY","quantity":1,"variety":"NORMAL","orderid":"201216000755110","price":122.2,"tradingsymbol":"ASHOKLEY","exchange":"NSE","symboltoken":"3045","producttype":"DELIVERY","ordertype":"LIMIT"},Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:43:55.823 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:43:55.896 [main] ERROR c.a.s.http.SmartAPIRequestHandler - Error in POST request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/order/v1/cancelOrder, Request Headers: Content-Type: application/json +Authorization: ██ +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: +X-UserType: USER +X-SourceID: WEB +, Request Body: {"variety":"NORMAL","orderid":"201009000000015"},Response : {"success":false,"message":"Invalid Token","errorCode":"AG8001","data":""} +2025-06-16 02:43:55.898 [main] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 02:43:55.900 [main] ERROR c.a.smartapi.sample.APITest - Exception: null +2025-06-16 03:12:47.332 [Thread-4] ERROR c.angelbroking.smartapi.SmartConnect - JSONObject["data"] is not a JSONObject (class org.json.JSONObject$Null : null). +2025-06-16 03:50:55.926 [Thread-3] ERROR c.angelbroking.smartapi.SmartConnect - JSONObject["data"] is not a JSONObject (class org.json.JSONObject$Null : null). +2025-06-16 05:12:35.160 [Thread-16] ERROR c.a.s.http.SmartAPIRequestHandler - Error in POST request. Request URL: https://apiconnect.angelone.in/rest/secure/angelbroking/portfolio/v1/getAllHolding, Request Headers: User-Agent: javasmartapiconnect/3.0.0 +Authorization: ██ +Content-Type: application/json +X-ClientLocalIP: 192.168.31.22 +X-ClientPublicIP: 27.59.111.24 +X-MACAddress: 2C-58-B9-F0-92-90 +Accept: application/json +X-PrivateKey: HFnOUXEQ +X-UserType: USER +X-SourceID: WEB +,Response : Unrecognized token 'Access': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false') + at [Source: (String)"Access denied because of exceeding access rate"; line: 1, column: 7] +2025-06-16 05:12:35.161 [Thread-16] ERROR c.angelbroking.smartapi.SmartConnect - IOException occurred while getting all holdings Unrecognized token 'Access': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false') + at [Source: (String)"Access denied because of exceeding access rate"; line: 1, column: 7] +2025-06-16 05:22:51.436 [Thread-5] ERROR c.angelbroking.smartapi.SmartConnect - JSONObject["data"] is not a JSONArray (class org.json.JSONObject$Null : null). +2025-06-16 05:26:54.385 [Thread-5] ERROR c.angelbroking.smartapi.SmartConnect - Error fetching candle data from API: {"data":null,"message":"Something Went Wrong, Please Try After Sometime","errorcode":"AB1004","status":false} +2025-06-16 05:33:52.575 [Thread-5] ERROR c.angelbroking.smartapi.SmartConnect - Error fetching candle data from API: Something Went Wrong, Please Try After Sometime +2025-06-16 05:33:52.577 [Thread-5] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 05:33:53.454 [Thread-6] ERROR c.angelbroking.smartapi.SmartConnect - Error fetching candle data from API: Something Went Wrong, Please Try After Sometime +2025-06-16 05:33:53.456 [Thread-6] ERROR c.angelbroking.smartapi.SmartConnect - null +2025-06-16 05:45:35.671 [Thread-3] ERROR c.angelbroking.smartapi.SmartConnect - JSONObject["data"] is not a JSONObject (class org.json.JSONObject$Null : null). +2025-06-16 14:48:53.717 [Thread-15] ERROR c.angelbroking.smartapi.SmartConnect - Error fetching candle data from API: Something Went Wrong, Please Try After Sometime +2025-06-16 14:48:53.718 [Thread-15] ERROR c.angelbroking.smartapi.SmartConnect - SmartAPIException in candleData: null +2025-06-16 14:48:57.814 [Thread-16] ERROR c.angelbroking.smartapi.SmartConnect - Error fetching candle data from API: Something Went Wrong, Please Try After Sometime +2025-06-16 14:48:57.814 [Thread-16] ERROR c.angelbroking.smartapi.SmartConnect - SmartAPIException in candleData: null +2025-06-16 15:34:21.304 [JavaFX Application Thread] ERROR c.angelbroking.smartapi.SmartConnect - WebSocket connection failed: java.lang.IllegalArgumentException: invalid URI scheme: https +java.util.concurrent.CompletionException: java.lang.IllegalArgumentException: invalid URI scheme: https + at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:325) + at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:378) + at java.base/java.util.concurrent.CompletableFuture.uniAcceptNow(CompletableFuture.java:793) + at java.base/java.util.concurrent.CompletableFuture.uniAcceptStage(CompletableFuture.java:781) + at java.base/java.util.concurrent.CompletableFuture.thenAccept(CompletableFuture.java:2266) + at com.angelbroking.smartapi.SmartConnect.connectStream(SmartConnect.java:1162) + at com.angelbroking.smartapi.gui.SmartApiGui.startLiveFeed(SmartApiGui.java:1519) + at com.angelbroking.smartapi.gui.SmartApiGui.lambda$80(SmartApiGui.java:1341) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189) + at javafx.base@21/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49) + at javafx.base@21/javafx.event.Event.fireEvent(Event.java:198) + at javafx.graphics@21/javafx.scene.Node.fireEvent(Node.java:8875) + at javafx.controls@21/javafx.scene.control.CheckBox.fire(CheckBox.java:240) + at javafx.controls@21/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:207) + at javafx.controls@21/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189) + at javafx.base@21/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54) + at javafx.base@21/javafx.event.Event.fireEvent(Event.java:198) + at javafx.graphics@21/javafx.scene.Scene$MouseHandler.process(Scene.java:3984) + at javafx.graphics@21/javafx.scene.Scene.processMouseEvent(Scene.java:1890) + at javafx.graphics@21/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2708) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:411) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:301) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:138) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:450) + at javafx.graphics@21/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:449) + at javafx.graphics@21/com.sun.glass.ui.View.handleMouseEvent(View.java:551) + at javafx.graphics@21/com.sun.glass.ui.View.notifyMouse(View.java:937) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication._runLoop(Native Method) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:185) + at java.base/java.lang.Thread.run(Thread.java:1447) +Caused by: java.lang.IllegalArgumentException: invalid URI scheme: https + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.illegal(OpeningHandshake.java:346) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.checkURI(OpeningHandshake.java:337) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.(OpeningHandshake.java:107) + at java.net.http/jdk.internal.net.http.websocket.WebSocketImpl.newInstanceAsync(WebSocketImpl.java:139) + at java.net.http/jdk.internal.net.http.websocket.BuilderImpl.buildAsync(BuilderImpl.java:120) + at com.angelbroking.smartapi.SmartConnect.connectStream(SmartConnect.java:1161) + ... 47 common frames omitted +2025-06-16 15:34:35.253 [JavaFX Application Thread] ERROR c.angelbroking.smartapi.SmartConnect - WebSocket connection failed: java.lang.IllegalArgumentException: invalid URI scheme: https +java.util.concurrent.CompletionException: java.lang.IllegalArgumentException: invalid URI scheme: https + at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:325) + at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:378) + at java.base/java.util.concurrent.CompletableFuture.uniAcceptNow(CompletableFuture.java:793) + at java.base/java.util.concurrent.CompletableFuture.uniAcceptStage(CompletableFuture.java:781) + at java.base/java.util.concurrent.CompletableFuture.thenAccept(CompletableFuture.java:2266) + at com.angelbroking.smartapi.SmartConnect.connectStream(SmartConnect.java:1162) + at com.angelbroking.smartapi.gui.SmartApiGui.startLiveFeed(SmartApiGui.java:1519) + at com.angelbroking.smartapi.gui.SmartApiGui.lambda$80(SmartApiGui.java:1341) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189) + at javafx.base@21/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49) + at javafx.base@21/javafx.event.Event.fireEvent(Event.java:198) + at javafx.graphics@21/javafx.scene.Node.fireEvent(Node.java:8875) + at javafx.controls@21/javafx.scene.control.CheckBox.fire(CheckBox.java:240) + at javafx.controls@21/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:207) + at javafx.controls@21/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189) + at javafx.base@21/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54) + at javafx.base@21/javafx.event.Event.fireEvent(Event.java:198) + at javafx.graphics@21/javafx.scene.Scene$MouseHandler.process(Scene.java:3984) + at javafx.graphics@21/javafx.scene.Scene.processMouseEvent(Scene.java:1890) + at javafx.graphics@21/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2708) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:411) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:301) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:138) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:450) + at javafx.graphics@21/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:449) + at javafx.graphics@21/com.sun.glass.ui.View.handleMouseEvent(View.java:551) + at javafx.graphics@21/com.sun.glass.ui.View.notifyMouse(View.java:937) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication._runLoop(Native Method) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:185) + at java.base/java.lang.Thread.run(Thread.java:1447) +Caused by: java.lang.IllegalArgumentException: invalid URI scheme: https + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.illegal(OpeningHandshake.java:346) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.checkURI(OpeningHandshake.java:337) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.(OpeningHandshake.java:107) + at java.net.http/jdk.internal.net.http.websocket.WebSocketImpl.newInstanceAsync(WebSocketImpl.java:139) + at java.net.http/jdk.internal.net.http.websocket.BuilderImpl.buildAsync(BuilderImpl.java:120) + at com.angelbroking.smartapi.SmartConnect.connectStream(SmartConnect.java:1161) + ... 47 common frames omitted +2025-06-16 15:35:55.249 [JavaFX Application Thread] ERROR c.angelbroking.smartapi.SmartConnect - WebSocket connection failed: java.lang.IllegalArgumentException: invalid URI scheme: https +java.util.concurrent.CompletionException: java.lang.IllegalArgumentException: invalid URI scheme: https + at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:325) + at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:378) + at java.base/java.util.concurrent.CompletableFuture.uniAcceptNow(CompletableFuture.java:793) + at java.base/java.util.concurrent.CompletableFuture.uniAcceptStage(CompletableFuture.java:781) + at java.base/java.util.concurrent.CompletableFuture.thenAccept(CompletableFuture.java:2266) + at com.angelbroking.smartapi.SmartConnect.connectStream(SmartConnect.java:1162) + at com.angelbroking.smartapi.gui.SmartApiGui.startLiveFeed(SmartApiGui.java:1519) + at com.angelbroking.smartapi.gui.SmartApiGui.lambda$80(SmartApiGui.java:1341) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189) + at javafx.base@21/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49) + at javafx.base@21/javafx.event.Event.fireEvent(Event.java:198) + at javafx.graphics@21/javafx.scene.Node.fireEvent(Node.java:8875) + at javafx.controls@21/javafx.scene.control.CheckBox.fire(CheckBox.java:240) + at javafx.controls@21/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:207) + at javafx.controls@21/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247) + at javafx.base@21/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232) + at javafx.base@21/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189) + at javafx.base@21/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56) + at javafx.base@21/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74) + at javafx.base@21/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54) + at javafx.base@21/javafx.event.Event.fireEvent(Event.java:198) + at javafx.graphics@21/javafx.scene.Scene$MouseHandler.process(Scene.java:3984) + at javafx.graphics@21/javafx.scene.Scene.processMouseEvent(Scene.java:1890) + at javafx.graphics@21/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2708) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:411) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:301) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:138) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:450) + at javafx.graphics@21/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424) + at javafx.graphics@21/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:449) + at javafx.graphics@21/com.sun.glass.ui.View.handleMouseEvent(View.java:551) + at javafx.graphics@21/com.sun.glass.ui.View.notifyMouse(View.java:937) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication._runLoop(Native Method) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:185) + at java.base/java.lang.Thread.run(Thread.java:1447) +Caused by: java.lang.IllegalArgumentException: invalid URI scheme: https + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.illegal(OpeningHandshake.java:346) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.checkURI(OpeningHandshake.java:337) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.(OpeningHandshake.java:107) + at java.net.http/jdk.internal.net.http.websocket.WebSocketImpl.newInstanceAsync(WebSocketImpl.java:139) + at java.net.http/jdk.internal.net.http.websocket.BuilderImpl.buildAsync(BuilderImpl.java:120) + at com.angelbroking.smartapi.SmartConnect.connectStream(SmartConnect.java:1161) + ... 47 common frames omitted +2025-06-16 15:42:21.960 [ForkJoinPool.commonPool-worker-1] ERROR c.angelbroking.smartapi.SmartConnect - WebSocket connection failed: java.net.http.WebSocketHandshakeException +java.util.concurrent.CompletionException: java.net.http.WebSocketHandshakeException + at java.base/java.util.concurrent.CompletableFuture.wrapInCompletionException(CompletableFuture.java:325) + at java.base/java.util.concurrent.CompletableFuture.encodeRelay(CompletableFuture.java:414) + at java.base/java.util.concurrent.CompletableFuture.completeRelay(CompletableFuture.java:423) + at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1198) + at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:556) + at java.base/java.util.concurrent.CompletableFuture.postFire(CompletableFuture.java:660) + at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:890) + at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:529) + at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:507) + at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1394) + at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1970) + at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:187) +Caused by: java.net.http.WebSocketHandshakeException: null + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.resultFrom(OpeningHandshake.java:214) + at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1196) + ... 8 common frames omitted +Caused by: jdk.internal.net.http.websocket.CheckFailedException: Unexpected HTTP response status code 401 + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.checkFailed(OpeningHandshake.java:331) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.handleResponse(OpeningHandshake.java:240) + at java.net.http/jdk.internal.net.http.websocket.OpeningHandshake.resultFrom(OpeningHandshake.java:210) + ... 9 common frames omitted +2025-06-16 20:21:32.749 [JavaFX Application Thread] WARN c.a.smartapi.gui.SmartApiGui - chartWebEngine is null. Cannot send data to chart. +2025-06-16 22:32:25.492 [Thread-43] ERROR c.angelbroking.smartapi.SmartConnect - Error fetching candle data from API: Something Went Wrong, Please Try After Sometime +2025-06-16 22:32:25.492 [Thread-43] ERROR c.angelbroking.smartapi.SmartConnect - SmartAPIException in candleData: null +2025-06-16 22:32:25.497 [Thread-43] ERROR c.a.smartapi.gui.SmartApiGui - Backtest API/IO/JSON error +com.angelbroking.smartapi.http.exceptions.SmartAPIException: null + at com.angelbroking.smartapi.SmartConnect.candleData(SmartConnect.java:776) + at com.angelbroking.smartapi.gui.SmartApiGui.lambda$100(SmartApiGui.java:1704) + at java.base/java.lang.Thread.run(Thread.java:1447) +2025-06-16 22:32:31.082 [Thread-45] ERROR c.angelbroking.smartapi.SmartConnect - Error fetching candle data from API: Something Went Wrong, Please Try After Sometime +2025-06-16 22:32:31.083 [Thread-45] ERROR c.angelbroking.smartapi.SmartConnect - SmartAPIException in candleData: null +2025-06-16 22:32:31.083 [Thread-45] ERROR c.a.smartapi.gui.SmartApiGui - Backtest API/IO/JSON error +com.angelbroking.smartapi.http.exceptions.SmartAPIException: null + at com.angelbroking.smartapi.SmartConnect.candleData(SmartConnect.java:776) + at com.angelbroking.smartapi.gui.SmartApiGui.lambda$100(SmartApiGui.java:1704) + at java.base/java.lang.Thread.run(Thread.java:1447) +2025-06-17 20:26:11.799 [JavaFX Application Thread] ERROR c.a.smartapi.gui.SmartApiGui - Error sending comprehensive backtest data to chart +netscape.javascript.JSException: ReferenceError: Can't find variable: plotBacktestData + at javafx.web@21/com.sun.webkit.dom.JSObject.fwkMakeException(JSObject.java:160) + at javafx.web@21/com.sun.webkit.WebPage.twkExecuteScript(Native Method) + at javafx.web@21/com.sun.webkit.WebPage.executeScript(WebPage.java:1576) + at javafx.web@21/javafx.scene.web.WebEngine.executeScript(WebEngine.java:985) + at com.angelbroking.smartapi.gui.SmartApiGui.sendBacktestDataToChart(SmartApiGui.java:2092) + at com.angelbroking.smartapi.gui.SmartApiGui.lambda$createBacktestingTab$9(SmartApiGui.java:1880) + at javafx.graphics@21/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:456) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:138) + at javafx.graphics@21/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:455) + at javafx.graphics@21/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication._runLoop(Native Method) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:185) + at java.base/java.lang.Thread.run(Thread.java:1447) +2025-06-17 20:26:28.169 [JavaFX Application Thread] ERROR c.a.smartapi.gui.SmartApiGui - Error sending comprehensive backtest data to chart +netscape.javascript.JSException: ReferenceError: Can't find variable: Plotly + at javafx.web@21/com.sun.webkit.dom.JSObject.fwkMakeException(JSObject.java:160) + at javafx.web@21/com.sun.webkit.WebPage.twkExecuteScript(Native Method) + at javafx.web@21/com.sun.webkit.WebPage.executeScript(WebPage.java:1576) + at javafx.web@21/javafx.scene.web.WebEngine.executeScript(WebEngine.java:985) + at com.angelbroking.smartapi.gui.SmartApiGui.sendBacktestDataToChart(SmartApiGui.java:2092) + at com.angelbroking.smartapi.gui.SmartApiGui.lambda$createBacktestingTab$9(SmartApiGui.java:1880) + at javafx.graphics@21/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:456) + at java.base/java.security.AccessController.doPrivileged(AccessController.java:138) + at javafx.graphics@21/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:455) + at javafx.graphics@21/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication._runLoop(Native Method) + at javafx.graphics@21/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:185) + at java.base/java.lang.Thread.run(Thread.java:1447) +2025-06-17 20:43:39.795 [Thread-5] WARN c.a.s.algos.AngelOneChargeCalculator - Unknown product type for charge calculation: SQUAREOFF +2025-06-17 20:45:57.324 [Thread-20] WARN c.a.s.algos.AngelOneChargeCalculator - Unknown product type for charge calculation: SQUAREOFF +2025-06-17 20:46:59.007 [Thread-31] WARN c.a.s.algos.AngelOneChargeCalculator - Unknown product type for charge calculation: SQUAREOFF diff --git a/pom.xml b/pom.xml index ebc8e8c..92d0716 100644 --- a/pom.xml +++ b/pom.xml @@ -20,35 +20,49 @@ - 1.8 - 3.8.1 + 21 + 3.13.0 3.5.1 UTF-8 5.8.1 5.5.0 - 3.24.2 + 3.24.2 1.16.0 2.4.0 2.14 20230618 4.11.0 - 3.6.0 + 3.6.0 4.11.0 1.9.10 2.10.1 - 2.0.9 - 1.4.11 8.16.0 - 1.18.30 - 4.12 + 1.18.32 + + 4.12 5.5.0 4.11.0 1.5.0 3.0.1 - 1.7.32 - 1.2.6 + 1.7.36 + 1.2.11 + 21 + + + + + org.javassist + javassist + 3.29.2-GA + + + + @@ -60,7 +74,19 @@ com.spotify docker-client ${docker-client.version} + + + org.javassist + javassist + + + + + org.javassist + javassist + commons-codec commons-codec @@ -141,10 +167,9 @@ junit junit - ${junit.version} + ${junit4.version} test - org.mockito mockito-junit-jupiter @@ -152,30 +177,46 @@ test - com.squareup.okhttp3 - mockwebserver - ${okhttp.mockwebserver.version} - test + org.openjfx + javafx-controls + ${javafx.version} - com.warrenstrange - googleauth - ${google-auth.version} + org.openjfx + javafx-fxml + ${javafx.version} - com.github.tomakehurst - wiremock - ${wiremock.version} + org.openjfx + javafx-swing + ${javafx.version} - org.slf4j - slf4j-api - ${slf4j-api.version} + org.openjfx + javafx-web + ${javafx.version} + + + + - ch.qos.logback - logback-classic - ${logback-classic.version} + org.jfree + jfreechart + 1.5.3 + + + org.wiremock + wiremock + ${wiremock.version} + + + + + + com.tictactec + ta-lib + 0.4.0 @@ -184,10 +225,10 @@ org.apache.maven.plugins maven-compiler-plugin - ${maven-compiler-plugin-version} + ${maven-compiler-plugin-version} - ${java-version} - ${java-version} + 21 + 21 @@ -225,8 +266,49 @@ - - + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + com.angelbroking.smartapi.gui.SmartApiGui + + + + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + com.angelbroking.smartapi.gui.SmartApiGui + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + com.angelbroking.smartapi.gui.SmartApiGui + + --enable-native-access=ALL-UNNAMED + + + diff --git a/src/main/java/com/angelbroking/smartapi/Routes.java b/src/main/java/com/angelbroking/smartapi/Routes.java index 52ee185..1e7b55e 100644 --- a/src/main/java/com/angelbroking/smartapi/Routes.java +++ b/src/main/java/com/angelbroking/smartapi/Routes.java @@ -61,6 +61,7 @@ public Routes() { put("api.nseIntraday", "/rest/secure/angelbroking/marketData/v1/nseIntraday"); put("api.bseIntraday", "/rest/secure/angelbroking/marketData/v1/bseIntraday"); put("api.oIBuildup", "/rest/secure/angelbroking/marketData/v1/OIBuildup"); + // Add WebSocket connect key, but its value will be fetched differently } }; } @@ -69,6 +70,15 @@ public String get(String key) { return _rootUrl + routes.get(key); } + // New method to get WebSocket URLs directly + public String getWsUrl(String key) { + if ("ws.connect".equals(key)) { + return _smartStreamWSURI; // Or _swsuri, or _wsuri depending on which one is current + } + // Add other WebSocket URL keys here if needed + return null; + } + public String getLoginUrl() { return _loginUrl; } diff --git a/src/main/java/com/angelbroking/smartapi/SmartConnect.java b/src/main/java/com/angelbroking/smartapi/SmartConnect.java index 6bc9f4f..cce613e 100644 --- a/src/main/java/com/angelbroking/smartapi/SmartConnect.java +++ b/src/main/java/com/angelbroking/smartapi/SmartConnect.java @@ -4,17 +4,24 @@ import com.angelbroking.smartapi.http.SmartAPIRequestHandler; import com.angelbroking.smartapi.http.exceptions.SmartAPIException; import com.angelbroking.smartapi.models.*; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; +import java.net.URI; import java.net.Proxy; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.nio.ByteBuffer; import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; import javax.net.ssl.HttpsURLConnection; import static com.angelbroking.smartapi.utils.Constants.IO_EXCEPTION_ERROR_MSG; @@ -24,8 +31,10 @@ import static com.angelbroking.smartapi.utils.Constants.SMART_API_EXCEPTION_ERROR_MSG; import static com.angelbroking.smartapi.utils.Constants.SMART_API_EXCEPTION_OCCURRED; -@Slf4j public class SmartConnect { + + private static final Logger log = LoggerFactory.getLogger(SmartConnect.class); + public static SessionExpiryHook sessionExpiryHook = null; public static boolean ENABLE_LOGGING = false; private Proxy proxy = null; @@ -33,9 +42,22 @@ public class SmartConnect { private String accessToken; private String refreshToken; private Routes routes = new Routes(); + private String feedToken; // Added for WebSocket private String userId; private SmartAPIRequestHandler smartAPIRequestHandler = new SmartAPIRequestHandler(proxy); + // WebSocket related fields + private HttpClient wsHttpClient; + private WebSocket webSocketClient; + private OnStreamListener streamListener; + + public interface OnStreamListener { + void onConnected(); + void onDisconnected(int statusCode, String reason); + void onError(Throwable error); + void onData(ByteBuffer data); // Listener to handle raw binary data + } + public SmartConnect() { //setting up TLS min and max version System.setProperty("https.protocols","TLSv1.2,TLSv1.3"); @@ -179,16 +201,29 @@ public User generateSession(String clientCode, String password, String totp) { JSONObject loginResultObject = smartAPIRequestHandler.postRequest(this.apiKey, routes.getLoginUrl(), params); log.info("login result: {}",loginResultObject); - String jwtToken = loginResultObject.getJSONObject("data").getString("jwtToken"); - String refreshToken = loginResultObject.getJSONObject("data").getString("refreshToken"); - String feedToken = loginResultObject.getJSONObject("data").getString("feedToken"); - String url = routes.get("api.user.profile"); - User user = new User().parseResponse(smartAPIRequestHandler.getRequest(this.apiKey, url, jwtToken)); - user.setAccessToken(jwtToken); - user.setRefreshToken(refreshToken); - user.setFeedToken(feedToken); - return user; + if (loginResultObject != null && loginResultObject.optBoolean("status", false) && loginResultObject.has("data") && !loginResultObject.isNull("data")) { + JSONObject dataObject = loginResultObject.getJSONObject("data"); + String jwtToken = dataObject.getString("jwtToken"); + String refreshToken = dataObject.getString("refreshToken"); + String feedToken = dataObject.getString("feedToken"); + this.feedToken = feedToken; // Store feedToken for WebSocket + + String url = routes.get("api.user.profile"); + User user = new User().parseResponse(smartAPIRequestHandler.getRequest(this.apiKey, url, jwtToken)); + user.setAccessToken(jwtToken); + // Store tokens and user ID in the SmartConnect instance + this.setAccessToken(jwtToken); + this.setRefreshToken(refreshToken); + this.setUserId(user.getUserId()); + + user.setRefreshToken(refreshToken); + user.setFeedToken(feedToken); + return user; + } else { + log.error("Login failed or data object is missing/null in response: {}", loginResultObject != null ? loginResultObject.toString() : "Null response"); + return null; // Indicate login failure + } } catch (Exception | SmartAPIException e) { log.error(e.getMessage()); return null; @@ -215,13 +250,17 @@ public TokenSet renewAccessToken(String accessToken, String refreshToken) { String url = routes.get("api.refresh"); JSONObject response = smartAPIRequestHandler.postRequest(this.apiKey, url, params, accessToken); - accessToken = response.getJSONObject("data").getString("jwtToken"); - refreshToken = response.getJSONObject("data").getString("refreshToken"); + String newAccessToken = response.getJSONObject("data").getString("jwtToken"); + String newRefreshToken = response.getJSONObject("data").getString("refreshToken"); + + // Update instance tokens + this.setAccessToken(newAccessToken); + this.setRefreshToken(newRefreshToken); TokenSet tokenSet = new TokenSet(); tokenSet.setUserId(userId); - tokenSet.setAccessToken(accessToken); - tokenSet.setRefreshToken(refreshToken); + tokenSet.setAccessToken(newAccessToken); + tokenSet.setRefreshToken(newRefreshToken); return tokenSet; } catch (Exception | SmartAPIException e) { @@ -723,15 +762,25 @@ public JSONArray gttRuleList(List status, Integer page, Integer count) { * @param params is historic data params. * @return returns the details of historic data. */ - public JSONArray candleData(JSONObject params) { + public JSONArray candleData(JSONObject params) throws SmartAPIException, IOException, JSONException { try { String url = routes.get("api.candle.data"); JSONObject response = smartAPIRequestHandler.postRequest(this.apiKey, url, params, accessToken); - log.info("response : {}",response); - return response.getJSONArray("data"); - } catch (Exception | SmartAPIException e) { - log.error(e.getMessage()); - return null; + //log.info("Candle data API response : {}",response);// + if (response != null && response.optBoolean("status", false) && response.has("data") && !response.isNull("data")) { + return response.getJSONArray("data"); + } else { + String message = response != null ? response.optString("message", "Unknown error from API") : "Null response from API"; + String errorCode = response != null ? response.optString("errorcode", "N/A") : "N/A"; + log.error("Error fetching candle data from API: {}", message); + throw new SmartAPIException(message, errorCode); // Throw exception with API error details + } + } catch (SmartAPIException e) { + log.error("SmartAPIException in candleData: {}", e.getMessage()); + throw e; + } catch (IOException | JSONException e) { // Catch and re-throw IO and JSON exceptions + log.error("Exception in candleData: {}", e.getMessage()); + throw e; } } @@ -808,6 +857,12 @@ public JSONObject logout() { JSONObject params = new JSONObject(); params.put("clientcode", this.userId); JSONObject response = smartAPIRequestHandler.postRequest(this.apiKey, url, params, accessToken); + // Clear local session details upon successful logout from API + if (response != null && response.optBoolean("status", false)) { + this.accessToken = null; + this.refreshToken = null; + // this.userId = null; // Optional: clear userId or keep for re-login + } return response; } catch (Exception | SmartAPIException e) { log.error(e.getMessage()); @@ -1071,6 +1126,190 @@ public JSONObject oIBuildup(JSONObject params) throws SmartAPIException, IOExcep } } + /** + * Connects to the WebSocket stream for live market data. + * + * @param listener The listener to handle WebSocket events and data. + */ + public void connectStream(OnStreamListener listener) { + if (this.feedToken == null || this.apiKey == null || this.userId == null) { + String errorMessage = "Feed token, API key, or User ID is not available. Please login first."; + log.error(errorMessage); + if (listener != null) { + listener.onError(new SmartAPIException(errorMessage)); + } + return; + } -} + this.streamListener = listener; + try { + String wsUrl = routes.getWsUrl("ws.connect"); // Use the new method for WebSocket URLs + if (wsUrl == null) { + // Fallback or error if not in Routes, for example: + wsUrl = "wss://smartapisocket.angelone.in/smart-stream"; // Fallback to a likely correct one + log.warn("WebSocket URL not found in Routes, using default: {}", wsUrl); + } + + wsHttpClient = HttpClient.newBuilder() + .proxy(proxy != null ? HttpClient.Builder.NO_PROXY : HttpClient.Builder.NO_PROXY) // Configure proxy if needed + .build(); + + WebSocketClientListener wsListener = new WebSocketClientListener(); + + log.info("Connecting to WebSocket: {}", wsUrl); + wsHttpClient.newWebSocketBuilder() + // Add authentication headers for the initial handshake + .header("Authorization", "Bearer " + this.accessToken) // Assuming feedToken is the primary auth for WS, but accessToken might be needed for handshake + .header("x-api-key", this.apiKey) + .header("x-client-code", this.userId) // Or "client-code" - check API docs + .header("x-feed-token", this.feedToken) // Send feedToken as a header as well + .buildAsync(URI.create(wsUrl), wsListener) + .thenAccept(ws -> { + webSocketClient = ws; + log.info("WebSocket connection initiated."); + // Authentication will be handled in onOpen + }).exceptionally(e -> { + log.error("WebSocket connection failed: {}", e.getMessage(), e); + if (streamListener != null) { + streamListener.onError(e); + } + return null; + }); + + } catch (Exception e) { + log.error("Exception while connecting to WebSocket: {}", e.getMessage(), e); + if (streamListener != null) { + streamListener.onError(e); + } + } + } + + private class WebSocketClientListener implements WebSocket.Listener { + private StringBuilder textMessageBuilder = new StringBuilder(); + + @Override + public void onOpen(WebSocket webSocket) { + log.info("WebSocket connected."); + webSocket.request(1); // Request the next message + if (streamListener != null) { + streamListener.onConnected(); + } + // Send authentication message + authenticateStream(); + } + + @Override + public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { + textMessageBuilder.append(data); + webSocket.request(1); + if (last) { + String message = textMessageBuilder.toString(); + log.info("WebSocket Text Message Received: {}", message); + // Handle text messages (e.g., responses to auth/subscribe, errors) + // For AngelBroking, most data ticks are binary. + textMessageBuilder.setLength(0); // Reset for next message + } + return null; + } + + @Override + public CompletionStage onBinary(WebSocket webSocket, ByteBuffer data, boolean last) { + // log.debug("WebSocket Binary Message Received, size: {}", data.remaining()); + webSocket.request(1); + if (streamListener != null) { + // Pass the raw ByteBuffer. The listener is responsible for parsing. + // Note: data ByteBuffer might be reused by the WebSocket client, so copy if needed for async processing. + ByteBuffer dataCopy = ByteBuffer.allocate(data.remaining()); + dataCopy.put(data); + dataCopy.flip(); + streamListener.onData(dataCopy); + } + return null; + } + + @Override + public CompletionStage onClose(WebSocket webSocket, int statusCode, String reason) { + log.info("WebSocket closed. Status: {}, Reason: {}", statusCode, reason); + if (streamListener != null) { + streamListener.onDisconnected(statusCode, reason); + } + webSocketClient = null; // Clear the client + return null; + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + log.error("WebSocket error: {}", error.getMessage(), error); + if (streamListener != null) { + streamListener.onError(error); + } + // Attempt to close gracefully if an error occurs and connection is still perceived as open + if (webSocketClient != null && !webSocketClient.isOutputClosed()) { + webSocketClient.sendClose(WebSocket.NORMAL_CLOSURE, "Error occurred"); + } + webSocketClient = null; + } + } + private void authenticateStream() { + if (webSocketClient != null && !webSocketClient.isInputClosed()) { + // It's possible the server authenticates solely via handshake headers. + // If an explicit "authorize" message is still required after connection, + // keep this. Otherwise, this might be redundant if headers work. + // For now, let's assume headers are primary and this is secondary or not needed if 401 is handshake issue. + // JSONObject authPayload = new JSONObject(); + // authPayload.put("action", "authorize"); // Or "auth" or "authenticate" - check docs + // JSONObject params = new JSONObject(); + // params.put("token", this.feedToken); // Parameter name might be just "token" or "feedToken" + // // API key and client code might not be needed in this message if sent in headers + // authPayload.put("params", params); + // String authMsg = authPayload.toString(); + // log.info("Sending WebSocket Post-Connection Auth Message (if required): {}", authMsg); + // webSocketClient.sendText(authMsg, true); + log.info("WebSocket handshake successful, authentication likely handled by headers. If further auth message needed, it would be sent here."); + } + } + + public void subscribeStream(String mode, List instrumentTokens) { + if (webSocketClient != null && !webSocketClient.isInputClosed()) { + JSONObject subPayload = new JSONObject(); + subPayload.put("action", "subscribe"); + JSONObject params = new JSONObject(); + params.put("mode", mode); // e.g., "MODE_LTP", "MODE_QUOTE", "MODE_FULL" + params.put("tokens", new JSONArray(instrumentTokens)); + subPayload.put("params", params); + String subMsg = subPayload.toString(); + log.info("Sending WebSocket Subscription: {}", subMsg); + webSocketClient.sendText(subMsg, true); + } + } + + public void unsubscribeStream(String mode, List instrumentTokens) { + if (webSocketClient != null && !webSocketClient.isInputClosed()) { + JSONObject unsubPayload = new JSONObject(); + unsubPayload.put("action", "unsubscribe"); // Changed action to "unsubscribe" + JSONObject params = new JSONObject(); + // Mode might not be strictly necessary for unsubscribe for some brokers, + // but including it for consistency or if the API requires it. + // If not needed, it can be removed from the params. + params.put("mode", mode); + params.put("tokens", new JSONArray(instrumentTokens)); + unsubPayload.put("params", params); + String unsubMsg = unsubPayload.toString(); + log.info("Sending WebSocket Unsubscription: {}", unsubMsg); + webSocketClient.sendText(unsubMsg, true); + } + } + public void disconnectStream() { + if (webSocketClient != null) { + log.info("Disconnecting WebSocket stream."); + webSocketClient.sendClose(WebSocket.NORMAL_CLOSURE, "User initiated disconnect"); + webSocketClient = null; + } + if (wsHttpClient != null) { + // HttpClient doesn't have a direct close method for general cleanup. + // It's designed to be reused. If executor was custom, shut it down. + } + } + +} diff --git a/src/main/java/com/angelbroking/smartapi/algos/ActionType.java b/src/main/java/com/angelbroking/smartapi/algos/ActionType.java new file mode 100644 index 0000000..978d6b1 --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/ActionType.java @@ -0,0 +1,7 @@ +package com.angelbroking.smartapi.algos; + +public enum ActionType { + BUY, + SELL, + HOLD +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/AngelOneChargeCalculator.java b/src/main/java/com/angelbroking/smartapi/algos/AngelOneChargeCalculator.java new file mode 100644 index 0000000..80ed2c9 --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/AngelOneChargeCalculator.java @@ -0,0 +1,173 @@ +package com.angelbroking.smartapi.algos; + +import com.angelbroking.smartapi.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AngelOneChargeCalculator { + + private static final Logger log = LoggerFactory.getLogger(AngelOneChargeCalculator.class); + + /** + * Calculates estimated total charges for a single trade (buy or sell leg). + * This is a simplified calculation based on common Angel One brokerage structure. + * + * @param price Execution price of the trade. + * @param quantity Quantity traded. + * @param transactionType "BUY" or "SELL". + * @param productType e.g., "DELIVERY", "INTRADAY", "MARGIN", "BO", "CO", "FNO" (Futures/Options). + * @param exchange e.g., "NSE", "BSE", "NFO", "MCX". + * @return Total estimated charges for this trade leg. + */ + public double calculateTotalCharges(double price, int quantity, String transactionType, String productType, String exchange) { + if (price <= 0 || quantity <= 0) { + return 0.0; // No trade, no charges + } + + double turnover = price * quantity; + double brokerage = 0.0; + double stt = 0.0; + double transactionCharges = 0.0; + double sebiFees = 0.0; + double stampDuty = 0.0; + double gst = 0.0; + + // 1. Brokerage + switch (productType) { + case Constants.PRODUCT_DELIVERY: + brokerage = 0.0; // Zero brokerage for Equity Delivery + break; + case Constants.PRODUCT_INTRADAY: + case Constants.PRODUCT_MARGIN: // Assuming similar brokerage for Margin + case Constants.PRODUCT_BO: // Bracket Order + case Constants.PRODUCT_CO: // Cover Order + // 0.03% of turnover or Rs 20, whichever is lower + brokerage = Math.min(0.0003 * turnover, 20.0); + break; + case Constants.PRODUCT_CARRYFORWARD: // Assuming F&O or similar flat rate products + case "FNO": // Explicit F&O type if used + // Flat Rs 20 per executed order (leg) + brokerage = 20.0; + break; + default: + log.warn("Unknown product type for charge calculation: {}", productType); + // Default to a conservative flat fee if product type is unknown + brokerage = 20.0; + break; + } + + // 2. STT (Securities Transaction Tax) + // Calculated on turnover, varies by product and transaction type (Buy/Sell) + switch (productType) { + case Constants.PRODUCT_DELIVERY: + stt = 0.001 * turnover; // 0.1% on Buy and Sell + break; + case Constants.PRODUCT_INTRADAY: + case Constants.PRODUCT_MARGIN: + case Constants.PRODUCT_BO: + case Constants.PRODUCT_CO: + if ("SELL".equalsIgnoreCase(transactionType)) { + stt = 0.00025 * turnover; // 0.025% on Sell + } + break; + case Constants.PRODUCT_CARRYFORWARD: + case "FNO": + if ("SELL".equalsIgnoreCase(transactionType)) { + // F&O STT varies by segment (Futures/Options) and value (Premium/Settlement) + // Using simplified rates: 0.0125% on Sell Futures, 0.05% on Sell Options Premium + // This calculator doesn't distinguish Futures/Options or premium/settlement, + // so we'll use a general F&O sell rate or require productType to be more specific (e.g., "FNO_FUT", "FNO_OPT") + // For simplicity, let's assume a blended rate or require specific product types. + // If productType is just "FNO", this is an approximation. + // A more accurate way needs segment info. Let's use a placeholder/average. + // For now, let's assume the productType might be "FNO_OPT" or "FNO_FUT" or just "FNO" + if ("FNO_OPT".equalsIgnoreCase(productType)) { + // STT on Options is on premium value on SELL side + // Assuming 'price' here is the premium per share/lot + stt = 0.0005 * turnover; // 0.05% on Sell Options Premium + } else { // Assuming Futures or general FNO + stt = 0.000125 * turnover; // 0.0125% on Sell Futures + } + } + break; + } + + // 3. Transaction Charges (Exchange Turnover Tax) + // Varies by exchange and segment (Equity/F&O) + double transactionChargeRate = 0.0; + if ("NSE".equalsIgnoreCase(exchange)) { + switch (productType) { + case Constants.PRODUCT_DELIVERY: + case Constants.PRODUCT_INTRADAY: + case Constants.PRODUCT_MARGIN: + case Constants.PRODUCT_BO: + case Constants.PRODUCT_CO: + transactionChargeRate = 0.0000345; // 0.00345% for Equity + break; + case Constants.PRODUCT_CARRYFORWARD: + case "FNO": + case "FNO_FUT": + transactionChargeRate = 0.000018; // 0.0018% for Futures + break; + case "FNO_OPT": + transactionChargeRate = 0.00053; // 0.053% for Options (on premium value) + break; + } + } else if ("BSE".equalsIgnoreCase(exchange)) { + switch (productType) { + case Constants.PRODUCT_DELIVERY: + case Constants.PRODUCT_INTRADAY: + case Constants.PRODUCT_MARGIN: + case Constants.PRODUCT_BO: + case Constants.PRODUCT_CO: + // BSE has different slabs, using an average/common rate + transactionChargeRate = 0.0000345; // Approximation for Equity + break; + // Add BSE F&O rates if needed + } + } + // Add MCX, CDS if needed + + transactionCharges = transactionChargeRate * turnover; + + // 4. GST (Goods and Services Tax) + // 18% on Brokerage + Transaction Charges + gst = 0.18 * (brokerage + transactionCharges); + + // 5. SEBI Turnover Fees + // 0.0001% on turnover + sebiFees = 0.000001 * turnover; + + // 6. Stamp Duty + // Applied only on BUY side, varies by state and segment. Using common rates. + if ("BUY".equalsIgnoreCase(transactionType)) { + switch (productType) { + case Constants.PRODUCT_DELIVERY: + stampDuty = 0.00015 * turnover; // 0.015% + break; + case Constants.PRODUCT_INTRADAY: + case Constants.PRODUCT_MARGIN: + case Constants.PRODUCT_BO: + case Constants.PRODUCT_CO: + stampDuty = 0.00003 * turnover; // 0.003% + break; + case Constants.PRODUCT_CARRYFORWARD: + case "FNO": + case "FNO_FUT": + stampDuty = 0.00002 * turnover; // 0.002% + break; + case "FNO_OPT": + stampDuty = 0.00003 * turnover; // 0.003% + break; + } + } + + double totalCharges = brokerage + stt + transactionCharges + gst + sebiFees + stampDuty; + + // Log the breakdown for debugging + log.debug("Charges for {} {} {} shares @ {:.2f} (Turnover: {:.2f}): Brokerage={:.4f}, STT={:.4f}, TxnCharges={:.4f}, GST={:.4f}, SEBI={:.4f}, StampDuty={:.4f} | Total={:.4f}", + transactionType, productType, quantity, price, turnover, brokerage, stt, transactionCharges, gst, sebiFees, stampDuty, totalCharges); + + return totalCharges; + } +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/Backtester.java b/src/main/java/com/angelbroking/smartapi/algos/Backtester.java new file mode 100644 index 0000000..1216a61 --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/Backtester.java @@ -0,0 +1,121 @@ +package com.angelbroking.smartapi.algos; + +import com.angelbroking.smartapi.models.BacktestReport; +import com.angelbroking.smartapi.models.TradeLog; +import com.angelbroking.smartapi.models.Candle; +import com.angelbroking.smartapi.algos.AngelOneChargeCalculator; // Added import +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class Backtester { + private Strategy strategy; + private List historicalData; + private double initialCapital; + private int sharesPerTrade; + private AngelOneChargeCalculator chargeCalculator; + + public Backtester(Strategy strategy, List historicalData, JSONObject params) { + this.strategy = strategy; + this.historicalData = historicalData; + this.initialCapital = params.optDouble("initialCapital", 100000.0); + this.sharesPerTrade = params.optInt("sharesPerTrade", 10); + this.chargeCalculator = new AngelOneChargeCalculator(); // Initialize charge calculator + } + + public BacktestReport run(JSONObject strategyParams) { + BacktestReport report = new BacktestReport(); + report.setInitialCapital(initialCapital); + + List trades = new ArrayList<>(); + double currentCapital = initialCapital; + int sharesHeld = 0; + List equityTimestamps = new ArrayList<>(); + List equityValues = new ArrayList<>(); + double entryPrice = 0.0; + + // Generate all signals from the strategy based on the full historical data + // Expecting TradingAction objects from strategies now + List allGeneratedSignals = strategy.generateSignals(historicalData, strategyParams); + int signalIndex = 0; // To iterate through the allGeneratedSignals list + + for (Candle candle : historicalData) { // Iterate through each candle in the historical data + TradingAction actionForThisCandle = null; + // Check if the next signal from the strategy matches the current candle's timestamp + if (signalIndex < allGeneratedSignals.size()) { + TradingAction nextPotentialSignal = allGeneratedSignals.get(signalIndex); + // Ensure nextPotentialSignal and its timestamp are not null before comparing + if (nextPotentialSignal != null && nextPotentialSignal.getTimestamp() != null && + nextPotentialSignal.getTimestamp().equals(candle.getTimestamp())) { + actionForThisCandle = nextPotentialSignal; // Assign the found signal + signalIndex++; // Consume this signal + } + } + + // Process BUY signal + if (actionForThisCandle != null && actionForThisCandle.getActionType() == ActionType.BUY && sharesHeld == 0) { + double tradePrice = actionForThisCandle.getPrice(); // Use signal price, not necessarily candle close + int tradeQuantity = actionForThisCandle.getQuantity(); // Use signal quantity + String productType = actionForThisCandle.getProductType(); + String exchange = actionForThisCandle.getExchange(); + + double estimatedCharges = chargeCalculator.calculateTotalCharges(tradePrice, tradeQuantity, "BUY", productType, exchange); + + if (currentCapital >= (tradeQuantity * tradePrice + estimatedCharges)) { + sharesHeld = sharesPerTrade; + entryPrice = tradePrice; // Use trade price for entry + currentCapital -= (sharesHeld * entryPrice); + currentCapital -= estimatedCharges; + trades.add(new TradeLog(actionForThisCandle.getActionType().toString(), candle.getTimestamp(), sharesHeld, entryPrice, estimatedCharges)); + } + } + // Process SELL signal + else if (actionForThisCandle != null && actionForThisCandle.getActionType() == ActionType.SELL && sharesHeld > 0) { + double tradePrice = actionForThisCandle.getPrice(); // Use signal price + String productType = actionForThisCandle.getProductType(); + String exchange = actionForThisCandle.getExchange(); + double estimatedCharges = chargeCalculator.calculateTotalCharges(tradePrice, sharesHeld, "SELL", productType, exchange); // Use sharesHeld for sell quantity + currentCapital += (sharesHeld * tradePrice); // Use trade price for exit + currentCapital -= estimatedCharges; + trades.add(new TradeLog(actionForThisCandle.getActionType().toString(), candle.getTimestamp(), sharesHeld, tradePrice, estimatedCharges)); + sharesHeld = 0; + entryPrice = 0.0; + } + + // Record equity at the end of each candle + double currentPortfolioValue = currentCapital + (sharesHeld * candle.getClose()); + equityTimestamps.add(candle.getTimestamp()); + equityValues.add(currentPortfolioValue); + } + + // If position is still open at the end, square it off + if (sharesHeld > 0 && !historicalData.isEmpty()) { + Candle lastCandle = historicalData.get(historicalData.size() - 1); + currentCapital += (sharesHeld * lastCandle.getClose()); + // Calculate and subtract square-off charges + double squareOffCharges = chargeCalculator.calculateTotalCharges(lastCandle.getClose(), sharesHeld, "SELL", "SQUAREOFF", "UNKNOWN"); // Use a placeholder product/exchange + currentCapital -= squareOffCharges; + trades.add(new TradeLog("SQUAREOFF_END", lastCandle.getTimestamp(), sharesHeld, lastCandle.getClose(), squareOffCharges)); + // Update the last equity point to reflect the square-off + if (!equityValues.isEmpty()) { + equityValues.set(equityValues.size() - 1, currentCapital); + } + } + + report.setTrades(trades); + report.setFinalCapital(currentCapital); + report.setNetProfit(currentCapital - initialCapital); + if (initialCapital != 0) { + report.setProfitPercentage(((currentCapital - initialCapital) / initialCapital) * 100); + } else { + report.setProfitPercentage(0); + } + report.setEquityCurveTimestamps(equityTimestamps); + report.setEquityCurveValues(equityValues); + report.setHistoricalCandles(new ArrayList<>(historicalData)); // Store a copy + + return report; + } +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/Signal.java b/src/main/java/com/angelbroking/smartapi/algos/Signal.java new file mode 100644 index 0000000..410873e --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/Signal.java @@ -0,0 +1,44 @@ +package com.angelbroking.smartapi.algos; + +import java.util.Date; + +public class Signal { + private Date timestamp; + private String type; // e.g., "BUY", "SELL", "HOLD" + private double price; + private int quantity; + + public Signal(Date timestamp, String type, double price, int quantity) { + this.timestamp = timestamp; + this.type = type; + this.price = price; + this.quantity = quantity; + } + + // Getters + public Date getTimestamp() { + return timestamp; + } + + public String getType() { + return type; + } + + public double getPrice() { + return price; + } + + public int getQuantity() { + return quantity; + } + + @Override + public String toString() { + return "Signal{" + + "timestamp=" + timestamp + + ", type='" + type + '\'' + + ", price=" + price + + ", quantity=" + quantity + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/Strategy.java b/src/main/java/com/angelbroking/smartapi/algos/Strategy.java new file mode 100644 index 0000000..247ef9a --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/Strategy.java @@ -0,0 +1,18 @@ +package com.angelbroking.smartapi.algos; + +import com.angelbroking.smartapi.models.Candle; +import org.json.JSONObject; + +import com.angelbroking.smartapi.algos.TradingAction; + +import java.util.List; +import java.util.Map; + +public interface Strategy { + /** + * Generates trading signals based on historical data and parameters. + * Should return a list of TradingAction objects. + */ + List generateSignals(List historicalData, JSONObject params); + Map> getIndicatorData(List historicalData, JSONObject params); +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/TradingAction.java b/src/main/java/com/angelbroking/smartapi/algos/TradingAction.java new file mode 100644 index 0000000..cf4aa9c --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/TradingAction.java @@ -0,0 +1,58 @@ +package com.angelbroking.smartapi.algos; + +import java.util.Date; + +public class TradingAction extends Signal { + private ActionType actionType; + private String symbolToken; + private String tradingSymbol; + private String exchange; + private double triggerPrice; + private String orderType; + private String productType; + private String variety; + private String duration; + + // Constructor for HOLD signals + public TradingAction(Date timestamp, ActionType actionType, String symbolToken) { + super(timestamp, actionType.toString(), 0, 0); // price and quantity are 0 for HOLD + this.actionType = actionType; + this.symbolToken = symbolToken; + // Initialize other fields to defaults or null as appropriate + this.orderType = "MARKET"; + this.productType = "INTRADAY"; + this.variety = "NORMAL"; + this.duration = "DAY"; + } + + // Constructor for BUY/SELL signals with full order details + public TradingAction(Date timestamp, ActionType actionType, String symbolToken, String tradingSymbol, String exchange, int quantity, + double price, double triggerPrice, String orderType, String productType, + String variety, String duration) { + super(timestamp, actionType.toString(), price, quantity); + + this.actionType = actionType; + this.symbolToken = symbolToken; + this.tradingSymbol = tradingSymbol; + this.exchange = exchange; + this.triggerPrice = triggerPrice; + this.orderType = orderType; + this.productType = productType; + this.variety = variety; + this.duration = duration; + } + + public ActionType getActionType() { + return this.actionType; + } + + // Getters for additional fields + public String getSymbolToken() { return symbolToken; } + public String getTradingSymbol() { return tradingSymbol; } + public String getExchange() { return exchange; } + public double getTriggerPrice() { return triggerPrice; } + public String getOrderType() { return orderType; } + public String getProductType() { return productType; } + public String getVariety() { return variety; } + public String getDuration() { return duration; } +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/strategies/ExampleCustomStrategy.java.txt b/src/main/java/com/angelbroking/smartapi/algos/strategies/ExampleCustomStrategy.java.txt new file mode 100644 index 0000000..fcba081 --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/strategies/ExampleCustomStrategy.java.txt @@ -0,0 +1,129 @@ +package com.angelbroking.smartapi.algos.strategies; + +import com.angelbroking.smartapi.algos.Strategy; +import com.angelbroking.smartapi.models.Candle; +import com.angelbroking.smartapi.utils.Constants; // Assuming Constants.TRADE_ACTION_BUY etc. exist +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * ExampleCustomStrategy provides a template for creating your own trading strategies. + * To use this: + * 1. Implement your strategy logic within the {@code generateSignal} method. + * 2. You can use the {@code init} method to process any parameters passed from the GUI. + * 3. Ensure this class has a public default constructor. + * 4. When running a backtest in the SmartAPI GUI, select "Custom Strategy" and + * enter "ExampleCustomStrategy" (or the name of your class if you rename it) + * in the "Custom Strategy Class" field. If your class is in a different package + * than {@code com.angelbroking.smartapi.algos.strategies}, provide the fully qualified name. + */ +public class ExampleCustomStrategy implements Strategy { + + private static final Logger log = LoggerFactory.getLogger(ExampleCustomStrategy.class); + + private boolean initialized = false; + private int quantityPerTrade = 1; // Default, can be overridden by params + + /** + * Default constructor is required for dynamic instantiation by the GUI. + */ + public ExampleCustomStrategy() { + // Initialization that doesn't depend on external params can go here. + log.info("ExampleCustomStrategy instantiated."); + } + + /** + * Initializes the strategy with parameters provided from the backtesting UI. + * + * @param params A JSONObject containing parameters such as "quantity", "symbolToken", etc. + * These are typically passed from the GUI's backtesting setup. + */ + // @Override // Removed because the compiler indicates that the 'Strategy' interface + // // does not define an 'init(JSONObject params)' method. + // // If the Strategy interface is intended to have this method, + // // the interface itself should be updated. + public void init(JSONObject params) { + log.info("Initializing ExampleCustomStrategy with params: {}", params.toString()); + // Example: Extracting quantity per trade from parameters + if (params.has("quantity")) { + this.quantityPerTrade = params.getInt("quantity"); + } + // Add any other parameter extraction and initialization logic here. + this.initialized = true; + log.info("ExampleCustomStrategy initialized. Quantity per trade: {}", this.quantityPerTrade); + } + + /** + * Generates a trading signal based on the current candle and historical data. + * This is where you implement your core trading logic. + * + * @param currentCandle The most recent candle data. + * @param historicalCandles A list of historical candles. The exact content (e.g., whether + * it includes currentCandle) depends on the Backtester implementation. + * @param strategyParams Additional parameters or context for signal generation. This might + * be the same as or derived from the params passed to {@code init}. + * @return A string representing the signal: + * - {@link Constants#TRADE_ACTION_BUY} ("BUY") to enter a long position. + * - {@link Constants#TRADE_ACTION_SELL} ("SELL") to enter a short position or exit a long. + * - "HOLD" or null/empty string for no action. + * (Note: The exact signal strings "BUY", "SELL", "HOLD" might be defined in your Constants class + * or expected by your Backtester.) + */ + @Override + public String generateSignal(Candle currentCandle, List historicalCandles, JSONObject strategyParams) { + if (!initialized) { + log.warn("Strategy not initialized. Call init() first."); + return null; // Or "HOLD" + } + + // Implement your custom trading logic here. + // This is a placeholder example: Buy on the very first candle available, then hold. + if (historicalCandles == null || historicalCandles.isEmpty() || historicalCandles.size() < 2) { // Need at least one previous candle for some logic + log.info("First signal opportunity for {}: BUYING at {}", strategyParams.optString("tradingSymbol", "N/A"), currentCandle.getClose()); + return Constants.TRADE_ACTION_BUY; // Or "BUY" + } + + // Example: log current price + // log.debug("Processing candle for {}: Time: {}, Close: {}", + // strategyParams.optString("tradingSymbol", "N/A"), + // currentCandle.getTimestamp(), + // currentCandle.getClose()); + + return null; // Or "HOLD" - no action for subsequent candles in this simple example + } + + /** + * Generates trading signals based on historical candle data and strategy parameters. + * + * This method is intended for batch processing of historical data, potentially + * as an alternative or complement to the `generateSignal` method. The exact + * usage depends on the backtesting framework's design. + * + * @param historicalCandles A list of historical candles. + * @param strategyParams Additional parameters for signal generation. + * @return A list of signals, where each signal could be a trade action or related + * data. The specific format of signals (e.g., Strings, custom objects) + * depends on the backtesting framework. Return an empty list if no signals + * are generated. + */ + @Override + public List generateSignals(List historicalCandles, JSONObject strategyParams) { + // Placeholder: This method needs a proper implementation. Consider + // iterating through the `historicalCandles` and applying your strategy + // logic to determine buy/sell signals. + log.warn("generateSignals() method is not yet fully implemented."); + return List.of(); // Return an empty list for now + } + + @Override + public JSONObject getIndicatorData(List historicalCandles, JSONObject strategyParams) { + // This method needs to return indicator values calculated from the + // historical data, which can be used for charting or further analysis + log.warn("getIndicatorData() method is not yet implemented, returning empty JSON"); + //Example return, modify as per your needs. + return new JSONObject(); + } +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/strategies/MACrossoverStrategy.java b/src/main/java/com/angelbroking/smartapi/algos/strategies/MACrossoverStrategy.java new file mode 100644 index 0000000..8bd5afb --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/strategies/MACrossoverStrategy.java @@ -0,0 +1,216 @@ +package com.angelbroking.smartapi.algos.strategies; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.stream.Collectors; +import java.util.Arrays; + +import org.json.JSONObject; + +import com.angelbroking.smartapi.SmartConnect; +import com.angelbroking.smartapi.algos.ActionType; +import com.angelbroking.smartapi.algos.Strategy; // For order constants +import com.angelbroking.smartapi.algos.TradingAction; +import com.angelbroking.smartapi.models.Candle; +import com.angelbroking.smartapi.models.Tick; +import com.angelbroking.smartapi.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MACrossoverStrategy implements Strategy { + private SmartConnect smartConnect; + private String symbolToken; + private String tradingSymbol; + private String exchange; + private int shortPeriod; + private int longPeriod; + private int quantity; + private String productType; // Added product type field + + private static final Logger log = LoggerFactory.getLogger(MACrossoverStrategy.class); + + private List closePrices = new ArrayList<>(); + private boolean positionOpen = false; // True if we have an open long position + + public String getName() { + // Include parameters in name for clarity in logs/GUI + return "MA Crossover (" + shortPeriod + "/" + longPeriod + ") [" + productType + "]"; + } + + public void init(SmartConnect smartConnect, JSONObject params) { + this.smartConnect = smartConnect; // Store for potential live trading (can be null for backtesting) + this.symbolToken = params.getString("symbolToken"); + this.tradingSymbol = params.optString("tradingSymbol", this.symbolToken); // Fallback to token if no symbol + this.exchange = params.getString("exchange"); + this.shortPeriod = params.getInt("shortPeriod"); + this.longPeriod = params.getInt("longPeriod"); + this.quantity = params.optInt("quantity", 1); // Default quantity to 1 if not specified + + this.productType = params.optString("productType", "INTRADAY"); // Get product type from params + + // Reset state for new initialization (e.g., during backtesting a new period) + log.info("[{}] Initializing strategy with params: {}", getName(), params.toString()); + this.closePrices = new ArrayList<>(); // Re-initialize list + this.positionOpen = false; + } + + public void onCandle(Candle candle) { + if (candle != null) { + closePrices.add(candle.getClose()); + // log.trace("[{}] Added close price: {} from candle: {}", getName(), candle.getClose(), candle.getTimestamp()); + // Ensure we don't keep an infinitely growing list if not needed for very long MAs + // For this strategy, we need at least `longPeriod` prices. + // A more robust implementation might use a circular buffer or fixed-size list. + if (closePrices.size() > longPeriod + 50) { // Keep some buffer + closePrices = closePrices.stream().skip(closePrices.size() - (longPeriod + 50)).collect(Collectors.toList()); + } + } + } + + public void onTick(Tick tick) { + // This strategy is candle-based, so onTick might not be used directly for signals. + // However, you could use it to update LTP for trailing stops or real-time P&L. + // For now, we'll leave it empty. + } + + public TradingAction getActionSignal(Candle currentCandle) { + // `currentCandle` is the latest, potentially incomplete candle in live mode, + // or the current candle being processed in backtesting. + // The `onCandle` method should have already added its close price to `closePrices` + // if it's a completed candle. For `getActionSignal`, we operate on the historical `closePrices`. + + if (closePrices.size() < longPeriod) { + log.debug("[{}] Not enough data for MA calculation. Close prices count: {}, Required: {}", getName(), closePrices.size(), longPeriod); + // Pass currentCandle's timestamp, or null if not strictly needed for HOLD signal processing later + return new TradingAction(currentCandle != null ? currentCandle.getTimestamp() : null, ActionType.HOLD, symbolToken); + } + + double shortMA = calculateMA(shortPeriod); + double longMA = calculateMA(longPeriod); + log.debug("[{}] Calculated MAs - ShortMA({}): {}, LongMA({}): {}", getName(), shortPeriod, shortMA, longPeriod, longMA); + + // Need previous MAs to detect crossover + if (closePrices.size() < longPeriod + 1) { + log.debug("[{}] Not enough data for previous MA calculation. Close prices count: {}, Required: {}", getName(), closePrices.size(), longPeriod + 1); + return new TradingAction(currentCandle != null ? currentCandle.getTimestamp() : null, ActionType.HOLD, symbolToken); // Not enough data for previous MAs + } + double prevShortMA = calculatePreviousMA(shortPeriod, 1); + double prevLongMA = calculatePreviousMA(longPeriod, 1); + log.debug("[{}] Calculated Previous MAs - PrevShortMA({}): {}, PrevLongMA({}): {}", getName(), shortPeriod, prevShortMA, longPeriod, prevLongMA); + + // Buy signal: short MA crosses above long MA + if (prevShortMA <= prevLongMA && shortMA > longMA && !positionOpen) { + positionOpen = true; + log.info("[{}] BUY signal triggered at price of current candle {}: ShortMA ({}) crossed above LongMA ({}). PrevShortMA: {:.2f}, PrevLongMA: {:.2f}, CurrentShortMA: {:.2f}, CurrentLongMA: {:.2f}", + getName(), currentCandle != null ? currentCandle.getClose() : "N/A", shortPeriod, longPeriod, prevShortMA, prevLongMA, shortMA, longMA); + // Use currentCandle.getClose() as the price for the signal of a MARKET order + // Use the productType stored in the strategy instance + return new TradingAction(currentCandle.getTimestamp(), ActionType.BUY, symbolToken, tradingSymbol, exchange, quantity, currentCandle.getClose(), 0, Constants.ORDER_TYPE_MARKET, productType, + Constants.VARIETY_NORMAL, Constants.DURATION_DAY); + } + // Sell signal (to close long position): short MA crosses below long MA + else if (prevShortMA >= prevLongMA && shortMA < longMA && positionOpen) { + positionOpen = false; + log.info("[{}] SELL signal triggered at price of current candle {}: ShortMA ({}) crossed below LongMA ({}). PrevShortMA: {:.2f}, PrevLongMA: {:.2f}, CurrentShortMA: {:.2f}, CurrentLongMA: {:.2f}", + getName(), currentCandle != null ? currentCandle.getClose() : "N/A", shortPeriod, longPeriod, prevShortMA, prevLongMA, shortMA, longMA); + // Use currentCandle.getClose() as the price for the signal of a MARKET order + // Use the productType stored in the strategy instance + return new TradingAction(currentCandle.getTimestamp(), ActionType.SELL, symbolToken, tradingSymbol, exchange, quantity, currentCandle.getClose(), 0, Constants.ORDER_TYPE_MARKET, productType, + Constants.VARIETY_NORMAL, Constants.DURATION_DAY); + } + + return new TradingAction(currentCandle != null ? currentCandle.getTimestamp() : null, ActionType.HOLD, symbolToken); + } + + private double calculateMA(int period) { + if (closePrices.size() < period) return 0.0; + return closePrices.stream().skip(closePrices.size() - period).mapToDouble(Double::doubleValue).average().orElse(0.0); + } + + private double calculatePreviousMA(int period, int lookback) { + if (closePrices.size() < period + lookback) return 0.0; + return closePrices.stream().skip(closePrices.size() - period - lookback).limit(period).mapToDouble(Double::doubleValue).average().orElse(0.0); + } + + @Override + public List generateSignals(List historicalData, JSONObject params) { + // Initialize the strategy state for this specific run + // Assumes this.smartConnect is either already set or can be null if not used by init for backtesting logic + this.init(null, params); // Pass null for smartConnect during backtesting + + List signals = new ArrayList<>(); // Changed from List to List + for (Candle candle : historicalData) { + onCandle(candle); // Update internal state (closePrices) with the current candle + log.trace("[{}] Processing candle for generateSignals: {}", getName(), candle.getTimestamp()); + TradingAction action = getActionSignal(candle); // Get signal based on the updated state + + // Add action to signals list if it's not a HOLD + // Also, associate the candle's timestamp with the action if needed (not done here) + if (action.getActionType() != ActionType.HOLD) { + signals.add((TradingAction) action); // Cast to TradingAction + log.debug("[{}] Added signal: {} for candle at {}", getName(), action.getActionType(), candle.getTimestamp()); + } + } + return signals; + } + + @Override + public Map> getIndicatorData(List historicalData, JSONObject params) { + // This method calculates indicator values over a given historical dataset. + // It should not modify the primary state of the strategy instance (this.closePrices, this.positionOpen). + // It uses parameters passed to it, or falls back to strategy's initialized parameters. + + List localClosePrices = historicalData.stream() + .map(Candle::getClose) + .collect(Collectors.toList()); + + List shortMAList = new ArrayList<>(); + List longMAList = new ArrayList<>(); + + // Use periods from params if provided, otherwise use the strategy's configured periods + int currentShortPeriod = params.has("shortPeriod") ? params.getInt("shortPeriod") : this.shortPeriod; + int currentLongPeriod = params.has("longPeriod") ? params.getInt("longPeriod") : this.longPeriod; + + // Ensure periods are valid if they were not set by init (e.g. this.shortPeriod is 0) + if (currentShortPeriod <= 0 || currentLongPeriod <= 0) { + // Or throw an IllegalArgumentException + // For now, returning empty data or NaNs might be acceptable depending on requirements + Map> errorIndicators = new HashMap<>(); + // Optionally add an error message or return empty map + // errorIndicators.put("error", Arrays.asList(-1.0)); // Example error indication + log.warn("[{}] Invalid periods for MA calculation in getIndicatorData. Short: {}, Long: {}", getName(), currentShortPeriod, currentLongPeriod); + return errorIndicators; // Return empty map + } + + + for (int i = 0; i < localClosePrices.size(); i++) { + // Calculate Short MA + if (i >= currentShortPeriod - 1) { + double ma = localClosePrices.subList(i - currentShortPeriod + 1, i + 1) + .stream().mapToDouble(Double::doubleValue).average().orElse(Double.NaN); + shortMAList.add(ma); + } else { + shortMAList.add(Double.NaN); // Not enough data for MA + } + + // Calculate Long MA + if (i >= currentLongPeriod - 1) { + double ma = localClosePrices.subList(i - currentLongPeriod + 1, i + 1) + .stream().mapToDouble(Double::doubleValue).average().orElse(Double.NaN); + longMAList.add(ma); + } else { + longMAList.add(Double.NaN); // Not enough data for MA + } + } + + Map> indicators = new HashMap<>(); + indicators.put("SMA(" + currentShortPeriod + ")", shortMAList); + indicators.put("SMA(" + currentLongPeriod + ")", longMAList); + // Optionally, include timestamps or other candle data for alignment if plotting + // List timestamps = historicalData.stream().map(Candle::getTimestampString).collect(Collectors.toList()); + // indicators.put("timestamps", historicalData.stream().map(c -> (double) c.getTimestamp().getTime()).collect(Collectors.toList())); + return indicators; + } +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/strategies/RsiDeviationStrategy.java b/src/main/java/com/angelbroking/smartapi/algos/strategies/RsiDeviationStrategy.java new file mode 100644 index 0000000..08964bc --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/strategies/RsiDeviationStrategy.java @@ -0,0 +1,390 @@ +package com.angelbroking.smartapi.algos.strategies; + +import com.angelbroking.smartapi.algos.TradingAction; +import com.angelbroking.smartapi.algos.Signal; +import com.angelbroking.smartapi.algos.Strategy; +import com.angelbroking.smartapi.algos.ActionType; +import com.angelbroking.smartapi.models.Candle; +import com.tictactec.ta.lib.Core; +import com.tictactec.ta.lib.MInteger; +import com.tictactec.ta.lib.RetCode; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.SimpleDateFormat; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RsiDeviationStrategy implements Strategy { + private static final Logger log = LoggerFactory.getLogger(RsiDeviationStrategy.class); + + // Store parameters passed during initialization/run + private JSONObject params; + + public String getName() { + // Include parameters in name for clarity in logs/GUI + return "RSI Deviation (" + params.optInt("rsiPeriod", 14) + ")"; + } + + @Override + public List generateSignals(List historicalData, JSONObject params) { + this.params = params; // Store params for getName() and TradingAction creation + List signals = new ArrayList<>(); // Changed from List to List + if (historicalData == null || historicalData.isEmpty()) { + log.warn("[{}] Historical data is empty or null. Cannot generate RSI signals.", getName()); + return signals; + } + + // Enhanced parameters with better defaults + String symbolToken = params.optString("symbolToken", "UNKNOWN"); + String tradingSymbol = params.optString("tradingSymbol", "UNKNOWN"); + String exchange = params.optString("exchange", "UNKNOWN"); + String productType = params.optString("productType", "INTRADAY"); // Get product type from params + + int rsiPeriod = params.optInt("rsiPeriod", 14); + double oversoldThreshold = params.optDouble("oversoldThreshold", 25.0); + double overboughtThreshold = params.optDouble("overboughtThreshold", 75.0); + double rsiConfirmationBuffer = params.optDouble("rsiConfirmationBuffer", 2.0); + + // Enhanced price stability parameters + double priceStabilityThresholdPercent = params.optDouble("priceStabilityThresholdPercent", 0.15); // Increased from 0.05% + double minRsiPositiveDifference = params.optDouble("minRsiPositiveDifference", 2.0); // Increased threshold + + // Enhanced control parameters + int minCandlesBetweenTrades = params.optInt("minCandlesBetweenTrades", 10); // Increased gap + double minProfitTarget = params.optDouble("minProfitTarget", 0.2); // Percentage + double stopLossPercent = params.optDouble("stopLossPercent", 0.4); + + // Market timing parameters + boolean avoidMarketOpenClose = params.optBoolean("avoidMarketOpenClose", true); + int marketOpenAvoidMinutes = params.optInt("marketOpenAvoidMinutes", 15); // Avoid first 15 min + int marketCloseAvoidMinutes = params.optInt("marketCloseAvoidMinutes", 15); // Avoid last 15 min + + log.info("[{}] Enhanced parameters - Price Stability: {}%, Min RSI Diff: {}, Gap: {}, Avoid Open/Close: {}", + getName(), priceStabilityThresholdPercent, minRsiPositiveDifference, minCandlesBetweenTrades, avoidMarketOpenClose); + log.info("[{}] Backtest Params: Token={}, Exchange={}, Product={}, Interval={}", + getName(), symbolToken, exchange, productType, params.optString("interval", "UNKNOWN")); + + double[] closePrices = historicalData.stream().mapToDouble(Candle::getClose).toArray(); + + if (closePrices.length < rsiPeriod + 20) { // Increased minimum data requirement + log.warn("[{}] Not enough data for RSI calculation. Data points: {}, Need at least: {}", + getName(), closePrices.length, rsiPeriod + 20); + return signals; + } + + Core taLib = new Core(); + double[] rsiValues = new double[closePrices.length]; + MInteger begin = new MInteger(); + MInteger length = new MInteger(); + + RetCode retCode = taLib.rsi(0, closePrices.length - 1, closePrices, rsiPeriod, begin, length, rsiValues); + + if (retCode != RetCode.Success || length.value == 0) { + log.error("[{}] TA-Lib RSI calculation failed. RetCode: {}", getName(), retCode); + return signals; + } + + boolean inPosition = false; + int lastTradeIndex = -minCandlesBetweenTrades; + double entryPrice = 0.0; + + // Increased stabilization period + int rsiSignalStabilizationCount = params.optInt("rsiSignalStabilizationCount", 15); // Increased from 5 + + if (length.value < rsiSignalStabilizationCount + 10) { + log.warn("[{}] Not enough RSI values after stabilization. Available: {}, Need: {}", + getName(), length.value, rsiSignalStabilizationCount + 10); + return signals; + } + + // Calculate volume-based filter if available + double[] volumes = historicalData.stream().mapToDouble(candle -> candle.getVolume() == 0L ? 1.0 : candle.getVolume()).toArray(); + double avgVolume = calculateAverageVolume(volumes, Math.min(20, volumes.length)); + + for (int i = rsiSignalStabilizationCount + 5; i < length.value - 5; i++) { // Avoid last 5 candles too + int candleIndex = begin.value + i; + if (candleIndex >= historicalData.size() - 5 || candleIndex - 5 < 0) continue; // Enhanced boundary check + + double currentRsi = rsiValues[i]; + double previousRsi = rsiValues[i - 1]; + double prevPrevRsi = rsiValues[i - 2]; + + Candle currentCandle = historicalData.get(candleIndex); + Candle previousCandle = historicalData.get(candleIndex - 1); + + double currentPrice = currentCandle.getClose(); + double previousPrice = previousCandle.getClose(); + + // Enhanced market timing filter + if (avoidMarketOpenClose && isNearMarketOpenOrClose(currentCandle, marketOpenAvoidMinutes, marketCloseAvoidMinutes)) { + continue; + } + + // Volume filter - avoid low volume periods + double currentVolume = currentCandle.getVolume() == 0L ? 1.0 : currentCandle.getVolume(); + if (currentVolume < avgVolume * 0.5) { // Skip if volume is less than 50% of average + continue; + } + + // Anti-overtrading: ensure minimum gap between trades + boolean canTrade = (candleIndex - lastTradeIndex) >= minCandlesBetweenTrades; + + // Enhanced RSI stability check - ensure RSI has been stable for multiple periods + boolean rsiIsStable = isRsiStable(rsiValues, i, 3, 2.0); // Check last 3 periods for stability + + // ============ ENHANCED PRICE STABILITY + RSI DIVERGENCE LOGIC ============ + + // Multi-period price stability check + double priceDifferencePercent = Math.abs(currentPrice - previousPrice) / previousPrice * 100.0; + boolean pricesAreSimilar = priceDifferencePercent <= priceStabilityThresholdPercent; + + // Enhanced price stability - check multiple periods + boolean enhancedPriceStability = false; + if (candleIndex >= 3) { + double price3PeriodsAgo = historicalData.get(candleIndex - 3).getClose(); + double maxPriceInRange = Math.max(Math.max(currentPrice, previousPrice), price3PeriodsAgo); + double minPriceInRange = Math.min(Math.min(currentPrice, previousPrice), price3PeriodsAgo); + double priceRangePercent = (maxPriceInRange - minPriceInRange) / minPriceInRange * 100.0; + enhancedPriceStability = priceRangePercent <= priceStabilityThresholdPercent * 2; // Allow slightly more range + } + + // Check RSI positive movement with enhanced validation + double rsiDifference = currentRsi - previousRsi; + boolean rsiHasPositiveDifference = rsiDifference >= minRsiPositiveDifference; + + // Enhanced RSI trend confirmation - check longer trend + boolean rsiTrendUp = isRsiTrendUp(rsiValues, i, 3); // Check 3-period trend + boolean rsiTrendDown = isRsiTrendDown(rsiValues, i, 3); + + // ============ ENHANCED BUY SIGNAL CONDITIONS ============ + + // 1. Strong Oversold Recovery (more conservative) + boolean strongOversoldBuy = !inPosition && canTrade && rsiIsStable && + previousRsi <= (oversoldThreshold - 2) && // More oversold + currentRsi >= (oversoldThreshold + rsiConfirmationBuffer) && + rsiTrendUp; + + // 2. Enhanced Price Stability + RSI Divergence + boolean priceStableRsiDivergenceBuy = !inPosition && canTrade && rsiIsStable && + enhancedPriceStability && + rsiHasPositiveDifference && + currentRsi > 35 && currentRsi < 65 && // Tighter range + rsiTrendUp; + + // 3. Volume-confirmed momentum buy + boolean volumeConfirmedBuy = !inPosition && canTrade && + currentRsi > 40 && currentRsi < 60 && + rsiTrendUp && + currentVolume > avgVolume * 1.2 && // Above average volume + priceDifferencePercent <= 0.2; + + // ============ ENHANCED SELL SIGNAL CONDITIONS ============ + + // 1. Strong Overbought Sell (more conservative) + boolean strongOverboughtSell = inPosition && + previousRsi >= (overboughtThreshold + 2) && // More overbought + currentRsi <= (overboughtThreshold - rsiConfirmationBuffer) && + rsiTrendDown; + + // 2. Enhanced Price Stability + RSI Negative Divergence + boolean priceStableRsiNegativeDivergence = inPosition && + enhancedPriceStability && + rsiDifference <= -minRsiPositiveDifference && + currentRsi > 45; // Above midline + + // 3. Enhanced profit target and stop loss + boolean profitTargetHit = inPosition && entryPrice > 0 && + ((currentPrice - entryPrice) / entryPrice * 100.0) >= minProfitTarget; + + boolean stopLossHit = inPosition && entryPrice > 0 && + ((entryPrice - currentPrice) / entryPrice * 100.0) >= stopLossPercent; + + // 4. Time-based exit (avoid holding overnight if applicable) + boolean timeBasedExit = inPosition && (candleIndex - lastTradeIndex) > 50; // Max 50 candles holding + + // ============ EXECUTE TRADES ============ + + if (strongOversoldBuy) { + // Create TradingAction instead of Signal + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.BUY, symbolToken, tradingSymbol, + exchange, params.optInt("quantity", 1), currentPrice, 0, // Assuming Market order, no trigger price + "MARKET", productType, "NORMAL", "DAY")); // Use default order params + inPosition = true; + entryPrice = currentPrice; + lastTradeIndex = candleIndex; + log.info("[{}] STRONG OVERSOLD BUY at {}: RSI({:.1f}→{:.1f}) Price: {:.2f} Vol: {:.0f}", + getName(), currentCandle.getTimestamp(), previousRsi, currentRsi, currentPrice, currentVolume); + + } else if (priceStableRsiDivergenceBuy) { + // Create TradingAction instead of Signal + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.BUY, symbolToken, tradingSymbol, + exchange, params.optInt("quantity", 1), currentPrice, 0, + "MARKET", productType, "NORMAL", "DAY")); + inPosition = true; + entryPrice = currentPrice; + lastTradeIndex = candleIndex; + log.info("[{}] ENHANCED DIVERGENCE BUY at {}: Price Stability: {:.3f}%, RSI Diff: +{:.1f} ({:.1f}→{:.1f}) Price: {:.2f}", + getName(), currentCandle.getTimestamp(), priceDifferencePercent, rsiDifference, previousRsi, currentRsi, currentPrice); + + } else if (volumeConfirmedBuy) { + // Create TradingAction instead of Signal + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.BUY, symbolToken, tradingSymbol, + exchange, params.optInt("quantity", 1), currentPrice, 0, + "MARKET", productType, "NORMAL", "DAY")); + inPosition = true; + entryPrice = currentPrice; + lastTradeIndex = candleIndex; + log.info("[{}] VOLUME MOMENTUM BUY at {}: RSI({:.1f}→{:.1f}) Price: {:.2f} Vol: {:.0f} (Avg: {:.0f})", + getName(), currentCandle.getTimestamp(), previousRsi, currentRsi, currentPrice, currentVolume, avgVolume); + + } else if (strongOverboughtSell || priceStableRsiNegativeDivergence || profitTargetHit || stopLossHit || timeBasedExit) { + String sellReason = strongOverboughtSell ? "OVERBOUGHT" : + priceStableRsiNegativeDivergence ? "DIVERGENCE_NEGATIVE" : + profitTargetHit ? "PROFIT_TARGET" : + timeBasedExit ? "TIME_EXIT" : "STOP_LOSS"; + + // Create TradingAction instead of Signal + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.SELL, symbolToken, tradingSymbol, + exchange, params.optInt("quantity", 1), currentPrice, 0, // Use currentPrice as exit price + "MARKET", productType, "NORMAL", "DAY")); + + double profitPercent = entryPrice > 0 ? ((currentPrice - entryPrice) / entryPrice * 100.0) : 0; + log.info("[{}] {} SELL at {}: RSI({:.1f}→{:.1f}) Price: {:.2f} P&L: {:.2f}%", + getName(), sellReason, currentCandle.getTimestamp(), previousRsi, currentRsi, currentPrice, profitPercent); + + inPosition = false; + entryPrice = 0.0; + lastTradeIndex = candleIndex; + } + } + + log.info("[{}] Generated {} signals with enhanced anti-early/late trading logic.", getName(), signals.size()); + log.info("[{}] Generated {} trading actions.", getName(), signals.size()); + return signals; + } + + // Helper methods + private boolean isNearMarketOpenOrClose(Candle candle, int openAvoidMinutes, int closeAvoidMinutes) { + try { + Date timestampDate = candle.getTimestamp(); + if (timestampDate == null) { + log.debug("Candle timestamp is null for candle: {}", candle); + return false; + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String timeStr = sdf.format(timestampDate); + + // Extract time part (assuming format like "2025-03-17 09:15:00") + if (timeStr.contains(" ")) { + String timePart = timeStr.split(" ")[1]; + LocalTime candleTime = LocalTime.parse(timePart); + + LocalTime marketOpen = LocalTime.of(9, 15); + LocalTime marketClose = LocalTime.of(15, 30); + LocalTime avoidOpenUntil = marketOpen.plusMinutes(openAvoidMinutes); + LocalTime avoidCloseFrom = marketClose.minusMinutes(closeAvoidMinutes); + + return candleTime.isBefore(avoidOpenUntil) || candleTime.isAfter(avoidCloseFrom); + } + } catch (Exception e) { + log.debug("Could not parse time from candle timestamp: {}", candle.getTimestamp()); + } + return false; + } + + private boolean isRsiStable(double[] rsiValues, int currentIndex, int lookbackPeriods, double maxDeviation) { + if (currentIndex < lookbackPeriods) return false; + + double avgRsi = 0; + for (int i = currentIndex - lookbackPeriods + 1; i <= currentIndex; i++) { + avgRsi += rsiValues[i]; + } + avgRsi /= lookbackPeriods; + + for (int i = currentIndex - lookbackPeriods + 1; i <= currentIndex; i++) { + if (Math.abs(rsiValues[i] - avgRsi) > maxDeviation) { + return false; + } + } + return true; + } + + private boolean isRsiTrendUp(double[] rsiValues, int currentIndex, int periods) { + if (currentIndex < periods) return false; + for (int i = 1; i < periods; i++) { + if (rsiValues[currentIndex - i + 1] <= rsiValues[currentIndex - i]) { + return false; + } + } + return true; + } + + private boolean isRsiTrendDown(double[] rsiValues, int currentIndex, int periods) { + if (currentIndex < periods) return false; + for (int i = 1; i < periods; i++) { + if (rsiValues[currentIndex - i + 1] >= rsiValues[currentIndex - i]) { + return false; + } + } + return true; + } + + private double calculateAverageVolume(double[] volumes, int periods) { + double sum = 0; + int count = Math.min(periods, volumes.length); + for (int i = volumes.length - count; i < volumes.length; i++) { + sum += volumes[i]; + } + return sum / count; + } + + @Override + public Map> getIndicatorData(List historicalData, JSONObject params) { + int rsiPeriod = params.optInt("rsiPeriod", 14); + double[] closePrices = historicalData.stream().mapToDouble(Candle::getClose).toArray(); + + if (closePrices.length < rsiPeriod) { + return Map.of(); + } + + Core taLib = new Core(); + double[] rsiOut = new double[closePrices.length]; + MInteger outBegIdx = new MInteger(); + MInteger outNBElement = new MInteger(); + + RetCode retCode = taLib.rsi(0, closePrices.length - 1, closePrices, rsiPeriod, outBegIdx, outNBElement, rsiOut); + + if (retCode == RetCode.Success && outNBElement.value > 0) { + List rsiValuesList = new ArrayList<>(); + List rsiUpperBand = new ArrayList<>(); + List rsiLowerBand = new ArrayList<>(); + + double overbought = params.optDouble("overboughtThreshold", 75.0); + double oversold = params.optDouble("oversoldThreshold", 25.0); + + for (int i = 0; i < outBegIdx.value; i++) { + rsiValuesList.add(Double.NaN); + rsiUpperBand.add(Double.NaN); + rsiLowerBand.add(Double.NaN); + } + for (int i = 0; i < outNBElement.value; i++) { + rsiValuesList.add(rsiOut[i]); + rsiUpperBand.add(overbought); + rsiLowerBand.add(oversold); + } + + Map> indicators = new HashMap<>(); + indicators.put("RSI(" + rsiPeriod + ")", rsiValuesList); + indicators.put("RSI_Overbought", rsiUpperBand); + indicators.put("RSI_Oversold", rsiLowerBand); + return indicators; + } + return Map.of(); + } +} \ No newline at end of file diff --git a/src/main/java/com/angelbroking/smartapi/algos/strategies/Rsipricedeviation.java b/src/main/java/com/angelbroking/smartapi/algos/strategies/Rsipricedeviation.java new file mode 100644 index 0000000..96797da --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/algos/strategies/Rsipricedeviation.java @@ -0,0 +1,397 @@ +package com.angelbroking.smartapi.algos.strategies; + +import com.angelbroking.smartapi.algos.TradingAction; +import com.angelbroking.smartapi.algos.Strategy; +import com.angelbroking.smartapi.algos.ActionType; +import com.angelbroking.smartapi.models.Candle; +import com.tictactec.ta.lib.Core; +import com.tictactec.ta.lib.MInteger; +import com.tictactec.ta.lib.RetCode; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.SimpleDateFormat; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public class Rsipricedeviation implements Strategy { + private static final Logger log = LoggerFactory.getLogger(Rsipricedeviation.class); + + public String getName() { + return "RSI Price Deviation Strategy"; + } + + @Override + public List generateSignals(List historicalData, JSONObject params) { + List signals = new ArrayList<>(); + if (historicalData == null || historicalData.isEmpty()) { + log.warn("[{}] Historical data is empty or null. Cannot generate RSI signals.", getName()); + return signals; + } + + String interval = params.optString("interval", "UNKNOWN"); + log.info("[{}] Backtesting with interval: {}", getName(), interval); + + if (!historicalData.isEmpty()) { + SimpleDateFormat sdfLog = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String fromDateStr = sdfLog.format(historicalData.get(0).getTimestamp()); + String toDateStr = sdfLog.format(historicalData.get(historicalData.size() - 1).getTimestamp()); + log.info("[{}] Processing data from: {} to: {}", getName(), fromDateStr, toDateStr); + } + + // Parameters needed for TradingAction + String symbolToken = params.optString("symbolToken", "UNKNOWN_TOKEN"); + String tradingSymbol = params.optString("tradingSymbol", "UNKNOWN_SYMBOL"); + String exchange = params.optString("exchange", "NSE"); // Default or get from params + String productType = params.optString("productType", "INTRADAY"); // Default or get from params + + // Existing strategy parameters + int rsiPeriod = params.optInt("rsiPeriod", 14); + double oversoldThreshold = params.optDouble("oversoldThreshold", 25.0); + double overboughtThreshold = params.optDouble("overboughtThreshold", 75.0); + double rsiConfirmationBuffer = params.optDouble("rsiConfirmationBuffer", 2.0); + + // Price stability parameters for divergence detection + double priceStabilityThresholdPercent = params.optDouble("priceStabilityThresholdPercent", 0.15); + double minRsiPositiveDifference = params.optDouble("minRsiPositiveDifference", 2.0); // Ensure this is used + + // Control parameters + int minCandlesBetweenTrades = params.optInt("minCandlesBetweenTrades", 10); + double minProfitTarget = params.optDouble("minProfitTarget", 0.2); + double stopLossPercent = params.optDouble("stopLossPercent", 0.4); // Ensure this is used + int quantity = params.optInt("quantity", 1); + + // Market timing parameters + boolean avoidMarketOpenClose = params.optBoolean("avoidMarketOpenClose", true); + int marketOpenAvoidMinutes = params.optInt("marketOpenAvoidMinutes", 15); + int marketCloseAvoidMinutes = params.optInt("marketCloseAvoidMinutes", 15); + + log.info("[{}] Parameters - RSI Period: {}, Price Stability: {}%, Min RSI Diff: {}, Gap: {}, Avoid Open/Close: {}", + getName(), rsiPeriod, priceStabilityThresholdPercent, minRsiPositiveDifference, minCandlesBetweenTrades, avoidMarketOpenClose); + + double[] closePrices = historicalData.stream().mapToDouble(Candle::getClose).toArray(); + + if (closePrices.length < rsiPeriod + 20) { + log.warn("[{}] Not enough data for RSI calculation. Data points: {}, Need at least: {}", + getName(), closePrices.length, rsiPeriod + 20); + return signals; + } + + // Calculate RSI using TA-Lib + Core taLib = new Core(); + double[] rsiValues = new double[closePrices.length]; + MInteger begin = new MInteger(); + MInteger length = new MInteger(); + + RetCode retCode = taLib.rsi(0, closePrices.length - 1, closePrices, rsiPeriod, begin, length, rsiValues); + + if (retCode != RetCode.Success || length.value == 0) { + log.error("[{}] TA-Lib RSI calculation failed. RetCode: {}", getName(), retCode); + return signals; + } + + boolean inPosition = false; + int lastTradeIndex = -minCandlesBetweenTrades; + double entryPrice = 0.0; + + int rsiSignalStabilizationCount = params.optInt("rsiSignalStabilizationCount", 15); + + if (length.value < rsiSignalStabilizationCount + 10) { + log.warn("[{}] Not enough RSI values after stabilization. Available: {}, Need: {}", + getName(), length.value, rsiSignalStabilizationCount + 10); + return signals; + } + + // Calculate volume-based filter if available + double[] volumes = historicalData.stream().mapToDouble(candle -> + candle.getVolume() > 0 ? candle.getVolume() : 1.0).toArray(); + double avgVolume = calculateAverageVolume(volumes, Math.min(20, volumes.length)); + + // Main signal generation loop + for (int i = rsiSignalStabilizationCount + 5; i < length.value - 5; i++) { + int candleIndex = begin.value + i; + if (candleIndex >= historicalData.size() - 5 || candleIndex - 5 < 0) continue; + + double currentRsi = rsiValues[i]; + double previousRsi = rsiValues[i - 1]; + double prevPrevRsi = rsiValues[i - 2]; + + Candle currentCandle = historicalData.get(candleIndex); + Candle previousCandle = historicalData.get(candleIndex - 1); + + double currentPrice = currentCandle.getClose(); + double previousPrice = previousCandle.getClose(); + + // Market timing filter + if (avoidMarketOpenClose && isNearMarketOpenOrClose(currentCandle, marketOpenAvoidMinutes, marketCloseAvoidMinutes)) { + continue; + } + + // Volume filter + double currentVolume = currentCandle.getVolume() > 0 ? + currentCandle.getVolume() : 1.0; + if (currentVolume < avgVolume * 0.5) { + continue; + } + + // Anti-overtrading check + boolean canTrade = (candleIndex - lastTradeIndex) >= minCandlesBetweenTrades; + + // RSI stability check + boolean rsiIsStable = isRsiStable(rsiValues, i, 3, 2.0); + + // Price stability calculations + double priceDifferencePercent = Math.abs(currentPrice - previousPrice) / previousPrice * 100.0; + boolean pricesAreSimilar = priceDifferencePercent <= priceStabilityThresholdPercent; + + // Enhanced price stability check + boolean enhancedPriceStability = false; + if (candleIndex >= 3) { + double price3PeriodsAgo = historicalData.get(candleIndex - 3).getClose(); + double maxPriceInRange = Math.max(Math.max(currentPrice, previousPrice), price3PeriodsAgo); + double minPriceInRange = Math.min(Math.min(currentPrice, previousPrice), price3PeriodsAgo); + double priceRangePercent = (maxPriceInRange - minPriceInRange) / minPriceInRange * 100.0; + enhancedPriceStability = priceRangePercent <= priceStabilityThresholdPercent * 2; + } + + // RSI movement calculations + double rsiDifference = currentRsi - previousRsi; + boolean rsiHasPositiveDifference = rsiDifference >= minRsiPositiveDifference; + + // RSI trend confirmation + boolean rsiTrendUp = isRsiTrendUp(rsiValues, i, 3); + boolean rsiTrendDown = isRsiTrendDown(rsiValues, i, 3); + + // BUY SIGNAL CONDITIONS + + // 1. Strong Oversold Recovery + boolean strongOversoldBuy = !inPosition && canTrade && rsiIsStable && + previousRsi <= (oversoldThreshold - 2) && + currentRsi >= (oversoldThreshold + rsiConfirmationBuffer) && + rsiTrendUp; + + // 2. Price Stability + RSI Divergence (Main Strategy) + boolean priceStableRsiDivergenceBuy = !inPosition && canTrade && rsiIsStable && + enhancedPriceStability && + rsiHasPositiveDifference && + currentRsi > 35 && currentRsi < 65 && + rsiTrendUp; + + // 3. Volume-confirmed momentum buy + boolean volumeConfirmedBuy = !inPosition && canTrade && + currentRsi > 40 && currentRsi < 60 && + rsiTrendUp && + currentVolume > avgVolume * 1.2 && + priceDifferencePercent <= 0.2; + + // SELL SIGNAL CONDITIONS + + // 1. Strong Overbought Sell + boolean strongOverboughtSell = inPosition && + previousRsi >= (overboughtThreshold + 2) && + currentRsi <= (overboughtThreshold - rsiConfirmationBuffer) && + rsiTrendDown; + + // 2. Price Stability + RSI Negative Divergence + boolean priceStableRsiNegativeDivergence = inPosition && + enhancedPriceStability && + rsiDifference <= -minRsiPositiveDifference && + currentRsi > 45; + + // 3. Risk management exits + boolean profitTargetHit = inPosition && entryPrice > 0 && + ((currentPrice - entryPrice) / entryPrice * 100.0) >= minProfitTarget; + + boolean stopLossHit = inPosition && entryPrice > 0 && + ((entryPrice - currentPrice) / entryPrice * 100.0) >= stopLossPercent; + + boolean timeBasedExit = inPosition && (candleIndex - lastTradeIndex) > 50; + + // EXECUTE TRADES + + if (strongOversoldBuy) { + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.BUY, symbolToken, tradingSymbol, + exchange, quantity, currentPrice, 0, + "MARKET", productType, "NORMAL", "DAY")); + inPosition = true; + entryPrice = currentPrice; + lastTradeIndex = candleIndex; + log.info("[{}] OVERSOLD BUY at {}: RSI({}->{}) Price: {}", + getName(), currentCandle.getTimestamp(), + String.format("%.1f", previousRsi), String.format("%.1f", currentRsi), + String.format("%.2f", currentPrice)); + + } else if (priceStableRsiDivergenceBuy) { + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.BUY, symbolToken, tradingSymbol, + exchange, quantity, currentPrice, 0, + "MARKET", productType, "NORMAL", "DAY")); + inPosition = true; + entryPrice = currentPrice; + lastTradeIndex = candleIndex; + log.info("[{}] PRICE STABLE + RSI DIVERGENCE BUY at {}: Price Diff: {}%, RSI Diff: +{} ({}->{}) Price: {}", + getName(), currentCandle.getTimestamp(), + String.format("%.3f", priceDifferencePercent), + String.format("%.1f", rsiDifference), + String.format("%.1f", previousRsi), + String.format("%.1f", currentRsi), + String.format("%.2f", currentPrice)); + + } else if (volumeConfirmedBuy) { + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.BUY, symbolToken, tradingSymbol, + exchange, quantity, currentPrice, 0, + "MARKET", productType, "NORMAL", "DAY")); + inPosition = true; + entryPrice = currentPrice; + lastTradeIndex = candleIndex; + log.info("[{}] VOLUME MOMENTUM BUY at {}: RSI({}->{}) Price: {} Vol: {}", + getName(), currentCandle.getTimestamp(), + String.format("%.1f", previousRsi), String.format("%.1f", currentRsi), + String.format("%.2f", currentPrice), + String.format("%.0f", currentVolume)); + + } else if (strongOverboughtSell || priceStableRsiNegativeDivergence || profitTargetHit || stopLossHit || timeBasedExit) { + String sellReason = strongOverboughtSell ? "OVERBOUGHT" : + priceStableRsiNegativeDivergence ? "DIVERGENCE_NEGATIVE" : + profitTargetHit ? "PROFIT_TARGET" : + timeBasedExit ? "TIME_EXIT" : "STOP_LOSS"; + + signals.add(new TradingAction(currentCandle.getTimestamp(), ActionType.SELL, symbolToken, tradingSymbol, + exchange, quantity, currentPrice, 0, + "MARKET", productType, "NORMAL", "DAY")); + + double profitPercent = entryPrice > 0 ? ((currentPrice - entryPrice) / entryPrice * 100.0) : 0; + log.info("[{}] {} SELL at {}: RSI({}->{}) Price: {} P&L: {}%", + getName(), sellReason, currentCandle.getTimestamp(), + String.format("%.1f", previousRsi), + String.format("%.1f", currentRsi), + String.format("%.2f", currentPrice), + String.format("%.2f", profitPercent)); + + inPosition = false; + entryPrice = 0.0; + lastTradeIndex = candleIndex; + } + } + + log.info("[{}] Generated {} signals.", getName(), signals.size()); + return signals; + } + + // Helper methods + private boolean isNearMarketOpenOrClose(Candle candle, int openAvoidMinutes, int closeAvoidMinutes) { + try { + String timeStr = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(candle.getTimestamp()); + if (timeStr.contains(" ")) { + String timePart = timeStr.split(" ")[1]; + LocalTime candleTime = LocalTime.parse(timePart); + + LocalTime marketOpen = LocalTime.of(9, 15); + LocalTime marketClose = LocalTime.of(15, 30); + LocalTime avoidOpenUntil = marketOpen.plusMinutes(openAvoidMinutes); + LocalTime avoidCloseFrom = marketClose.minusMinutes(closeAvoidMinutes); + + return candleTime.isBefore(avoidOpenUntil) || candleTime.isAfter(avoidCloseFrom); + } + } catch (Exception e) { + log.debug("Could not parse time from candle timestamp: {}", candle.getTimestamp()); + } + return false; + } + + private boolean isRsiStable(double[] rsiValues, int currentIndex, int lookbackPeriods, double maxDeviation) { + if (currentIndex < lookbackPeriods) return false; + + double avgRsi = 0; + for (int i = currentIndex - lookbackPeriods + 1; i <= currentIndex; i++) { + avgRsi += rsiValues[i]; + } + avgRsi /= lookbackPeriods; + + for (int i = currentIndex - lookbackPeriods + 1; i <= currentIndex; i++) { + if (Math.abs(rsiValues[i] - avgRsi) > maxDeviation) { + return false; + } + } + return true; + } + + private boolean isRsiTrendUp(double[] rsiValues, int currentIndex, int periods) { + if (currentIndex < periods) return false; + for (int i = 1; i < periods; i++) { + if (rsiValues[currentIndex - i + 1] <= rsiValues[currentIndex - i]) { + return false; + } + } + return true; + } + + private boolean isRsiTrendDown(double[] rsiValues, int currentIndex, int periods) { + if (currentIndex < periods) return false; + for (int i = 1; i < periods; i++) { + if (rsiValues[currentIndex - i + 1] >= rsiValues[currentIndex - i]) { + return false; + } + } + return true; + } + + private double calculateAverageVolume(double[] volumes, int periods) { + double sum = 0; + int count = Math.min(periods, volumes.length); + for (int i = volumes.length - count; i < volumes.length; i++) { + sum += volumes[i]; + } + return sum / count; + } + + @Override + public Map> getIndicatorData(List historicalData, JSONObject params) { + int rsiPeriod = params.optInt("rsiPeriod", 14); + double[] closePrices = historicalData.stream().mapToDouble(Candle::getClose).toArray(); + + if (closePrices.length < rsiPeriod) { + return new HashMap<>(); + } + + Core taLib = new Core(); + double[] rsiOut = new double[closePrices.length]; + MInteger outBegIdx = new MInteger(); + MInteger outNBElement = new MInteger(); + + RetCode retCode = taLib.rsi(0, closePrices.length - 1, closePrices, rsiPeriod, outBegIdx, outNBElement, rsiOut); + + if (retCode == RetCode.Success && outNBElement.value > 0) { + List rsiValuesList = new ArrayList<>(); + List rsiUpperBand = new ArrayList<>(); + List rsiLowerBand = new ArrayList<>(); + + double overbought = params.optDouble("overboughtThreshold", 75.0); + double oversold = params.optDouble("oversoldThreshold", 25.0); + + for (int i = 0; i < outBegIdx.value; i++) { + rsiValuesList.add(Double.NaN); + rsiUpperBand.add(Double.NaN); + rsiLowerBand.add(Double.NaN); + } + for (int i = 0; i < outNBElement.value; i++) { + rsiValuesList.add(rsiOut[i]); + rsiUpperBand.add(overbought); + rsiLowerBand.add(oversold); + } + + Map> indicators = new HashMap<>(); + indicators.put("RSI(" + rsiPeriod + ")", rsiValuesList); + indicators.put("RSI_Overbought", rsiUpperBand); + indicators.put("RSI_Oversold", rsiLowerBand); + return indicators; + } + return new HashMap<>(); + } +} + +// Removed the Strategy interface as it is now in its own file diff --git a/src/main/java/com/angelbroking/smartapi/gui/SmartApiGui.java b/src/main/java/com/angelbroking/smartapi/gui/SmartApiGui.java new file mode 100644 index 0000000..85a8b74 --- /dev/null +++ b/src/main/java/com/angelbroking/smartapi/gui/SmartApiGui.java @@ -0,0 +1,2178 @@ +package com.angelbroking.smartapi.gui; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.angelbroking.smartapi.SmartConnect; +import com.angelbroking.smartapi.algos.Backtester; +import com.angelbroking.smartapi.algos.Strategy; +import com.angelbroking.smartapi.algos.strategies.MACrossoverStrategy; +import com.angelbroking.smartapi.algos.strategies.RsiDeviationStrategy; +import com.angelbroking.smartapi.http.exceptions.SmartAPIException; +import com.angelbroking.smartapi.models.BacktestReport; +import com.angelbroking.smartapi.models.Candle; +import com.angelbroking.smartapi.models.Order; +import com.angelbroking.smartapi.models.OrderParams; +import com.angelbroking.smartapi.models.TradeLog; +import com.angelbroking.smartapi.models.User; +import com.angelbroking.smartapi.utils.Constants; +import com.google.gson.Gson; +import com.tictactec.ta.lib.Core; +import com.tictactec.ta.lib.MInteger; +import com.tictactec.ta.lib.RetCode; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar.ButtonData; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.DatePicker; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.PasswordField; +import javafx.scene.control.Separator; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Stage; + +public class SmartApiGui extends Application { + + private SmartConnect smartConnect; + private Stage primaryStage; + + private final String lightModeCss = getClass().getResource("/styles/light-mode.css").toExternalForm(); + private static final Logger log = LoggerFactory.getLogger(SmartApiGui.class); + + private final String darkModeCss = getClass().getResource("/styles/dark-mode.css").toExternalForm(); + private WebEngine chartWebEngine; + private boolean isLiveChartActive = false; + private String currentLiveToken = null; + private String currentLiveExchange = null; + + // Removed hardcoded credentials + // private static final String API_KEY = "YOUR_API_KEY"; // Example: "HFnOUXPi"; + // private static final String USERNAME = "YOUR_CLIENT_ID"; // Example: "AAAL432762"; + // private static final String PASSWORD = "YOUR_PASSWORD_OR_PIN"; // Example: "4321"; + + private CheckBox candleDataTabLiveFeedCheckBox; + private TextField candleDataTabSymbolTokenField; + private ComboBox candleDataTabExchangeComboBox; + private DatePicker candleDataTabFromDatePicker; + private TextField candleDataTabFromTimeField; + private DatePicker candleDataTabToDatePicker; + private TextField candleDataTabToTimeField; + private Button candleDataTabFetchCandleDataButton; + + private WebEngine historicalDataChartEngine; + private WebView historicalDataChartWebView; + private Stage detachedHistoricalStage; + private WebEngine detachedHistoricalWebEngine; + private boolean isHistoricalChartDetached = false; + private Label historicalChartPlaceholder; // Added declaration + private StackPane historicalChartContainer; // Added declaration + + private Stage detachedBacktestChartStage; + private WebEngine detachedBacktestChartEngine; + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + this.primaryStage = primaryStage; + this.smartConnect = new SmartConnect(); + // API key will be set from the login form + // this.smartConnect.setApiKey(API_KEY); + + primaryStage.setTitle("SmartAPI Login"); + showLoginScreen(); + } + + private void showLoginScreen() { + VBox loginLayout = new VBox(20); + loginLayout.setAlignment(Pos.CENTER); + loginLayout.setPadding(new Insets(40, 50, 40, 50)); + loginLayout.getStyleClass().add("login-pane"); + + Text scenetitle = new Text("SmartAPI Login"); + scenetitle.setFont(Font.font("Arial", FontWeight.BOLD, 28)); + scenetitle.getStyleClass().add("login-title"); + + GridPane formGrid = new GridPane(); + formGrid.setAlignment(Pos.CENTER); + formGrid.setHgap(10); + formGrid.setVgap(15); + + Label apiKeyLabel = new Label("API Key:"); + formGrid.add(apiKeyLabel, 0, 0); + TextField apiKeyTextField = new TextField(); + apiKeyTextField.setPromptText("Enter your API Key"); + apiKeyTextField.setPrefWidth(250); + formGrid.add(apiKeyTextField, 1, 0); + + Label userNameLabel = new Label("Username (Client ID):"); + formGrid.add(userNameLabel, 0, 1); + TextField userNameTextField = new TextField(); + userNameTextField.setPromptText("Enter your Client ID"); + userNameTextField.setPrefWidth(250); + formGrid.add(userNameTextField, 1, 1); + + Label pwLabel = new Label("Password:"); + formGrid.add(pwLabel, 0, 2); + PasswordField pwBox = new PasswordField(); + // pwBox.setText(PASSWORD); // Removed pre-filled password + pwBox.setPromptText("Enter your Password/PIN"); + pwBox.setPrefWidth(250); + formGrid.add(pwBox, 1, 2); + + Label totpLabel = new Label("TOTP:"); + formGrid.add(totpLabel, 0, 3); + TextField totpTextField = new TextField(); + totpTextField.setPromptText("Enter current TOTP"); + totpTextField.setPrefWidth(250); + formGrid.add(totpTextField, 1, 3); + + Button loginButton = new Button("Login"); + loginButton.setPrefWidth(100); + loginButton.getStyleClass().add("login-button"); + loginButton.setDefaultButton(true); + formGrid.add(loginButton, 1, 4); + + final Text actiontarget = new Text(); + actiontarget.setWrappingWidth(250); + actiontarget.getStyleClass().add("login-action-target"); + + totpTextField.setOnKeyPressed(event -> { + if (event.getCode() == KeyCode.ENTER) { + loginButton.fire(); + } + }); + + loginLayout.getChildren().addAll(scenetitle, formGrid, actiontarget); + + loginButton.setOnAction(e -> { + String apiKey = apiKeyTextField.getText(); + String clientCode = userNameTextField.getText(); + String password = pwBox.getText(); + String totp = totpTextField.getText(); + + if (totp == null || totp.trim().isEmpty()) { + updateActionTargetStyle(actiontarget, "TOTP cannot be empty.", false); + actiontarget.setText("TOTP cannot be empty."); + return; + } + + smartConnect.setApiKey(apiKey); + + updateActionTargetStyle(actiontarget, "Attempting login...", null); + actiontarget.setText("Attempting login..."); + loginButton.setDisable(true); + + new Thread(() -> { + try { + User user = smartConnect.generateSession(clientCode, password, totp); + Platform.runLater(() -> { + loginButton.setDisable(false); + if (user != null && user.getAccessToken() != null) { + updateActionTargetStyle(actiontarget, "Login successful!", true); + showDashboardScreen(user); + } else { + String errorMessage = "Login failed. "; + updateActionTargetStyle(actiontarget, errorMessage + "Please check credentials and TOTP.", false); + } + }); + } catch (Exception ex) { + Platform.runLater(() -> { + loginButton.setDisable(false); + updateActionTargetStyle(actiontarget, "Login error: " + ex.getMessage(), false); + ex.printStackTrace(); + }); + } + }).start(); + }); + + Scene scene = new Scene(loginLayout, 450, 450); + primaryStage.setScene(scene); + applyStylesheet(scene, lightModeCss); + primaryStage.show(); + } + + private void updateActionTargetStyle(Text actionTarget, String message, Boolean success) { + actionTarget.setText(message); + actionTarget.getStyleClass().removeAll("success", "error"); + if (success == null) { + } else if (success) { + actionTarget.getStyleClass().add("success"); + } else { + actionTarget.getStyleClass().add("error"); + } + } + + private void applyStylesheet(Scene scene, String cssPath) { + scene.getStylesheets().clear(); + scene.getStylesheets().add(cssPath); + } + + private TableView orderTable; + private StackPane contentArea; + private TableView holdingsTable; + private TableView positionsTable; + + private void showDashboardScreen(User user) { + this.orderTable = new TableView<>(); + primaryStage.setTitle("SmartAPI Dashboard"); + + BorderPane rootLayout = new BorderPane(); + + VBox topControls = new VBox(10); + topControls.setAlignment(Pos.CENTER_LEFT); + topControls.setPadding(new Insets(10)); + + GridPane topGrid = new GridPane(); + topGrid.setHgap(20); + topGrid.setAlignment(Pos.CENTER_LEFT); + + Text welcomeText = new Text("Welcome, " + (user.getUserName() != null ? user.getUserName() : user.getUserId()) + "!"); + welcomeText.getStyleClass().add("welcome-text"); + welcomeText.setFont(Font.font("Arial", FontWeight.NORMAL, 16)); + topGrid.add(welcomeText, 0, 0); + + ToggleButton themeToggle = new ToggleButton("Dark Mode"); + themeToggle.setSelected(primaryStage.getScene().getStylesheets().contains(darkModeCss)); + themeToggle.setOnAction(event -> { + String cssToApply; + String themeNameForJs; + if (themeToggle.isSelected()) { + cssToApply = darkModeCss; + themeToggle.setText("Light Mode"); + themeNameForJs = "dark"; + } else { + cssToApply = lightModeCss; + themeToggle.setText("Dark Mode"); + themeNameForJs = "light"; + } + applyStylesheet(primaryStage.getScene(), cssToApply); + + // Update embedded historical chart engine + if (historicalDataChartEngine != null) { + historicalDataChartEngine.executeScript("applyTheme('" + themeNameForJs + "');"); + } + // Update detached historical chart engine + if (detachedHistoricalWebEngine != null && detachedHistoricalStage != null && detachedHistoricalStage.isShowing()) { + detachedHistoricalWebEngine.executeScript("applyTheme('" + themeNameForJs + "');"); + applyStylesheet(detachedHistoricalStage.getScene(), cssToApply); + } + // Update detached algo chart engine + if (detachedBacktestChartEngine != null && detachedBacktestChartStage != null && detachedBacktestChartStage.isShowing()) { + detachedBacktestChartEngine.executeScript("applyTheme('" + themeNameForJs + "');"); + applyStylesheet(detachedBacktestChartStage.getScene(), cssToApply); + } + }); + topGrid.add(themeToggle, 1, 0); + + Button logoutButton = new Button("Logout"); + logoutButton.setOnAction(e -> { + logoutButton.setDisable(true); + new Thread(() -> { + try { + if (smartConnect.getAccessToken() != null) { + JSONObject logoutResponse = smartConnect.logout(); + Platform.runLater(() -> { + if (logoutResponse != null && logoutResponse.optBoolean("status", false)) { + System.out.println("Logout successful via API."); + } else { + System.err.println("Logout API call failed or status false."); + } + }); + } + } catch (Exception ex) { + System.err.println("Error during logout: " + ex.getMessage()); + ex.printStackTrace(); + } finally { + Platform.runLater(() -> { + logoutButton.setDisable(false); + primaryStage.setTitle("SmartAPI Login"); + showLoginScreen(); + }); + } + }).start(); + }); + VBox logoutBox = new VBox(logoutButton); + logoutBox.setAlignment(Pos.CENTER_RIGHT); + GridPane.setHgrow(logoutBox, javafx.scene.layout.Priority.ALWAYS); + topGrid.add(logoutBox, 2, 0); + + topControls.getChildren().add(topGrid); + rootLayout.setTop(topControls); + + VBox navPane = new VBox(10); + navPane.setPadding(new Insets(10)); + navPane.getStyleClass().add("nav-pane"); + navPane.setPrefWidth(180); + + contentArea = new StackPane(); + StackPane contentArea = new StackPane(); + contentArea.setPadding(new Insets(10)); + + GridPane profileDetailsGrid = new GridPane(); + GridPane detailsGrid = new GridPane(); + detailsGrid.setHgap(10); + detailsGrid.setVgap(8); + detailsGrid.setPadding(new Insets(10)); + + Label userIdLabelKey = new Label("User ID:"); + Text userIdText = new Text("Loading..."); + userIdText.getStyleClass().add("profile-value-text"); + + Label userNameLabelKey = new Label("User Name:"); + Text userNameText = new Text("Loading..."); + userNameText.getStyleClass().add("profile-value-text"); + + Label emailLabelKey = new Label("Email:"); + Text emailText = new Text("Loading..."); + emailText.getStyleClass().add("profile-value-text"); + + Label mobileLabelKey = new Label("Mobile No:"); + Text mobileText = new Text("Loading..."); + mobileText.getStyleClass().add("profile-value-text"); + + Label exchangesLabelKey = new Label("Exchanges:"); + Text exchangesText = new Text("Loading..."); + exchangesText.getStyleClass().add("profile-value-text"); + + Label productsLabelKey = new Label("Products:"); + Text productsText = new Text("Loading..."); + productsText.getStyleClass().add("profile-value-text"); + + Label orderTypesLabelKey = new Label("Order Types:"); + Text orderTypesText = new Text("Loading..."); + orderTypesText.getStyleClass().add("profile-value-text"); + + profileDetailsGrid.add(userIdLabelKey, 0, 0); + profileDetailsGrid.add(userIdText, 1, 0); + profileDetailsGrid.add(userNameLabelKey, 0, 1); + profileDetailsGrid.add(userNameText, 1, 1); + profileDetailsGrid.add(emailLabelKey, 0, 2); + profileDetailsGrid.add(emailText, 1, 2); + profileDetailsGrid.add(mobileLabelKey, 0, 3); + profileDetailsGrid.add(mobileText, 1, 3); + + profileDetailsGrid.add(exchangesLabelKey, 0, 4); + profileDetailsGrid.add(exchangesText, 1, 4); + GridPane.setValignment(exchangesLabelKey, javafx.geometry.VPos.TOP); + exchangesText.setWrappingWidth(350); + + profileDetailsGrid.add(productsLabelKey, 0, 5); + profileDetailsGrid.add(productsText, 1, 5); + GridPane.setValignment(productsLabelKey, javafx.geometry.VPos.TOP); + productsText.setWrappingWidth(350); + + profileDetailsGrid.add(orderTypesLabelKey, 0, 6); + profileDetailsGrid.add(orderTypesText, 1, 6); + GridPane.setValignment(orderTypesLabelKey, javafx.geometry.VPos.TOP); + orderTypesText.setWrappingWidth(350); + + new Thread(() -> { + try { + User fullProfile = smartConnect.getProfile(); + Platform.runLater(() -> { + if (fullProfile != null) { + userIdText.setText(fullProfile.getUserId() != null ? fullProfile.getUserId() : "N/A"); + userNameText.setText(fullProfile.getUserName() != null ? fullProfile.getUserName() : "N/A"); + emailText.setText(fullProfile.getEmail() != null && !fullProfile.getEmail().isEmpty() ? fullProfile.getEmail() : "N/A"); + mobileText.setText(fullProfile.getMobileNo() != null && !fullProfile.getMobileNo().isEmpty() ? fullProfile.getMobileNo() : "N/A"); + exchangesText.setText(fullProfile.getExchanges() != null ? String.join(", ", fullProfile.getExchanges()) : "N/A"); + productsText.setText(fullProfile.getProducts() != null ? String.join(", ", fullProfile.getProducts()) : "N/A"); + orderTypesText.setText(fullProfile.getOrderTypes() != null && fullProfile.getOrderTypes().isEmpty() ? String.join(", ", fullProfile.getOrderTypes()) : "N/A"); + } else { + userIdText.setText("Failed to load"); + userNameText.setText("Failed to load"); + } + }); + } catch (Exception ex) { + Platform.runLater(() -> { + userIdText.setText("Error loading profile"); + ex.printStackTrace(); + }); + } + }).start(); + + BorderPane ordersContentLayout = new BorderPane(); + + VBox ordersControls = new VBox(10); + ordersControls.setPadding(new Insets(0, 0, 10, 0)); + Button refreshOrderBookButton = new Button("Refresh Order Book"); + Button placeNewOrderButton = new Button("Place New Order"); + Button modifyOrderButton = new Button("Modify Order"); + modifyOrderButton.setOnAction(e -> showPlaceOrderDialog(orderTable.getSelectionModel().getSelectedItem())); + modifyOrderButton.setDisable(true); + modifyOrderButton.setOnAction(e -> showPlaceOrderDialog(orderTable.getSelectionModel().getSelectedItem())); + modifyOrderButton.setOnAction(e -> showPlaceOrderDialog(this.orderTable.getSelectionModel().getSelectedItem())); + + placeNewOrderButton.setOnAction(e -> showPlaceOrderDialog()); + modifyOrderButton.setOnAction(e -> showPlaceOrderDialog(orderTable.getSelectionModel().getSelectedItem())); + modifyOrderButton.setDisable(true); + + ordersControls.getChildren().addAll(refreshOrderBookButton, placeNewOrderButton, modifyOrderButton); + ordersContentLayout.setTop(ordersControls); + + TableView orderTable = new TableView<>(); + setupOrderTableColumns(orderTable); + ordersContentLayout.setCenter(orderTable); + + refreshOrderBookButton.setOnAction(e -> fetchOrderBook(orderTable)); + + orderTable.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> { + modifyOrderButton.setDisable(newSelection == null); + }); + + VBox holdingsPositionsContent = new VBox(10); + holdingsPositionsContent.setPadding(new Insets(15)); + holdingsPositionsContent.setAlignment(Pos.CENTER); + holdingsPositionsContent.getChildren().add(new Label("Holdings and Positions will be displayed here.")); + + BorderPane holdingsPositionsContentLayout = new BorderPane(); + holdingsPositionsContentLayout.setPadding(new Insets(15)); + + VBox holdingsControls = new VBox(10); + holdingsControls.setPadding(new Insets(0, 0, 10, 0)); + Button refreshHoldingsButton = new Button("Refresh Holdings"); + Button refreshPositionsButton = new Button("Refresh Positions"); + holdingsControls.getChildren().addAll(refreshHoldingsButton, refreshPositionsButton); + holdingsPositionsContentLayout.setTop(holdingsControls); + + Label holdingsHeaderLabel = new Label("Holdings"); + holdingsHeaderLabel.setFont(Font.font("Arial", FontWeight.BOLD, 14)); + GridPane totalHoldingsGrid = new GridPane(); + totalHoldingsGrid.setHgap(10); + totalHoldingsGrid.setVgap(5); + totalHoldingsGrid.setPadding(new Insets(0, 0, 10, 0)); + Label totalHoldingValueLabel = new Label("Total Holding Value: N/A"); + Label totalInvestmentValueLabel = new Label("Total Investment: N/A"); + Label totalPnlLabel = new Label("Total P&L: N/A"); + totalHoldingsGrid.add(totalHoldingValueLabel, 0, 0); + totalHoldingsGrid.add(totalInvestmentValueLabel, 1, 0); + totalHoldingsGrid.add(totalPnlLabel, 2, 0); + + this.holdingsTable = new TableView<>(); + setupHoldingsTableColumns(holdingsTable); + + VBox holdingsSection = new VBox(10, holdingsHeaderLabel, totalHoldingsGrid, holdingsTable); + + Label positionsHeaderLabel = new Label("Positions"); + positionsHeaderLabel.setFont(Font.font("Arial", FontWeight.BOLD, 14)); + this.positionsTable = new TableView<>(); + setupPositionsTableColumns(positionsTable); + + VBox positionsSection = new VBox(10, positionsHeaderLabel, positionsTable); + + VBox centerContent = new VBox(20, holdingsSection, positionsSection); + centerContent.setPadding(new Insets(10, 0, 0, 0)); + holdingsPositionsContentLayout.setCenter(centerContent); + + refreshHoldingsButton.setOnAction(e -> fetchHoldingsData(totalHoldingValueLabel, totalInvestmentValueLabel, totalPnlLabel)); + refreshPositionsButton.setOnAction(e -> fetchPositionsData()); + + Button profileNavButton = new Button("Profile"); + profileNavButton.setMaxWidth(Double.MAX_VALUE); + profileNavButton.getStyleClass().add("nav-button"); + + Button ordersNavButton = new Button("Orders"); + ordersNavButton.setMaxWidth(Double.MAX_VALUE); + ordersNavButton.getStyleClass().add("nav-button"); + + Button holdingsNavButton = new Button("Holdings & Positions"); + holdingsNavButton.setMaxWidth(Double.MAX_VALUE); + holdingsNavButton.getStyleClass().add("nav-button"); + + Button historicalDataNavButton = new Button("Historical Data"); + historicalDataNavButton.setMaxWidth(Double.MAX_VALUE); + historicalDataNavButton.getStyleClass().add("nav-button"); + Button backtestingNavButton = new Button("Backtesting"); + backtestingNavButton.setMaxWidth(Double.MAX_VALUE); + backtestingNavButton.getStyleClass().add("nav-button"); + + List