File size: 12,073 Bytes
9757462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
"""
Synthetic data generator for TradeParse-LoRA / OrderIntent.

Generates clean instruction -> JSON training pairs for fine-tuning a model to
parse natural-language trade instructions into one consistent schema.

The model may output either:
- a successful trade parse
- a structured error when the instruction is missing required information

idempotency_key is intentionally excluded from model output. Generate it in
Python after validation if needed.
"""

import argparse
import json
import random
from pathlib import Path

RNG = random.Random(42)

SYSTEM_PROMPT = (
    "Convert the user trade instruction into JSON only. "
    "If the instruction has enough information, use exactly this success schema: "
    '{"symbol": str, "action_conditions": [{"trigger": "price_below"|"price_above", '
    '"value": number, "action": "buy"|"sell"}], "strategy_type": "intraday"|null}. '
    "If required information is missing or ambiguous, use exactly this error schema: "
    '{"error": "missing_symbol"|"ambiguous_symbol"|"missing_price_condition"|"missing_action"|"unsupported_strategy", '
    '"symbol": str|null, "message": str}. '
    "Rules: if strategy type is not mentioned, set strategy_type to null. "
    "Only intraday is supported as a strategy_type; futures, delivery, and swing should not be copied into strategy_type. "
    "If no numeric price condition is present, return missing_price_condition. "
    "If no symbol/company is present, return missing_symbol. "
    "If no buy/sell style action is present, return missing_action. "
    "Do not include markdown, explanations, or extra keys."
)

SYMBOL_ALIASES = {
    "INFY": ["INFY", "Infy", "Infosys", "INFY.NS"],
    "RELIANCE": ["RELIANCE", "Reliance"],
    "TCS": ["TCS", "Tcs", "Tata Consultancy Services"],
    "HDFC": ["HDFC", "Hdfc", "HDFC Bank"],
    "ICICIBANK": ["ICICIBANK", "Icicibank", "ICICI Bank"],
    "SBIN": ["SBIN", "Sbin", "SBI", "State Bank of India"],
    "WIPRO": ["WIPRO", "Wipro", "Wipro Ltd"],
    "TATASTEEL": ["TATASTEEL", "Tatasteel", "Tata Steel"],
    "BAJFINANCE": ["BAJFINANCE", "Bajfinance", "Bajaj Finance"],
    "ADANIENT": ["ADANIENT", "Adanient", "Adani Enterprises"],
    "MARUTI": ["MARUTI", "Maruti"],
    "SUNPHARMA": ["SUNPHARMA", "Sunpharma", "Sun Pharma"],
    "HCLTECH": ["HCLTECH", "Hcltech", "HCL Tech"],
    "AXISBANK": ["AXISBANK", "Axisbank", "Axis Bank"],
    "ONGC": ["ONGC", "Ongc"],
}

DOUBLE_TEMPLATES = [
    "Buy {name} shares if it drops below {below} and sell if it goes above {above}{strategy_suffix}",
    "Buy {name} when price is below {below}, sell above {above}{strategy_suffix}",
    "Buy {name} if price falls under {below}, sell above {above}{strategy_suffix}",
    "If {name} goes below {below} buy it, and if it crosses {above} sell it{strategy_suffix}",
    "{name} - buy under {below}, book profit above {above}{strategy_suffix}",
    "Get me into {name} below {below}, exit above {above}{strategy_sentence}",
    "Place buy order for {name} below {below} and sell order above {above}{strategy_suffix}",
]

BUY_ONLY_TEMPLATES = [
    "Buy {name} if it drops below {below}{strategy_suffix}",
    "Buy {name} when price is below {below}{strategy_suffix}",
    "Get {name} below {below}{strategy_suffix}",
    "buy {name} under {below}{strategy_suffix}",
    "Accumulate {name} if it falls below {below}{strategy_suffix}",
]

SELL_ONLY_TEMPLATES = [
    "Sell {name} if it goes above {above}{strategy_suffix}",
    "Sell {name} above {above}{strategy_suffix}",
    "Exit {name} once it crosses {above}{strategy_suffix}",
    "Book profit in {name} above {above}{strategy_suffix}",
]

MISSING_PRICE_TEMPLATES = [
    "Buy {name} when it is cheap, sell when it is expensive",
    "Pick up {name} at a good level and exit higher",
    "Trade {name} based on support and resistance",
    "Buy {name} when it looks attractive",
]

MISSING_SYMBOL_TEMPLATES = [
    "buy this stock below {below}, sell above {above}{strategy_suffix}",
    "enter below {below} and exit above {above}",
    "buy it under {below}, sell it over {above}",
]

MISSING_ACTION_TEMPLATES = [
    "{name} at {price}{strategy_suffix}",
    "Watch {name} near {price}",
    "Alert me about {name} around {price}",
]

AMBIGUOUS_SYMBOL_TEMPLATES = [
    "Buy Tata below {below}",
    "Sell Tata above {above}",
    "Tata below {below} buy, above {above} sell",
]


def choose_symbol():
    symbol = RNG.choice(list(SYMBOL_ALIASES.keys()))
    name = RNG.choice(SYMBOL_ALIASES[symbol])
    return symbol, name


def choose_strategy_suffix(include_strategy: bool):
    if not include_strategy:
        return "", ""
    return ", intraday", ". intraday trade."


def output(symbol, conditions, strategy_type):
    return {
        "symbol": symbol,
        "action_conditions": conditions,
        "strategy_type": strategy_type,
    }


def error(error_type, symbol, message):
    return {
        "error": error_type,
        "symbol": symbol,
        "message": message,
    }


def gen_double_condition_example():
    symbol, name = choose_symbol()
    below = RNG.randint(100, 4000)
    above = below + RNG.randint(50, 500)
    include_strategy = RNG.random() < 0.6
    strategy_suffix, strategy_sentence = choose_strategy_suffix(include_strategy)
    template = RNG.choice(DOUBLE_TEMPLATES)

    instruction = template.format(
        name=name,
        below=below,
        above=above,
        strategy_suffix=strategy_suffix,
        strategy_sentence=strategy_sentence,
    ).strip()

    return instruction, output(
        symbol,
        [
            {"trigger": "price_below", "value": below, "action": "buy"},
            {"trigger": "price_above", "value": above, "action": "sell"},
        ],
        "intraday" if include_strategy else None,
    )


def gen_buy_only_example():
    symbol, name = choose_symbol()
    below = RNG.randint(100, 4000)
    include_strategy = RNG.random() < 0.5
    strategy_suffix, _ = choose_strategy_suffix(include_strategy)
    template = RNG.choice(BUY_ONLY_TEMPLATES)
    instruction = template.format(name=name, below=below, strategy_suffix=strategy_suffix).strip()

    return instruction, output(
        symbol,
        [{"trigger": "price_below", "value": below, "action": "buy"}],
        "intraday" if include_strategy else None,
    )


def gen_sell_only_example():
    symbol, name = choose_symbol()
    above = RNG.randint(100, 4000)
    include_strategy = RNG.random() < 0.5
    strategy_suffix, _ = choose_strategy_suffix(include_strategy)
    template = RNG.choice(SELL_ONLY_TEMPLATES)
    instruction = template.format(name=name, above=above, strategy_suffix=strategy_suffix).strip()

    return instruction, output(
        symbol,
        [{"trigger": "price_above", "value": above, "action": "sell"}],
        "intraday" if include_strategy else None,
    )


def gen_error_example():
    kind = RNG.choice(["missing_price", "missing_symbol", "missing_action", "ambiguous_symbol"])
    below = RNG.randint(100, 4000)
    above = below + RNG.randint(50, 500)
    strategy_suffix, _ = choose_strategy_suffix(RNG.random() < 0.3)

    if kind == "missing_price":
        symbol, name = choose_symbol()
        instruction = RNG.choice(MISSING_PRICE_TEMPLATES).format(name=name)
        return instruction, error("missing_price_condition", symbol, "No numeric price condition found.")

    if kind == "missing_symbol":
        instruction = RNG.choice(MISSING_SYMBOL_TEMPLATES).format(
            below=below,
            above=above,
            strategy_suffix=strategy_suffix,
        )
        return instruction, error("missing_symbol", None, "No symbol or company name found.")

    if kind == "missing_action":
        symbol, name = choose_symbol()
        instruction = RNG.choice(MISSING_ACTION_TEMPLATES).format(
            name=name,
            price=below,
            strategy_suffix=strategy_suffix,
        )
        return instruction, error("missing_action", symbol, "No buy or sell action found.")

    instruction = RNG.choice(AMBIGUOUS_SYMBOL_TEMPLATES).format(below=below, above=above)
    return instruction, error("ambiguous_symbol", "TATA", "Symbol is ambiguous. Specify the exact Tata company.")


def fixed_examples():
    """High-value examples that exactly match likely manual tests."""
    return [
        (
            "Buy Infy when price below 1500 and sell above 1700, intraday",
            output("INFY", [
                {"trigger": "price_below", "value": 1500, "action": "buy"},
                {"trigger": "price_above", "value": 1700, "action": "sell"},
            ], "intraday"),
        ),
        (
            "Buy INFY if it drops below 1500, sell above 1700, intraday",
            output("INFY", [
                {"trigger": "price_below", "value": 1500, "action": "buy"},
                {"trigger": "price_above", "value": 1700, "action": "sell"},
            ], "intraday"),
        ),
        (
            "Buy Infosys if it drops below 1500, sell above 1700, intraday",
            output("INFY", [
                {"trigger": "price_below", "value": 1500, "action": "buy"},
                {"trigger": "price_above", "value": 1700, "action": "sell"},
            ], "intraday"),
        ),
        (
            "Buy Infy when price below 1500",
            output("INFY", [
                {"trigger": "price_below", "value": 1500, "action": "buy"},
            ], None),
        ),
        (
            "Buy ONGC when it's cheap, sell when it's expensive",
            error("missing_price_condition", "ONGC", "No numeric price condition found."),
        ),
        (
            "buy this stock below 500, sell above 600, futures",
            error("missing_symbol", None, "No symbol or company name found."),
        ),
        (
            "Do something with Infosys at 1500",
            error("missing_action", "INFY", "No buy or sell action found."),
        ),
        (
            "Buy Tata below 500",
            error("ambiguous_symbol", "TATA", "Symbol is ambiguous. Specify the exact Tata company."),
        ),
    ]


def to_chat_format(instruction, output_json):
    return {
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": instruction},
            {"role": "assistant", "content": json.dumps(output_json, ensure_ascii=False)},
        ]
    }


def generate_dataset(n_total=1600):
    examples = fixed_examples()

    while len(examples) < n_total:
        r = RNG.random()
        if r < 0.52:
            examples.append(gen_double_condition_example())
        elif r < 0.68:
            examples.append(gen_buy_only_example())
        elif r < 0.84:
            examples.append(gen_sell_only_example())
        else:
            examples.append(gen_error_example())

    RNG.shuffle(examples)
    return [to_chat_format(instruction, parsed) for instruction, parsed in examples]


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--n", type=int, default=1600, help="Total examples to generate")
    parser.add_argument("--train-split", type=float, default=0.85)
    parser.add_argument("--out-dir", type=str, default="data_v2")
    args = parser.parse_args()

    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    data = generate_dataset(args.n)
    split_idx = int(len(data) * args.train_split)
    train_data = data[:split_idx]
    valid_data = data[split_idx:]

    train_path = out_dir / "train.jsonl"
    valid_path = out_dir / "valid.jsonl"

    with train_path.open("w", encoding="utf-8") as f:
        for row in train_data:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")

    with valid_path.open("w", encoding="utf-8") as f:
        for row in valid_data:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")

    print(f"Generated {len(train_data)} training examples -> {train_path}")
    print(f"Generated {len(valid_data)} validation examples -> {valid_path}")
    print(json.dumps(train_data[0], indent=2))


if __name__ == "__main__":
    main()