File size: 10,938 Bytes
ce4bc73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
334
335
336
337
338
339
340
341
342
"""
Tests for validation utilities.
"""

import pandas as pd
import pytest

from src.folio.exceptions import DataError
from src.folio.validation import (
    clean_numeric_value,
    extract_option_data,
    validate_dataframe,
    validate_option_data,
)


class TestValidateOptionData:
    """Tests for validate_option_data function."""

    def test_valid_option_data(self):
        """Test with valid option data."""
        option_row = pd.Series(
            {
                "Description": "SPY JUN 15 2025 $100 CALL",
                "Quantity": "2",
                "Last Price": "5.00",
            }
        )

        description, quantity, price = validate_option_data(option_row)

        assert description == "SPY JUN 15 2025 $100 CALL"
        assert quantity == 2
        assert price == 5.0

    def test_missing_description(self):
        """Test with missing description."""
        option_row = pd.Series(
            {
                "Description": None,
                "Quantity": "2",
                "Last Price": "5.00",
            }
        )

        with pytest.raises(DataError, match="Missing description"):
            validate_option_data(option_row)

    def test_missing_quantity(self):
        """Test with missing quantity."""
        option_row = pd.Series(
            {
                "Description": "SPY JUN 15 2025 $100 CALL",
                "Quantity": None,
                "Last Price": "5.00",
            }
        )

        with pytest.raises(DataError, match="Missing quantity"):
            validate_option_data(option_row)

    def test_invalid_quantity(self):
        """Test with invalid quantity format."""
        option_row = pd.Series(
            {
                "Description": "SPY JUN 15 2025 $100 CALL",
                "Quantity": "not a number",
                "Last Price": "5.00",
            }
        )

        with pytest.raises(DataError, match="Invalid quantity format"):
            validate_option_data(option_row)

    def test_missing_price(self):
        """Test with missing price."""
        option_row = pd.Series(
            {
                "Description": "SPY JUN 15 2025 $100 CALL",
                "Quantity": "2",
                "Last Price": None,
            }
        )

        with pytest.raises(DataError, match="Missing price"):
            validate_option_data(option_row)

    def test_invalid_price(self):
        """Test with invalid price format."""
        option_row = pd.Series(
            {
                "Description": "SPY JUN 15 2025 $100 CALL",
                "Quantity": "2",
                "Last Price": "not a price",
            }
        )

        with pytest.raises(DataError, match="Invalid price format"):
            validate_option_data(option_row)

    def test_custom_field_names(self):
        """Test with custom field names."""
        option_row = pd.Series(
            {
                "OptionDesc": "SPY JUN 15 2025 $100 CALL",
                "OptionQty": "2",
                "OptionPrice": "5.00",
            }
        )

        description, quantity, price = validate_option_data(
            option_row,
            description_field="OptionDesc",
            quantity_field="OptionQty",
            price_field="OptionPrice",
        )

        assert description == "SPY JUN 15 2025 $100 CALL"
        assert quantity == 2
        assert price == 5.0


class TestExtractOptionData:
    """Tests for extract_option_data function."""

    def test_extract_valid_options(self):
        """Test extracting valid options."""
        option_df = pd.DataFrame(
            [
                {
                    "Description": "SPY JUN 15 2025 $100 CALL",
                    "Symbol": "-SPY",
                    "Quantity": "2",
                    "Last Price": "5.00",
                    "Current Value": "1000.00",
                },
                {
                    "Description": "SPY JUN 15 2025 $110 CALL",
                    "Symbol": "-SPY",
                    "Quantity": "-1",
                    "Last Price": "2.00",
                    "Current Value": "-200.00",
                },
            ]
        )

        options_data = extract_option_data(option_df)

        assert len(options_data) == 2
        assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL"
        assert options_data[0]["quantity"] == 2
        assert options_data[0]["price"] == 5.0
        assert options_data[0]["symbol"] == "-SPY"
        assert "row_index" in options_data[0]

        assert options_data[1]["description"] == "SPY JUN 15 2025 $110 CALL"
        assert options_data[1]["quantity"] == -1
        assert options_data[1]["price"] == 2.0

    def test_extract_with_filter(self):
        """Test extracting options with a filter function."""
        option_df = pd.DataFrame(
            [
                {
                    "Description": "SPY JUN 15 2025 $100 CALL",
                    "Symbol": "-SPY",
                    "Quantity": "2",
                    "Last Price": "5.00",
                },
                {
                    "Description": "AAPL JUN 15 2025 $200 CALL",
                    "Symbol": "-AAPL",
                    "Quantity": "1",
                    "Last Price": "10.00",
                },
            ]
        )

        # Filter for SPY options only
        options_data = extract_option_data(
            option_df,
            filter_func=lambda row: row["Symbol"] == "-SPY",
        )

        assert len(options_data) == 1
        assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL"
        assert options_data[0]["symbol"] == "-SPY"

    def test_extract_with_invalid_options(self):
        """Test extracting options with some invalid data."""
        option_df = pd.DataFrame(
            [
                {
                    "Description": "SPY JUN 15 2025 $100 CALL",
                    "Symbol": "-SPY",
                    "Quantity": "2",
                    "Last Price": "5.00",
                },
                {
                    "Description": None,  # Invalid: missing description
                    "Symbol": "-AAPL",
                    "Quantity": "1",
                    "Last Price": "10.00",
                },
                {
                    "Description": "AAPL JUN 15 2025 $200 CALL",
                    "Symbol": "-AAPL",
                    "Quantity": "not a number",  # Invalid: bad quantity
                    "Last Price": "10.00",
                },
            ]
        )

        options_data = extract_option_data(option_df)

        # Only the first option should be extracted
        assert len(options_data) == 1
        assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL"

    def test_extract_without_row_index(self):
        """Test extracting options without including row index."""
        option_df = pd.DataFrame(
            [
                {
                    "Description": "SPY JUN 15 2025 $100 CALL",
                    "Symbol": "-SPY",
                    "Quantity": "2",
                    "Last Price": "5.00",
                },
            ]
        )

        options_data = extract_option_data(option_df, include_row_index=False)

        assert len(options_data) == 1
        assert "row_index" not in options_data[0]


class TestValidateDataframe:
    """Tests for validate_dataframe function."""

    def test_valid_dataframe(self):
        """Test with a valid DataFrame."""
        df = pd.DataFrame(
            {
                "Symbol": ["SPY", "AAPL"],
                "Quantity": [10, 20],
                "Price": [100.0, 200.0],
            }
        )

        result = validate_dataframe(df, ["Symbol", "Quantity", "Price"])

        # Should return the original DataFrame
        assert result is df

    def test_none_dataframe(self):
        """Test with None DataFrame."""
        with pytest.raises(DataError, match="dataframe is None"):
            validate_dataframe(None, ["Symbol"])

    def test_empty_dataframe(self):
        """Test with empty DataFrame."""
        df = pd.DataFrame()

        with pytest.raises(DataError, match="dataframe is empty"):
            validate_dataframe(df, ["Symbol"])

    def test_missing_columns(self):
        """Test with missing required columns."""
        df = pd.DataFrame(
            {
                "Symbol": ["SPY", "AAPL"],
                "Quantity": [10, 20],
            }
        )

        with pytest.raises(DataError, match="missing required columns: Price"):
            validate_dataframe(df, ["Symbol", "Quantity", "Price"])

    def test_custom_name(self):
        """Test with custom DataFrame name."""
        with pytest.raises(DataError, match="portfolio is None"):
            validate_dataframe(None, ["Symbol"], name="portfolio")


class TestCleanNumericValue:
    """Tests for clean_numeric_value function."""

    def test_valid_numeric(self):
        """Test with valid numeric values."""
        assert clean_numeric_value(10) == 10.0
        assert clean_numeric_value(10.5) == 10.5
        assert clean_numeric_value("10") == 10.0
        assert clean_numeric_value("10.5") == 10.5

    def test_currency_format(self):
        """Test with currency formatted values."""
        assert clean_numeric_value("$10.50") == 10.5
        assert clean_numeric_value("$1,234.56") == 1234.56
        assert clean_numeric_value("($10.50)") == -10.5  # Parentheses for negative

    def test_none_value(self):
        """Test with None value."""
        with pytest.raises(ValueError, match="Value is NaN or None"):
            clean_numeric_value(None)

        # With default
        assert clean_numeric_value(None, default=0) == 0

    def test_nan_value(self):
        """Test with NaN value."""
        with pytest.raises(ValueError, match="Value is NaN or None"):
            clean_numeric_value(float("nan"))

        # With default
        assert clean_numeric_value(float("nan"), default=0) == 0

    def test_invalid_format(self):
        """Test with invalid format."""
        with pytest.raises(ValueError, match="Could not convert"):
            clean_numeric_value("not a number")

        # With default
        assert clean_numeric_value("not a number", default=0) == 0

    def test_zero_constraint(self):
        """Test with zero constraint."""
        with pytest.raises(ValueError, match="Zero value not allowed"):
            clean_numeric_value(0, allow_zero=False)

        # With default
        assert clean_numeric_value(0, allow_zero=False, default=1) == 1

    def test_negative_constraint(self):
        """Test with negative constraint."""
        with pytest.raises(ValueError, match="Negative value not allowed"):
            clean_numeric_value(-10, allow_negative=False)

        # With default
        assert clean_numeric_value(-10, allow_negative=False, default=10) == 10