quantlib-st 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantlib_st/__init__.py +0 -0
- quantlib_st/cli/__init__.py +3 -0
- quantlib_st/cli/__main__.py +5 -0
- quantlib_st/cli/corr_cmd.py +138 -0
- quantlib_st/cli/costs_cmd.py +129 -0
- quantlib_st/cli/main.py +45 -0
- quantlib_st/config/__init__.py +0 -0
- quantlib_st/config/configdata.py +329 -0
- quantlib_st/config/defaults.py +36 -0
- quantlib_st/config/fill_config_dict_with_defaults.py +25 -0
- quantlib_st/config/instruments.py +103 -0
- quantlib_st/config/private_config.py +29 -0
- quantlib_st/config/private_directory.py +15 -0
- quantlib_st/core/__init__.py +0 -0
- quantlib_st/core/constants.py +21 -0
- quantlib_st/core/dateutils.py +201 -0
- quantlib_st/core/exceptions.py +39 -0
- quantlib_st/core/fileutils.py +195 -0
- quantlib_st/core/genutils.py +391 -0
- quantlib_st/core/maths.py +117 -0
- quantlib_st/core/objects.py +118 -0
- quantlib_st/core/pandas/__init__.py +0 -0
- quantlib_st/core/pandas/find_data.py +81 -0
- quantlib_st/core/pandas/frequency.py +157 -0
- quantlib_st/core/pandas/full_merge_with_replacement.py +200 -0
- quantlib_st/core/pandas/merge_data_keeping_past_data.py +318 -0
- quantlib_st/core/pandas/merge_data_with_label_column.py +420 -0
- quantlib_st/core/pandas/pdutils.py +392 -0
- quantlib_st/core/pandas/strategy_functions.py +152 -0
- quantlib_st/core/text.py +123 -0
- quantlib_st/correlation/__init__.py +7 -0
- quantlib_st/correlation/correlation_over_time.py +275 -0
- quantlib_st/correlation/exponential_correlation.py +115 -0
- quantlib_st/correlation/fitting_dates.py +120 -0
- quantlib_st/costs/__init__.py +11 -0
- quantlib_st/costs/calculator.py +119 -0
- quantlib_st/costs/config.py +24 -0
- quantlib_st/costs/data_source.py +46 -0
- quantlib_st/estimators/__init__.py +0 -0
- quantlib_st/estimators/forecast_scalar.py +62 -0
- quantlib_st/estimators/turnover.py +29 -0
- quantlib_st/estimators/vol.py +199 -0
- quantlib_st/execution/__init__.py +0 -0
- quantlib_st/execution/orders/__init__.py +0 -0
- quantlib_st/execution/orders/base_orders.py +507 -0
- quantlib_st/execution/orders/named_order_objects.py +16 -0
- quantlib_st/execution/trade_qty.py +253 -0
- quantlib_st/init/__init__.py +0 -0
- quantlib_st/init/futures/__init__.py +0 -0
- quantlib_st/init/futures/build_multiple_prices_from_raw_data.py +337 -0
- quantlib_st/logging/__init__.py +0 -0
- quantlib_st/logging/adaptor.py +102 -0
- quantlib_st/logging/logger.py +86 -0
- quantlib_st/objects/__init__.py +4 -0
- quantlib_st/objects/adjusted_prices.py +194 -0
- quantlib_st/objects/carry_data.py +85 -0
- quantlib_st/objects/contract_dates_and_expiries.py +622 -0
- quantlib_st/objects/contracts.py +388 -0
- quantlib_st/objects/dict_of_futures_per_contract_prices.py +151 -0
- quantlib_st/objects/dict_of_named_futures_per_contract_prices.py +337 -0
- quantlib_st/objects/fills.py +124 -0
- quantlib_st/objects/instruments.py +392 -0
- quantlib_st/objects/multiple_prices.py +219 -0
- quantlib_st/objects/production/__init__.py +0 -0
- quantlib_st/objects/production/tradeable_object.py +245 -0
- quantlib_st/objects/rolls.py +451 -0
- quantlib_st/objects/spot_fx_prices.py +110 -0
- quantlib_st/sysdata/__init__.py +3 -0
- quantlib_st/sysdata/base_data.py +76 -0
- quantlib_st/sysdata/csv/__init__.py +15 -0
- quantlib_st/sysdata/csv/csv_adjusted_prices.py +84 -0
- quantlib_st/sysdata/csv/csv_instrument_data.py +138 -0
- quantlib_st/sysdata/csv/csv_multiple_prices.py +100 -0
- quantlib_st/sysdata/csv/csv_roll_parameters.py +120 -0
- quantlib_st/sysdata/csv/csv_spot_fx.py +126 -0
- quantlib_st/sysdata/csv/csv_spread_costs.py +71 -0
- quantlib_st/sysdata/data_blob.py +415 -0
- quantlib_st/sysdata/futures/__init__.py +13 -0
- quantlib_st/sysdata/futures/adjusted_prices.py +115 -0
- quantlib_st/sysdata/futures/instruments.py +157 -0
- quantlib_st/sysdata/futures/multiple_prices.py +126 -0
- quantlib_st/sysdata/futures/rolls_parameters.py +111 -0
- quantlib_st/sysdata/futures/spread_costs.py +44 -0
- quantlib_st/sysdata/fx/__init__.py +3 -0
- quantlib_st/sysdata/fx/spotfx.py +242 -0
- quantlib_st/sysdata/production_config.py +8 -0
- quantlib_st/sysdata/sim/__init__.py +15 -0
- quantlib_st/sysdata/sim/base_data.py +3 -0
- quantlib_st/sysdata/sim/csv_futures_sim_test_data.py +116 -0
- quantlib_st/sysdata/sim/futures_sim_data.py +266 -0
- quantlib_st/sysdata/sim/futures_sim_data_with_data_blob.py +130 -0
- quantlib_st/sysdata/sim/sim_data.py +325 -0
- quantlib_st/systems/__init__.py +0 -0
- quantlib_st/systems/accounts/__init__.py +0 -0
- quantlib_st/systems/accounts/account_buffering_subsystem.py +208 -0
- quantlib_st/systems/accounts/account_buffering_system.py +116 -0
- quantlib_st/systems/accounts/account_costs.py +366 -0
- quantlib_st/systems/accounts/account_forecast.py +330 -0
- quantlib_st/systems/accounts/account_inputs.py +317 -0
- quantlib_st/systems/accounts/account_instruments.py +225 -0
- quantlib_st/systems/accounts/account_portfolio.py +71 -0
- quantlib_st/systems/accounts/account_subsystem.py +206 -0
- quantlib_st/systems/accounts/account_trading_rules.py +216 -0
- quantlib_st/systems/accounts/account_with_multiplier.py +163 -0
- quantlib_st/systems/accounts/accounts_stage.py +12 -0
- quantlib_st/systems/accounts/curves/__init__.py +13 -0
- quantlib_st/systems/accounts/curves/account_curve.py +416 -0
- quantlib_st/systems/accounts/curves/account_curve_group.py +169 -0
- quantlib_st/systems/accounts/curves/dict_of_account_curves.py +41 -0
- quantlib_st/systems/accounts/curves/nested_account_curve_group.py +125 -0
- quantlib_st/systems/accounts/curves/stats_dict.py +254 -0
- quantlib_st/systems/accounts/pandl_calculators/__init__.py +25 -0
- quantlib_st/systems/accounts/pandl_calculators/pandl_SR_cost.py +132 -0
- quantlib_st/systems/accounts/pandl_calculators/pandl_calculation.py +244 -0
- quantlib_st/systems/accounts/pandl_calculators/pandl_calculation_dict.py +106 -0
- quantlib_st/systems/accounts/pandl_calculators/pandl_cash_costs.py +262 -0
- quantlib_st/systems/accounts/pandl_calculators/pandl_generic_costs.py +108 -0
- quantlib_st/systems/accounts/pandl_calculators/pandl_using_fills.py +173 -0
- quantlib_st/systems/accounts/tests/test_accounts.py +26 -0
- quantlib_st/systems/basesystem.py +442 -0
- quantlib_st/systems/forecast_scale_cap.py +523 -0
- quantlib_st/systems/forecasting.py +249 -0
- quantlib_st/systems/provided/__init__.py +0 -0
- quantlib_st/systems/provided/config/__init__.py +0 -0
- quantlib_st/systems/provided/futures_chapter15/basesystem.py +63 -0
- quantlib_st/systems/provided/rules/__init__.py +0 -0
- quantlib_st/systems/provided/rules/breakout.py +38 -0
- quantlib_st/systems/provided/rules/carry.py +41 -0
- quantlib_st/systems/provided/rules/ewmac.py +180 -0
- quantlib_st/systems/rawdata.py +745 -0
- quantlib_st/systems/stage.py +53 -0
- quantlib_st/systems/system_cache.py +793 -0
- quantlib_st/systems/tests/__init__.py +0 -0
- quantlib_st/systems/tests/test_forecast_scale_cap.py +88 -0
- quantlib_st/systems/tests/test_forecasting.py +38 -0
- quantlib_st/systems/tools/__init__.py +0 -0
- quantlib_st/systems/tools/autogroup.py +275 -0
- quantlib_st/systems/trading_rules.py +554 -0
- quantlib_st-1.8.0.dist-info/METADATA +94 -0
- quantlib_st-1.8.0.dist-info/RECORD +159 -0
- quantlib_st-1.8.0.dist-info/WHEEL +5 -0
- quantlib_st-1.8.0.dist-info/entry_points.txt +2 -0
- quantlib_st-1.8.0.dist-info/licenses/LICENSE +21 -0
- quantlib_st-1.8.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/core/__init__.py +0 -0
- tests/core/test_fileutils.py +90 -0
- tests/correlation/__init__.py +0 -0
- tests/correlation/test_correlation.py +75 -0
- tests/correlation/test_correlationlist_as.py +43 -0
- tests/correlation/test_fitting_dates.py +54 -0
- tests/correlation/test_jsonable_to_long.py +48 -0
- tests/estimators/test_forecast_scalar.py +55 -0
- tests/estimators/test_vol.py +40 -0
- tests/systems/__init__.py +0 -0
- tests/systems/provided/__init__.py +0 -0
- tests/systems/provided/rules/__init__.py +0 -0
- tests/systems/provided/rules/test_ewmac.py +48 -0
- tests/systems/test_account_forecast.py +93 -0
quantlib_st/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from io import StringIO
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_corr_subcommand(subparsers: argparse._SubParsersAction) -> None:
|
|
10
|
+
parser = subparsers.add_parser(
|
|
11
|
+
"corr",
|
|
12
|
+
help="Compute correlations over time from CSV piped on stdin (outputs JSON).",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--frequency",
|
|
17
|
+
default="D",
|
|
18
|
+
help="Resample frequency before correlation (default: D). Use W if you want weekly.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--date-method",
|
|
22
|
+
default="in_sample",
|
|
23
|
+
choices=["expanding", "rolling", "in_sample"],
|
|
24
|
+
help="How to choose the fit window over time (default: in_sample)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--rollyears",
|
|
28
|
+
type=int,
|
|
29
|
+
default=20,
|
|
30
|
+
help="Rolling years (used only if --date-method rolling; default: 20)",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--interval-frequency",
|
|
34
|
+
default="12M",
|
|
35
|
+
help="How often to emit a new correlation matrix (default: 12M)",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--using-exponent",
|
|
40
|
+
action=argparse.BooleanOptionalAction,
|
|
41
|
+
default=True,
|
|
42
|
+
help="Use EWMA correlation (default: true)",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--ew-lookback",
|
|
46
|
+
type=int,
|
|
47
|
+
default=250,
|
|
48
|
+
help="EWMA span/lookback (default: 250)",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--min-periods",
|
|
52
|
+
type=int,
|
|
53
|
+
default=20,
|
|
54
|
+
help="Minimum observations before correlations appear (default: 20)",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--floor-at-zero",
|
|
59
|
+
action=argparse.BooleanOptionalAction,
|
|
60
|
+
default=True,
|
|
61
|
+
help="Floor negative correlations at 0 (default: true)",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--clip",
|
|
65
|
+
type=float,
|
|
66
|
+
default=None,
|
|
67
|
+
help="Optional absolute clip value for correlations (e.g. 0.9)",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--shrinkage",
|
|
71
|
+
type=float,
|
|
72
|
+
default=0.0,
|
|
73
|
+
help="Optional shrinkage-to-average in [0,1] (default: 0)",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--forward-fill-price-index",
|
|
78
|
+
action=argparse.BooleanOptionalAction,
|
|
79
|
+
default=True,
|
|
80
|
+
help="Forward fill the synthetic price index before resampling (default: true)",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--is-price-series",
|
|
84
|
+
action=argparse.BooleanOptionalAction,
|
|
85
|
+
default=False,
|
|
86
|
+
help="If true, treat input as prices. If false (default), treat as returns.",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--index-col",
|
|
91
|
+
type=int,
|
|
92
|
+
default=0,
|
|
93
|
+
help="Which CSV column is the datetime index (default: 0)",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
parser.set_defaults(_handler=run_corr)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_corr(args: argparse.Namespace) -> int:
|
|
100
|
+
import pandas as pd
|
|
101
|
+
from quantlib_st.correlation.correlation_over_time import (
|
|
102
|
+
correlation_over_time,
|
|
103
|
+
correlation_list_to_jsonable,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
csv_text = sys.stdin.read()
|
|
107
|
+
if not csv_text.strip():
|
|
108
|
+
print(json.dumps({"error": "no input on stdin"}), file=sys.stderr)
|
|
109
|
+
return 2
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
df = pd.read_csv(StringIO(csv_text), index_col=args.index_col, parse_dates=True)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(json.dumps({"error": f"failed to parse CSV: {e}"}), file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
|
|
117
|
+
df = df.sort_index()
|
|
118
|
+
|
|
119
|
+
corr_list = correlation_over_time(
|
|
120
|
+
df,
|
|
121
|
+
frequency=args.frequency,
|
|
122
|
+
forward_fill_price_index=args.forward_fill_price_index,
|
|
123
|
+
is_price_series=args.is_price_series,
|
|
124
|
+
date_method=args.date_method,
|
|
125
|
+
rollyears=args.rollyears,
|
|
126
|
+
interval_frequency=args.interval_frequency,
|
|
127
|
+
using_exponent=args.using_exponent,
|
|
128
|
+
ew_lookback=args.ew_lookback,
|
|
129
|
+
min_periods=args.min_periods,
|
|
130
|
+
floor_at_zero=args.floor_at_zero,
|
|
131
|
+
clip=args.clip,
|
|
132
|
+
shrinkage=args.shrinkage,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
out = correlation_list_to_jsonable(corr_list)
|
|
136
|
+
sys.stdout.write(json.dumps(out))
|
|
137
|
+
sys.stdout.write("\n")
|
|
138
|
+
return 0
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from io import StringIO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_costs_subcommand(subparsers: argparse._SubParsersAction) -> None:
|
|
11
|
+
parser = subparsers.add_parser(
|
|
12
|
+
"costs",
|
|
13
|
+
help="Calculate SR costs for an instrument from price CSV piped on stdin or provided via file.",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--instrument",
|
|
18
|
+
required=True,
|
|
19
|
+
help="Instrument code (e.g., ES, GC).",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--config",
|
|
23
|
+
help="Path to JSON file containing instrument cost configuration.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--use-ibkr",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="Use IBKR API for cost data (currently a stub).",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--vol",
|
|
32
|
+
type=float,
|
|
33
|
+
help="Override annualized volatility (as a decimal, e.g., 0.15 for 15%%).",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--price",
|
|
37
|
+
type=float,
|
|
38
|
+
help="Override current price (otherwise uses the last price in the CSV).",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
parser.set_defaults(_handler=handle_costs)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def handle_costs(args: argparse.Namespace) -> int:
|
|
45
|
+
import pandas as pd
|
|
46
|
+
from quantlib_st.costs.data_source import (
|
|
47
|
+
ConfigFileCostDataSource,
|
|
48
|
+
IBKRCostDataSource,
|
|
49
|
+
)
|
|
50
|
+
from quantlib_st.costs.calculator import (
|
|
51
|
+
calculate_sr_cost,
|
|
52
|
+
calculate_annualized_volatility,
|
|
53
|
+
calculate_recent_average_price,
|
|
54
|
+
calculate_cost_percentage_terms,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 1. Get Cost Config
|
|
58
|
+
if args.use_ibkr:
|
|
59
|
+
data_source = IBKRCostDataSource()
|
|
60
|
+
elif args.config:
|
|
61
|
+
data_source = ConfigFileCostDataSource(args.config)
|
|
62
|
+
else:
|
|
63
|
+
print("Error: Must provide either --config or --use-ibkr", file=sys.stderr)
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
cost_config = data_source.get_cost_config(args.instrument)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"Error fetching cost config: {e}", file=sys.stderr)
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
# 2. Get Price Data
|
|
73
|
+
if not sys.stdin.isatty():
|
|
74
|
+
# Read from stdin
|
|
75
|
+
input_data = sys.stdin.read()
|
|
76
|
+
df = pd.read_csv(StringIO(input_data), index_col=0, parse_dates=True)
|
|
77
|
+
else:
|
|
78
|
+
# If no stdin, we need at least --price and --vol if we want to calculate anything
|
|
79
|
+
df = pd.DataFrame()
|
|
80
|
+
|
|
81
|
+
if df.empty and (args.price is None or args.vol is None):
|
|
82
|
+
print(
|
|
83
|
+
"Error: Must pipe price CSV to stdin or provide both --price and --vol overrides.",
|
|
84
|
+
file=sys.stderr,
|
|
85
|
+
)
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
# 3. Determine Price and Volatility
|
|
89
|
+
if args.price is not None:
|
|
90
|
+
average_price = float(args.price)
|
|
91
|
+
else:
|
|
92
|
+
# Use average price over the last year (256 days)
|
|
93
|
+
average_price = calculate_recent_average_price(df.iloc[:, 0])
|
|
94
|
+
|
|
95
|
+
if args.vol is not None:
|
|
96
|
+
# If user provides --vol, we assume it's annualized volatility in price units
|
|
97
|
+
ann_stdev_price_units = float(args.vol)
|
|
98
|
+
else:
|
|
99
|
+
# Calculate annualized volatility in price units (average over last year)
|
|
100
|
+
ann_stdev_price_units = float(calculate_annualized_volatility(df.iloc[:, 0]))
|
|
101
|
+
|
|
102
|
+
# 4. Calculate Costs
|
|
103
|
+
sr_cost = float(
|
|
104
|
+
calculate_sr_cost(
|
|
105
|
+
cost_config,
|
|
106
|
+
price=average_price,
|
|
107
|
+
ann_stdev_price_units=ann_stdev_price_units,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
pct_cost = float(
|
|
112
|
+
calculate_cost_percentage_terms(
|
|
113
|
+
cost_config,
|
|
114
|
+
blocks_traded=1.0,
|
|
115
|
+
price=average_price,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# 5. Output Results
|
|
120
|
+
result = {
|
|
121
|
+
"instrument": args.instrument,
|
|
122
|
+
"average_price": round(average_price, 4),
|
|
123
|
+
"ann_stdev_price_units": round(ann_stdev_price_units, 4),
|
|
124
|
+
"sr_cost": round(sr_cost, 5),
|
|
125
|
+
"percentage_cost": round(pct_cost, 6),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
print(json.dumps(result, indent=2))
|
|
129
|
+
return 0
|
quantlib_st/cli/main.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
|
|
6
|
+
from quantlib_st.cli.corr_cmd import add_corr_subcommand
|
|
7
|
+
from quantlib_st.cli.costs_cmd import add_costs_subcommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_version() -> str:
|
|
11
|
+
"""Get the version of quantlib-st package."""
|
|
12
|
+
try:
|
|
13
|
+
return importlib.metadata.version("quantlib-st")
|
|
14
|
+
except importlib.metadata.PackageNotFoundError:
|
|
15
|
+
return "unknown"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: list[str] | None = None) -> int:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="quantlib",
|
|
21
|
+
description="quantlib CLI (corr is the first subcommand; more will be added).",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-v",
|
|
26
|
+
"--version",
|
|
27
|
+
action="version",
|
|
28
|
+
version=f"%(prog)s {get_version()}",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
|
32
|
+
|
|
33
|
+
add_corr_subcommand(subparsers)
|
|
34
|
+
add_costs_subcommand(subparsers)
|
|
35
|
+
|
|
36
|
+
args = parser.parse_args(argv)
|
|
37
|
+
|
|
38
|
+
# Dispatch
|
|
39
|
+
if args.subcommand == "corr":
|
|
40
|
+
return args._handler(args)
|
|
41
|
+
elif args.subcommand == "costs":
|
|
42
|
+
return args._handler(args)
|
|
43
|
+
|
|
44
|
+
parser.error(f"Unknown subcommand: {args.subcommand}")
|
|
45
|
+
return 2
|
|
File without changes
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration is used to control the behaviour of a system
|
|
3
|
+
|
|
4
|
+
Config can be passed as a dict, a filename from which a YAML spec is read in
|
|
5
|
+
and then parsed
|
|
6
|
+
|
|
7
|
+
There are no set elements for configurations, although typically they will
|
|
8
|
+
contain:
|
|
9
|
+
|
|
10
|
+
parameters - a dict of values which override those in system.defaults
|
|
11
|
+
trading_rules - a specification of the trading rules for a system
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, Union, Any
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from quantlib_st.core.exceptions import missingData
|
|
22
|
+
from quantlib_st.core.fileutils import resolve_path_and_filename_for_package
|
|
23
|
+
from quantlib_st.config.defaults import get_system_defaults_dict
|
|
24
|
+
from quantlib_st.config.fill_config_dict_with_defaults import (
|
|
25
|
+
fill_config_dict_with_defaults,
|
|
26
|
+
)
|
|
27
|
+
from quantlib_st.config.private_config import (
|
|
28
|
+
get_private_config_as_dict,
|
|
29
|
+
PRIVATE_CONFIG_FILE,
|
|
30
|
+
)
|
|
31
|
+
from quantlib_st.config.private_directory import (
|
|
32
|
+
get_full_path_for_private_config,
|
|
33
|
+
PRIVATE_CONFIG_DIR_ENV_VAR,
|
|
34
|
+
)
|
|
35
|
+
from quantlib_st.logging.logger import *
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
RESERVED_NAMES = [
|
|
39
|
+
"log",
|
|
40
|
+
"_elements",
|
|
41
|
+
"elements",
|
|
42
|
+
"_default_filename",
|
|
43
|
+
"_private_filename",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Config(object):
|
|
48
|
+
# Common configuration attributes (type hints for IDE)
|
|
49
|
+
trading_rules: Any
|
|
50
|
+
forecast_scalar_estimate: Any
|
|
51
|
+
forecast_scalar_fixed: Any
|
|
52
|
+
instruments: Any
|
|
53
|
+
parameters: Any
|
|
54
|
+
base_currency: Any
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
config_object: Optional[Union[str, dict, list]] = None,
|
|
59
|
+
default_filename=None,
|
|
60
|
+
private_filename=None,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Config objects control the behaviour of systems
|
|
64
|
+
|
|
65
|
+
:param config_object: Either:
|
|
66
|
+
a string (which points to a YAML filename)
|
|
67
|
+
or a dict (which may nest many things)
|
|
68
|
+
or a list of strings or dicts or configs (build config from
|
|
69
|
+
multiple elements, latter elements will overwrite
|
|
70
|
+
earlier ones)
|
|
71
|
+
|
|
72
|
+
:type config_object: str or dict
|
|
73
|
+
|
|
74
|
+
:returns: new Config object
|
|
75
|
+
|
|
76
|
+
>>> Config(dict(parameters=dict(p1=3, p2=4.6), another_thing=[]))
|
|
77
|
+
Config with elements: another_thing, parameters
|
|
78
|
+
|
|
79
|
+
>>> Config("systems.provided.example.exampleconfig.yaml")
|
|
80
|
+
Config with elements: base_currency, ... trading_rules
|
|
81
|
+
|
|
82
|
+
>>> Config(["systems.provided.example.exampleconfig.yaml", dict(parameters=dict(p1=3, p2=4.6), another_thing=[])])
|
|
83
|
+
Config with elements: another_thing, ... parameters, ...trading_rules
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
# this will normally be overridden by the base system
|
|
88
|
+
self.log = get_logger(
|
|
89
|
+
"config", {TYPE_LOG_LABEL: "config", STAGE_LOG_LABEL: "config"}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self._default_filename = default_filename
|
|
93
|
+
self._private_filename = private_filename
|
|
94
|
+
|
|
95
|
+
if config_object is None:
|
|
96
|
+
config_object = dict()
|
|
97
|
+
|
|
98
|
+
self._init_config(config_object)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def elements(self) -> list:
|
|
102
|
+
elements = getattr(self, "_elements", [])
|
|
103
|
+
|
|
104
|
+
return elements
|
|
105
|
+
|
|
106
|
+
def add_elements(self, new_elements: list):
|
|
107
|
+
_ = [self.add_single_element(element_name) for element_name in new_elements]
|
|
108
|
+
|
|
109
|
+
def remove_element(self, element: str):
|
|
110
|
+
current_elements = self.elements
|
|
111
|
+
current_elements.remove(element)
|
|
112
|
+
self._elements = current_elements
|
|
113
|
+
|
|
114
|
+
def add_single_element(self, element_name):
|
|
115
|
+
if element_name not in RESERVED_NAMES:
|
|
116
|
+
elements = self.elements
|
|
117
|
+
if element_name not in elements:
|
|
118
|
+
elements.append(element_name)
|
|
119
|
+
self._elements = elements
|
|
120
|
+
|
|
121
|
+
def __getattr__(self, name: str) -> Any:
|
|
122
|
+
# This allows Pylance to know that Config can have dynamic attributes
|
|
123
|
+
# and prevents "unknown attribute" errors.
|
|
124
|
+
# We only get here if the attribute isn't already defined in __dict__
|
|
125
|
+
raise AttributeError(
|
|
126
|
+
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def get_element(self, element_name):
|
|
130
|
+
try:
|
|
131
|
+
result = getattr(self, element_name)
|
|
132
|
+
except AttributeError:
|
|
133
|
+
raise missingData("Missing config element %s" % element_name)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
def get_element_or_default(self, element_name, default):
|
|
137
|
+
result = getattr(self, element_name, default)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
def get_element_or_arg_not_supplied(self, element_name):
|
|
141
|
+
return self.get_element_or_default(element_name, None)
|
|
142
|
+
|
|
143
|
+
def __repr__(self):
|
|
144
|
+
elements = self.elements
|
|
145
|
+
elements.sort()
|
|
146
|
+
return "Config with elements: %s" % ", ".join(self.elements)
|
|
147
|
+
|
|
148
|
+
def _init_config(self, config_object):
|
|
149
|
+
if isinstance(config_object, list):
|
|
150
|
+
# multiple configs, already a list
|
|
151
|
+
config_list = config_object
|
|
152
|
+
else:
|
|
153
|
+
config_list = [config_object]
|
|
154
|
+
|
|
155
|
+
self._create_config_from_list(config_list)
|
|
156
|
+
|
|
157
|
+
def _create_config_from_list(self, config_object):
|
|
158
|
+
for config_item in config_object:
|
|
159
|
+
self._create_config_from_item(config_item)
|
|
160
|
+
|
|
161
|
+
def _create_config_from_item(self, config_item):
|
|
162
|
+
if isinstance(config_item, dict):
|
|
163
|
+
# its a dict
|
|
164
|
+
self._create_config_from_dict(config_item)
|
|
165
|
+
|
|
166
|
+
elif isinstance(config_item, str) or isinstance(config_item, Path):
|
|
167
|
+
# must be a file YAML'able, from which we load the
|
|
168
|
+
filename = resolve_path_and_filename_for_package(os.fspath(config_item))
|
|
169
|
+
with open(filename) as file_to_parse:
|
|
170
|
+
dict_to_parse = yaml.load(file_to_parse, Loader=yaml.FullLoader)
|
|
171
|
+
|
|
172
|
+
self._create_config_from_dict(dict_to_parse)
|
|
173
|
+
|
|
174
|
+
elif isinstance(config_item, Config):
|
|
175
|
+
self._create_config_from_dict(config_item.as_dict())
|
|
176
|
+
else:
|
|
177
|
+
error_msg = (
|
|
178
|
+
"Can only create a config with a nested dict or the "
|
|
179
|
+
"string of a 'yamable' filename, or a list "
|
|
180
|
+
"comprising these things"
|
|
181
|
+
)
|
|
182
|
+
self.log.critical(error_msg)
|
|
183
|
+
|
|
184
|
+
def _create_config_from_dict(self, config_object):
|
|
185
|
+
"""
|
|
186
|
+
Take a dictionary object and turn it into self
|
|
187
|
+
|
|
188
|
+
When we've close self will be an object where the attributes are
|
|
189
|
+
|
|
190
|
+
So if config_object=dict(a=2, b=2)
|
|
191
|
+
Then this object will become self.a=2, self.b=2
|
|
192
|
+
"""
|
|
193
|
+
base_config = config_object.get("base_config")
|
|
194
|
+
if base_config is not None:
|
|
195
|
+
self._create_config_from_item(base_config)
|
|
196
|
+
|
|
197
|
+
attr_names = list(config_object.keys())
|
|
198
|
+
[setattr(self, keyname, config_object[keyname]) for keyname in config_object]
|
|
199
|
+
|
|
200
|
+
self.add_elements(attr_names)
|
|
201
|
+
|
|
202
|
+
def system_init(self, base_system):
|
|
203
|
+
"""
|
|
204
|
+
This is run when added to a base system
|
|
205
|
+
|
|
206
|
+
:param base_system
|
|
207
|
+
:return: nothing
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
# fill with defaults
|
|
211
|
+
self.fill_with_defaults()
|
|
212
|
+
|
|
213
|
+
def __delattr__(self, element_name: str):
|
|
214
|
+
"""
|
|
215
|
+
Remove element_name from config
|
|
216
|
+
|
|
217
|
+
>>> config=Config(dict(parameters=dict(p1=3, p2=4.6), another_thing=[]))
|
|
218
|
+
>>> del(config.another_thing)
|
|
219
|
+
>>> config
|
|
220
|
+
Config with elements: parameters
|
|
221
|
+
>>>
|
|
222
|
+
"""
|
|
223
|
+
# to avoid recursion, we must first avoid recursion
|
|
224
|
+
super().__delattr__(element_name)
|
|
225
|
+
|
|
226
|
+
self.remove_element(element_name)
|
|
227
|
+
|
|
228
|
+
def __setattr__(self, element_name: str, value):
|
|
229
|
+
"""
|
|
230
|
+
Add / replace element_name in config
|
|
231
|
+
|
|
232
|
+
>>> config=Config(dict(parameters=dict(p1=3, p2=4.6), another_thing=[]))
|
|
233
|
+
>>> config.another_thing="test"
|
|
234
|
+
>>> config.another_thing
|
|
235
|
+
'test'
|
|
236
|
+
>>> config.yet_another_thing="more testing"
|
|
237
|
+
>>> config
|
|
238
|
+
Config with elements: another_thing, parameters, yet_another_thing
|
|
239
|
+
>>>
|
|
240
|
+
"""
|
|
241
|
+
# to avoid recursion, we must first avoid recursion
|
|
242
|
+
super().__setattr__(element_name, value)
|
|
243
|
+
self.add_single_element(element_name)
|
|
244
|
+
|
|
245
|
+
def fill_with_defaults(self):
|
|
246
|
+
"""
|
|
247
|
+
Fills with defaults - private stuff first, then defaults
|
|
248
|
+
"""
|
|
249
|
+
# self.log.debug("Adding config defaults")
|
|
250
|
+
|
|
251
|
+
self_as_dict = self.as_dict()
|
|
252
|
+
defaults_dict = self.default_config_dict
|
|
253
|
+
private_dict = self.private_config_dict
|
|
254
|
+
|
|
255
|
+
## order is - self (backtest filename), private, defaults
|
|
256
|
+
new_dict_with_private = fill_config_dict_with_defaults(
|
|
257
|
+
self_as_dict, private_dict
|
|
258
|
+
)
|
|
259
|
+
new_dict_with_defaults = fill_config_dict_with_defaults(
|
|
260
|
+
new_dict_with_private, defaults_dict
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self._create_config_from_dict(new_dict_with_defaults)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def default_config_dict(self) -> dict:
|
|
267
|
+
default_filename = self.default_config_filename
|
|
268
|
+
default_dict = get_system_defaults_dict(filename=default_filename)
|
|
269
|
+
|
|
270
|
+
return default_dict
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def default_config_filename(self) -> Optional[str]:
|
|
274
|
+
default_filename = getattr(self, "_default_filename", None)
|
|
275
|
+
|
|
276
|
+
return default_filename
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def private_config_dict(self) -> dict:
|
|
280
|
+
private_filename = self.private_config_filename
|
|
281
|
+
private_dict = get_private_config_as_dict(private_filename)
|
|
282
|
+
|
|
283
|
+
return private_dict
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def private_config_filename(self):
|
|
287
|
+
private_filename = getattr(self, "_private_filename", None)
|
|
288
|
+
|
|
289
|
+
return private_filename
|
|
290
|
+
|
|
291
|
+
def as_dict(self):
|
|
292
|
+
element_names = sorted(getattr(self, "_elements", []))
|
|
293
|
+
self_as_dict = {}
|
|
294
|
+
for element in element_names:
|
|
295
|
+
self_as_dict[element] = getattr(self, element, "")
|
|
296
|
+
|
|
297
|
+
return self_as_dict
|
|
298
|
+
|
|
299
|
+
def save(self, filename):
|
|
300
|
+
config_to_save = self.as_dict()
|
|
301
|
+
with open(filename, "w") as file:
|
|
302
|
+
yaml.dump(config_to_save, file)
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def default_config(cls):
|
|
306
|
+
if hasattr(cls, "evaluated"):
|
|
307
|
+
return cls.evaluated
|
|
308
|
+
|
|
309
|
+
if os.getenv(PRIVATE_CONFIG_DIR_ENV_VAR):
|
|
310
|
+
config = Config(
|
|
311
|
+
private_filename=get_full_path_for_private_config(PRIVATE_CONFIG_FILE)
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
config = Config()
|
|
315
|
+
config.fill_with_defaults()
|
|
316
|
+
|
|
317
|
+
cls.evaluated = config
|
|
318
|
+
return cls.evaluated
|
|
319
|
+
|
|
320
|
+
@classmethod
|
|
321
|
+
def reset(cls):
|
|
322
|
+
if hasattr(cls, "evaluated"):
|
|
323
|
+
delattr(cls, "evaluated")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
import doctest
|
|
328
|
+
|
|
329
|
+
doctest.testmod()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All default parameters that might be used in a system are stored here
|
|
3
|
+
|
|
4
|
+
Order of preferences is - passed in command line to calculation method,
|
|
5
|
+
stored in system config object
|
|
6
|
+
found in defaults
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from quantlib_st.core.fileutils import resolve_path_and_filename_for_package
|
|
15
|
+
|
|
16
|
+
DEFAULT_FILENAME = "config.defaults.yaml"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_system_defaults_dict(filename: Optional[str] = None) -> dict:
|
|
20
|
+
"""
|
|
21
|
+
>>> get_system_defaults_dict()['average_absolute_forecast']
|
|
22
|
+
10.0
|
|
23
|
+
"""
|
|
24
|
+
if filename is None:
|
|
25
|
+
filename = DEFAULT_FILENAME
|
|
26
|
+
default_file = resolve_path_and_filename_for_package(filename)
|
|
27
|
+
with open(default_file) as file_to_parse:
|
|
28
|
+
default_dict = yaml.load(file_to_parse, Loader=yaml.FullLoader)
|
|
29
|
+
|
|
30
|
+
return default_dict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
import doctest
|
|
35
|
+
|
|
36
|
+
doctest.testmod()
|