Fine-Tuning GPT model to Predict NIFTY50 Prices
In the previous post we have prepared a dataset consisting of minute level Nifty50 index price for the last 10 years. We've done some data cleansing and split the dataset into training and validation set. Please skim through that if you haven't yet. In this blog we're going to use the training set to fine-tune a GPT model in Azure AI Foundry. Then we'll use the validation set to check whether the fine-tuned model can make a profit for us.
Prepare Training Conversations
The prepared dataset is stored under the dataset directory. We'll use the train_price_movements.csv for fine-tuning the model. Here is the glimpse of the content of the file.
1 2 3 |
|
date | 09:15 | 09:16 | 09:17 | 09:18 | 09:19 | 09:20 | 09:21 | 09:22 | 09:23 | ... | 15:20 | 15:21 | 15:22 | 15:23 | 15:24 | 15:25 | 15:26 | 15:27 | 15:28 | 15:29 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2015-01-09 | 0.0 | 0.09 | -0.06 | 0.08 | 0.08 | -0.00 | 0.00 | 0.02 | -0.09 | ... | -0.01 | 0.02 | 0.01 | 0.01 | 0.03 | -0.00 | -0.04 | -0.01 | 0.01 | -0.03 |
1 | 2015-01-12 | 0.0 | -0.45 | 0.01 | 0.04 | 0.06 | 0.05 | -0.02 | 0.03 | 0.06 | ... | 0.04 | -0.04 | 0.02 | 0.00 | -0.01 | 0.04 | 0.01 | -0.01 | 0.00 | -0.01 |
2 | 2015-01-13 | 0.0 | 0.11 | -0.08 | -0.05 | -0.02 | -0.03 | -0.01 | 0.01 | -0.09 | ... | 0.08 | 0.01 | 0.01 | 0.01 | 0.01 | 0.00 | 0.00 | -0.01 | -0.02 | 0.03 |
3 | 2015-01-14 | 0.0 | -0.08 | 0.07 | 0.02 | -0.04 | -0.01 | -0.03 | -0.11 | 0.04 | ... | 0.08 | -0.01 | 0.02 | 0.03 | 0.01 | 0.01 | 0.00 | 0.02 | 0.02 | 0.01 |
4 | 2015-01-15 | 0.0 | 0.18 | -0.55 | -0.10 | 0.18 | 0.32 | -0.23 | -0.12 | 0.22 | ... | -0.04 | -0.07 | -0.11 | -0.06 | -0.11 | -0.05 | 0.06 | -0.05 | 0.03 | 0.00 |
Our expectation is when feed the price movement till 2.30PM, the model should tell us what is the most probable price it will reach before the market close. Based on that price we'll either buy or sell (short) the index at 2.30.
OpenAI GPTs are conversational models. So, the training dataset should be converted into set of conversations. Each conversation should have a system message, a user message and an assistant message. In our case the system message will be a role assignment for the LLM. The user message will be the market movement and the assistant message will be the target price for that day.
How to determine target price?
Before converting the current dataset into conversations, we need to establish a method for determining the target price for each day in the training dataset. This is essential because the training conversation requires the target_price
to be included as the assistant's response. Therefore, we must devise a systematic approach to calculate the target price using the price movement data available after 2:30 PM.
We have price movement in percentage from 2.31PM to 3.29PM. As we're going to square off our position by 3.25PM itself, let's consider only upto 3.25PM. And each number in the dataset is relative percentage difference from the previous price. So, it is a geometric progression.
We should know price oscillations with respect to the price at 2.30 at every minute until 3.25. So, if we're buying at 2.30, we should sell at a time when the price reached the peak. Or if we're selling (shorting) we should buy back when the price is at the bottom of the graph. Here we are going to identify the most probable price (or movement) close to the maximum or minimum and use that as the target price.
Please note that we're not taking the best price here. Because the best price will be an outlier in the sequence. It may not properly represent the trend in the price setting. So, take the price ocillation range and take the first standard deviation. By this we have approximately 66% possibility to reach the target price. And this is our expectation from the LLM also. To make profit in long-term, we trade high profit in a day for more stable returns.
Let's create a sequence of price difference with respect to the base price at 2.30 and find it's mean and std. If the mean is positive, let's set the target price as mean+std
. Otherwise the short target price is mean-std
. Let's do this for first sequence in this dataset.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
Price movement at 14:30 on 2015-01-09: 0.01%
Cumulative price changes from 2:30 PM baseline:
Mean: 1.35
Std: 0.24
Min: 0.90
Max: 1.84
Call. Target Percentage: 1.0053
By multiplying the target_percentage
with the base price i.e. the price at 2.30PM, we'll get the target price. We have the price sequence data in dataset/train_daily_opens.csv
. Let's calculate the position and target price for 09-01-2015
.
1 2 3 4 5 6 7 8 |
|
Base Price at 14:30 on 2015-01-09: 8240.0
Call. Target Price: 8283.672
To prepare the training dataset for fine-tuning, we need to convert the entire dataset into a conversational format and save it as a JSONL file. For each day in the dataset, we will calculate the multiplication factor (target_percentage
) following the above method. The model will be trained to predict only the target_percentage
given the market movement sequence. The target_price
will be derived later using the base_price
at the time of evaluation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
|
Generated 1975 training conversations
{'messages': [{'role': 'system', 'content': 'You are a stock market expert. You will predict the most profit making position and what is the expected percentage change in the next 1 hour given the percentage movement between 9.15 AM and 2.30 PM.'}, {'role': 'user', 'content': 'The price movements in percentage from 9:15 AM to 2:30 PM on 2015-01-09 (Friday) are as follows: [0.0, 0.09, -0.06, 0.08, 0.08, -0.0, 0.0, 0.02, -0.09, 0.0, 0.08, -0.08, -0.07, 0.03, -0.02, 0.04, -0.06, 0.0, -0.08, 0.01, 0.07, -0.05, 0.0, -0.07, 0.04, 0.01, 0.02, 0.01, -0.07, -0.03, 0.09, -0.05, 0.02, -0.04, -0.01, 0.06, -0.1, 0.04, 0.0, -0.0, -0.03, -0.01, 0.05, 0.04, -0.04, 0.03, 0.02, 0.01, 0.02, -0.02, 0.01, -0.02, 0.02, -0.03, 0.0, -0.04, -0.01, -0.04, 0.02, 0.02, 0.03, 0.01, -0.02, -0.0, 0.03, 0.01, 0.0, 0.03, 0.0, -0.01, 0.02, 0.01, -0.02, 0.02, 0.03, -0.0, -0.01, -0.04, 0.02, -0.01, 0.01, -0.02, -0.03, 0.02, -0.01, 0.02, -0.02, -0.02, 0.0, -0.01, -0.01, -0.02, 0.03, -0.02, 0.01, -0.0, -0.01, 0.02, 0.02, -0.01, 0.02, -0.02, 0.03, 0.01, 0.0, -0.01, 0.02, -0.01, -0.05, 0.03, 0.03, -0.02, -0.02, -0.02, -0.02, 0.01, 0.02, -0.01, -0.0, 0.04, -0.04, -0.0, -0.03, -0.02, -0.01, 0.02, -0.01, 0.02, 0.02, -0.13, -0.03, -0.03, 0.02, -0.0, -0.0, -0.02, -0.13, 0.06, -0.07, 0.0, 0.04, 0.05, -0.02, -0.05, -0.01, -0.01, 0.02, 0.03, 0.04, -0.04, 0.0, 0.04, 0.0, -0.01, 0.01, -0.06, 0.02, -0.01, 0.01, -0.01, 0.02, 0.02, -0.01, 0.01, 0.0, 0.0, -0.04, 0.02, -0.05, -0.06, -0.45, 0.05, 0.02, 0.01, 0.04, -0.11, -0.09, -0.02, 0.12, 0.04, 0.08, 0.02, -0.05, 0.01, 0.01, -0.0, -0.05, 0.03, 0.0, 0.07, -0.06, 0.04, -0.09, -0.08, 0.05, 0.01, -0.0, 0.03, 0.0, -0.06, -0.04, 0.0, 0.01, -0.02, -0.08, 0.03, 0.06, -0.02, -0.01, 0.06, 0.03, 0.07, -0.03, -0.1, 0.1, -0.01, -0.02, 0.15, 0.21, -0.02, 0.03, -0.17, 0.15, 0.07, 0.12, 0.08, -0.06, 0.07, -0.06, -0.05, 0.02, 0.07, -0.03, 0.07, 0.01, 0.02, 0.01, -0.06, -0.01, -0.07, -0.15, -0.07, -0.04, 0.01, -0.07, 0.16, -0.06, -0.07, -0.04, 0.02, 0.01, 0.0, 0.04, 0.02, 0.04, -0.0, 0.04, -0.08, 0.02, 0.07, -0.0, -0.07, 0.03, 0.03, 0.04, 0.02, -0.07, 0.01, -0.04, 0.01, 0.03, -0.0, 0.01, -0.02, 0.0, -0.1, 0.04, 0.02, -0.03, -0.04, -0.05, -0.02, -0.01, -0.17, -0.12, 0.14, -0.09, 0.02, 0.0, 0.1, 0.01, 0.04, 0.03, 0.05, -0.08, 0.08, -0.0, 0.04, 0.07, -0.02, -0.16, 0.09, 0.06, 0.04, 0.01, 0.01, 0.02, 0.04, 0.01, -0.05, -0.08, 0.04, -0.03, 0.0, 0.02, 0.01, 0.06, -0.04, -0.12, 0.01, 0.06, 0.04, 0.05, 0.01, 0.07, 0.01, -0.05, -0.01, 0.06, 0.02, -0.03, -0.0, -0.03, 0.14, -0.01, -0.03, 0.07, 0.05, 0.09, 0.02, -0.02, -0.07, -0.0, -0.02, -0.04].'}, {'role': 'assistant', 'content': '1.0053'}]}
Training GPT LLM
I refer to the Azure Documentation to fine-tune an OpenAI GPT model. To do so we need to use Serverless Training Infrastructure. This is cheaper and easier to handle compared to the alternative - Managed Compute Infrastructure. Azure offers three kinds of fine-tuning.
- SFT - Supervised Fine Tuning
- DPO - Direct Preference Optimization
- RFT - Reinforcement Fine-Tuning
Though RFT is recommended for tasks like stock market prediction, we'll go with SFT this time for its simplicity. Our training file is prepared for SFT only. In the future articles, we'll explore other training methods and compare their performance.
To follow this article and fine-tune a model, you need to be an owner or contributor of a Paid Azure Account. Free or trail Azure accounts may not work. We Microsoft employees get $150 Azure credit per month. I'm using that credit for this project.
Now we're going to follow the step-by-step instructions provided at Customize a model with fine-tuning. It is a well written document with screenshots. So, I'm not going to elaborate each step here in this blog.
Here is a short quote from the Azure documentation.
If you train the model on a large amount of internal data, without first pruning the dataset for only the highest quality examples you could end up with a model that performs much worse than expected.
This is the reason we have done all the pre-processing in the previous dataset prep blog.
Below are the steps to be followed to fine-tune a GPT model. I've decided to use gpt-4.1-mini
as the base model. It is faster and cheaper because of it's size.
1. Create an Azure Hub project
Create a new Azure Hub resource in Azure AI Foundry. Because Azure AI Foundry resource doesn't have an option to fune-tune models.
2. Start Fine-Tuning
Once the project is opened in the Azure AI Foundry, select Fine-Tuning from ...More. Refer to the screenshot below.
In the newly opened dialogue, click the Fine Tune a Model button and select your base model. In my case, I selected gpt-4.1-mini. This step will deploy a resource with the pre-trained model in a specified location. Detailed step-by-step instructions with screenshots can be found in the documentation mentioned above.
Once the resource is created, you'll be asked to select the fine-tuning method and training data.
Keep the method to default as Supervised and then select the jsonl file we have created in the previous section using "Upload Files" method. We don't have any validation set. We could have split the training-set into training and validation. Just for simplicity we're not doing it now.
The validation set prepared in the previous blog is intentionally excluded from this step. This ensures that the validation dataset remains untouched by the LLM during training, preserving its integrity for independent backtesting and evaluation purposes.
Leave every other value to default and click Submit. Now fine-tuning will start. This will take some hours based on how quickly the descent of loss occurs. Here is the loss function drop graph after 45 minutes of training.
The training got completed after 2 hours. Suppose if you're using bigger models like gpt-4.1, you can expect even longer.
Backtesting
With the training complete and the final loss stabilizing around 0.6, the model shows potential for making accurate predictions. The next step is to test its real-world performance by evaluating if GPT-4.1-mini can generate profitable trades in NIFTY50 index trading on our validation set.
Deploy the fine-tuned model and copy it's API key and Endpoints to the .env
file as below.
1 2 3 4 5 |
|
XXX
with appropriate values.
We use AzureOpenAI library to inference with the newly deployed model. Similar to the training conversation prep we need to prepare conversations using the validation dataset and send to the LLM.
Let me give 1 million rupees initial value and see how much profit it makes in the 247 days of validation dataset.
Make sure that you've installed openai
in your venv
.
1 |
|
This is a big chunk of code. In the notebook you will find it in a single cell. For easier understanding let me give it in multiple chunks. Import all the necessary modules and setup variables to interact with Azure-Foundry. And create a AzureOpenAI client to start the inferencing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Next setup some global variables to control the flow. We've offset
and count
to test the validation set partially. cash_in_hand
represents how much cash is available at any day of the trade. Initially we set it to 1 million INR. Then we've metrics object which counts number profits, losses, time-outs, etc.,. Time out is nothing but time 3.26 PM is reached before making any trade for the day. The variable message
is going to have the system and user prompt before sending them to the LLM.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Load the validation datasets - price movement and open prices itself.
1 2 3 4 5 |
|
Create a list which has time string from 09:15 AM to 02:30 PM. This list will serve as an index filter for filtering the validation dataset.
1 2 3 4 5 6 7 8 9 10 |
|
Start iterating over every day in the validation set. Skip offset
days without processing them.
1 2 3 4 5 |
|
When starting to process a day, first print the date and the cash-in-hand. Then there is a random delay between 3 and 5 seconds. This delay is to avoid hitting the LLM API ratelimiting. Then we get the date from the index and find out the day of the week of that trade day.
1 2 3 4 5 6 7 8 |
|
Filter only the values upto 2.30 PM from validation dataset and construct a chat message to be passed to the LLM.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Call the LLM and get its response.
1 2 3 4 5 6 7 8 9 10 |
|
Calculate the position and target price based on the response from the LLM. If the LLM provided output is more than 1, it is a "Call" otherwise it's a "Put". We don't have corresponding options prices. So, we take it against the index price itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now the actual validation happens. Iterate over the price from 2:31 PM to 3:26 PM. Book profit or loss based on below rules.
- If target price reached, book profit
- If stop-loss reached, book loss
- If neither of the above condition occurred by time reached 3.26 PM, close the position with whatever price at the moment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|
At the end of the for
loop, break it if expected number of trades have happened. At the end print the final cash-in-hand and metrics.
1 2 3 4 5 |
|
Verdict
Overall gpt-4.1-fine-tuned made a loss for me! At end of 246 trades final cash remaining in hand was Rs.97,4114. Approximately Rs.26,000 loss.
1 2 3 4 5 6 7 8 9 10 |
|
Conclusion
The concerning aspect is the high occurrence of time-outs, which happened 55% (136) of the time. This suggests that the 1-sigma target price might be overly ambitious. While theoretically, it should be achievable 66% of the time, the results indicate otherwise. This approach is quite rudimentary, relying on the LLM to deduce all patterns and features from the limited data provided. To improve, we should consider engineering additional features, such as the day of the week, and implementing a custom loss function that accounts for time-outs more effectively.
Despite the lack of profitability, this experiment was a fascinating learning experience. The absence of profit doesn't necessarily mean there's no underlying pattern—it simply means we haven't uncovered it yet. If you have suggestions or ideas for a better approach, feel free to connect with me on LinkedIn. Additionally, if you spot any issues in the code, please raise them on GitHub. Thank you for taking the time to read and engage with this content!
The complete code can be found in this Notebook