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.
Files changed (159) hide show
  1. quantlib_st/__init__.py +0 -0
  2. quantlib_st/cli/__init__.py +3 -0
  3. quantlib_st/cli/__main__.py +5 -0
  4. quantlib_st/cli/corr_cmd.py +138 -0
  5. quantlib_st/cli/costs_cmd.py +129 -0
  6. quantlib_st/cli/main.py +45 -0
  7. quantlib_st/config/__init__.py +0 -0
  8. quantlib_st/config/configdata.py +329 -0
  9. quantlib_st/config/defaults.py +36 -0
  10. quantlib_st/config/fill_config_dict_with_defaults.py +25 -0
  11. quantlib_st/config/instruments.py +103 -0
  12. quantlib_st/config/private_config.py +29 -0
  13. quantlib_st/config/private_directory.py +15 -0
  14. quantlib_st/core/__init__.py +0 -0
  15. quantlib_st/core/constants.py +21 -0
  16. quantlib_st/core/dateutils.py +201 -0
  17. quantlib_st/core/exceptions.py +39 -0
  18. quantlib_st/core/fileutils.py +195 -0
  19. quantlib_st/core/genutils.py +391 -0
  20. quantlib_st/core/maths.py +117 -0
  21. quantlib_st/core/objects.py +118 -0
  22. quantlib_st/core/pandas/__init__.py +0 -0
  23. quantlib_st/core/pandas/find_data.py +81 -0
  24. quantlib_st/core/pandas/frequency.py +157 -0
  25. quantlib_st/core/pandas/full_merge_with_replacement.py +200 -0
  26. quantlib_st/core/pandas/merge_data_keeping_past_data.py +318 -0
  27. quantlib_st/core/pandas/merge_data_with_label_column.py +420 -0
  28. quantlib_st/core/pandas/pdutils.py +392 -0
  29. quantlib_st/core/pandas/strategy_functions.py +152 -0
  30. quantlib_st/core/text.py +123 -0
  31. quantlib_st/correlation/__init__.py +7 -0
  32. quantlib_st/correlation/correlation_over_time.py +275 -0
  33. quantlib_st/correlation/exponential_correlation.py +115 -0
  34. quantlib_st/correlation/fitting_dates.py +120 -0
  35. quantlib_st/costs/__init__.py +11 -0
  36. quantlib_st/costs/calculator.py +119 -0
  37. quantlib_st/costs/config.py +24 -0
  38. quantlib_st/costs/data_source.py +46 -0
  39. quantlib_st/estimators/__init__.py +0 -0
  40. quantlib_st/estimators/forecast_scalar.py +62 -0
  41. quantlib_st/estimators/turnover.py +29 -0
  42. quantlib_st/estimators/vol.py +199 -0
  43. quantlib_st/execution/__init__.py +0 -0
  44. quantlib_st/execution/orders/__init__.py +0 -0
  45. quantlib_st/execution/orders/base_orders.py +507 -0
  46. quantlib_st/execution/orders/named_order_objects.py +16 -0
  47. quantlib_st/execution/trade_qty.py +253 -0
  48. quantlib_st/init/__init__.py +0 -0
  49. quantlib_st/init/futures/__init__.py +0 -0
  50. quantlib_st/init/futures/build_multiple_prices_from_raw_data.py +337 -0
  51. quantlib_st/logging/__init__.py +0 -0
  52. quantlib_st/logging/adaptor.py +102 -0
  53. quantlib_st/logging/logger.py +86 -0
  54. quantlib_st/objects/__init__.py +4 -0
  55. quantlib_st/objects/adjusted_prices.py +194 -0
  56. quantlib_st/objects/carry_data.py +85 -0
  57. quantlib_st/objects/contract_dates_and_expiries.py +622 -0
  58. quantlib_st/objects/contracts.py +388 -0
  59. quantlib_st/objects/dict_of_futures_per_contract_prices.py +151 -0
  60. quantlib_st/objects/dict_of_named_futures_per_contract_prices.py +337 -0
  61. quantlib_st/objects/fills.py +124 -0
  62. quantlib_st/objects/instruments.py +392 -0
  63. quantlib_st/objects/multiple_prices.py +219 -0
  64. quantlib_st/objects/production/__init__.py +0 -0
  65. quantlib_st/objects/production/tradeable_object.py +245 -0
  66. quantlib_st/objects/rolls.py +451 -0
  67. quantlib_st/objects/spot_fx_prices.py +110 -0
  68. quantlib_st/sysdata/__init__.py +3 -0
  69. quantlib_st/sysdata/base_data.py +76 -0
  70. quantlib_st/sysdata/csv/__init__.py +15 -0
  71. quantlib_st/sysdata/csv/csv_adjusted_prices.py +84 -0
  72. quantlib_st/sysdata/csv/csv_instrument_data.py +138 -0
  73. quantlib_st/sysdata/csv/csv_multiple_prices.py +100 -0
  74. quantlib_st/sysdata/csv/csv_roll_parameters.py +120 -0
  75. quantlib_st/sysdata/csv/csv_spot_fx.py +126 -0
  76. quantlib_st/sysdata/csv/csv_spread_costs.py +71 -0
  77. quantlib_st/sysdata/data_blob.py +415 -0
  78. quantlib_st/sysdata/futures/__init__.py +13 -0
  79. quantlib_st/sysdata/futures/adjusted_prices.py +115 -0
  80. quantlib_st/sysdata/futures/instruments.py +157 -0
  81. quantlib_st/sysdata/futures/multiple_prices.py +126 -0
  82. quantlib_st/sysdata/futures/rolls_parameters.py +111 -0
  83. quantlib_st/sysdata/futures/spread_costs.py +44 -0
  84. quantlib_st/sysdata/fx/__init__.py +3 -0
  85. quantlib_st/sysdata/fx/spotfx.py +242 -0
  86. quantlib_st/sysdata/production_config.py +8 -0
  87. quantlib_st/sysdata/sim/__init__.py +15 -0
  88. quantlib_st/sysdata/sim/base_data.py +3 -0
  89. quantlib_st/sysdata/sim/csv_futures_sim_test_data.py +116 -0
  90. quantlib_st/sysdata/sim/futures_sim_data.py +266 -0
  91. quantlib_st/sysdata/sim/futures_sim_data_with_data_blob.py +130 -0
  92. quantlib_st/sysdata/sim/sim_data.py +325 -0
  93. quantlib_st/systems/__init__.py +0 -0
  94. quantlib_st/systems/accounts/__init__.py +0 -0
  95. quantlib_st/systems/accounts/account_buffering_subsystem.py +208 -0
  96. quantlib_st/systems/accounts/account_buffering_system.py +116 -0
  97. quantlib_st/systems/accounts/account_costs.py +366 -0
  98. quantlib_st/systems/accounts/account_forecast.py +330 -0
  99. quantlib_st/systems/accounts/account_inputs.py +317 -0
  100. quantlib_st/systems/accounts/account_instruments.py +225 -0
  101. quantlib_st/systems/accounts/account_portfolio.py +71 -0
  102. quantlib_st/systems/accounts/account_subsystem.py +206 -0
  103. quantlib_st/systems/accounts/account_trading_rules.py +216 -0
  104. quantlib_st/systems/accounts/account_with_multiplier.py +163 -0
  105. quantlib_st/systems/accounts/accounts_stage.py +12 -0
  106. quantlib_st/systems/accounts/curves/__init__.py +13 -0
  107. quantlib_st/systems/accounts/curves/account_curve.py +416 -0
  108. quantlib_st/systems/accounts/curves/account_curve_group.py +169 -0
  109. quantlib_st/systems/accounts/curves/dict_of_account_curves.py +41 -0
  110. quantlib_st/systems/accounts/curves/nested_account_curve_group.py +125 -0
  111. quantlib_st/systems/accounts/curves/stats_dict.py +254 -0
  112. quantlib_st/systems/accounts/pandl_calculators/__init__.py +25 -0
  113. quantlib_st/systems/accounts/pandl_calculators/pandl_SR_cost.py +132 -0
  114. quantlib_st/systems/accounts/pandl_calculators/pandl_calculation.py +244 -0
  115. quantlib_st/systems/accounts/pandl_calculators/pandl_calculation_dict.py +106 -0
  116. quantlib_st/systems/accounts/pandl_calculators/pandl_cash_costs.py +262 -0
  117. quantlib_st/systems/accounts/pandl_calculators/pandl_generic_costs.py +108 -0
  118. quantlib_st/systems/accounts/pandl_calculators/pandl_using_fills.py +173 -0
  119. quantlib_st/systems/accounts/tests/test_accounts.py +26 -0
  120. quantlib_st/systems/basesystem.py +442 -0
  121. quantlib_st/systems/forecast_scale_cap.py +523 -0
  122. quantlib_st/systems/forecasting.py +249 -0
  123. quantlib_st/systems/provided/__init__.py +0 -0
  124. quantlib_st/systems/provided/config/__init__.py +0 -0
  125. quantlib_st/systems/provided/futures_chapter15/basesystem.py +63 -0
  126. quantlib_st/systems/provided/rules/__init__.py +0 -0
  127. quantlib_st/systems/provided/rules/breakout.py +38 -0
  128. quantlib_st/systems/provided/rules/carry.py +41 -0
  129. quantlib_st/systems/provided/rules/ewmac.py +180 -0
  130. quantlib_st/systems/rawdata.py +745 -0
  131. quantlib_st/systems/stage.py +53 -0
  132. quantlib_st/systems/system_cache.py +793 -0
  133. quantlib_st/systems/tests/__init__.py +0 -0
  134. quantlib_st/systems/tests/test_forecast_scale_cap.py +88 -0
  135. quantlib_st/systems/tests/test_forecasting.py +38 -0
  136. quantlib_st/systems/tools/__init__.py +0 -0
  137. quantlib_st/systems/tools/autogroup.py +275 -0
  138. quantlib_st/systems/trading_rules.py +554 -0
  139. quantlib_st-1.8.0.dist-info/METADATA +94 -0
  140. quantlib_st-1.8.0.dist-info/RECORD +159 -0
  141. quantlib_st-1.8.0.dist-info/WHEEL +5 -0
  142. quantlib_st-1.8.0.dist-info/entry_points.txt +2 -0
  143. quantlib_st-1.8.0.dist-info/licenses/LICENSE +21 -0
  144. quantlib_st-1.8.0.dist-info/top_level.txt +2 -0
  145. tests/__init__.py +0 -0
  146. tests/core/__init__.py +0 -0
  147. tests/core/test_fileutils.py +90 -0
  148. tests/correlation/__init__.py +0 -0
  149. tests/correlation/test_correlation.py +75 -0
  150. tests/correlation/test_correlationlist_as.py +43 -0
  151. tests/correlation/test_fitting_dates.py +54 -0
  152. tests/correlation/test_jsonable_to_long.py +48 -0
  153. tests/estimators/test_forecast_scalar.py +55 -0
  154. tests/estimators/test_vol.py +40 -0
  155. tests/systems/__init__.py +0 -0
  156. tests/systems/provided/__init__.py +0 -0
  157. tests/systems/provided/rules/__init__.py +0 -0
  158. tests/systems/provided/rules/test_ewmac.py +48 -0
  159. tests/systems/test_account_forecast.py +93 -0
File without changes
@@ -0,0 +1,3 @@
1
+ __all__ = ["main"]
2
+
3
+ from quantlib_st.cli.main import main
@@ -0,0 +1,5 @@
1
+ from quantlib_st.cli.main import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -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
@@ -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()