mainsequence 2.0.2a0__tar.gz → 2.0.4__tar.gz

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 (122) hide show
  1. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/PKG-INFO +1 -1
  2. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/cli/api.py +31 -0
  3. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/cli/cli.py +28 -4
  4. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/cli/config.py +4 -0
  5. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/__init__.py +1 -1
  6. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/base.py +1 -0
  7. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/models_tdag.py +130 -2
  8. mainsequence-2.0.4/mainsequence/instruments/__init__.py +2 -0
  9. mainsequence-2.0.4/mainsequence/instruments/data_interface/__init__.py +22 -0
  10. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/data_interface/data_interface.py +9 -1
  11. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/interest_rate_swap.py +2 -0
  12. mainsequence-2.0.4/mainsequence/instruments/instruments.default.toml +17 -0
  13. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/pricing_models/indices.py +26 -18
  14. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/settings.py +25 -26
  15. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence.egg-info/PKG-INFO +1 -1
  16. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence.egg-info/SOURCES.txt +1 -0
  17. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/pyproject.toml +1 -1
  18. mainsequence-2.0.2a0/mainsequence/instruments/__init__.py +0 -1
  19. mainsequence-2.0.2a0/mainsequence/instruments/data_interface/__init__.py +0 -10
  20. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/LICENSE +0 -0
  21. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/README.md +0 -0
  22. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/__init__.py +0 -0
  23. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/__main__.py +0 -0
  24. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/cli/__init__.py +0 -0
  25. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/cli/ssh_utils.py +0 -0
  26. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  27. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  28. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/data_sources_interfaces/timescale.py +0 -0
  29. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/models_helpers.py +0 -0
  30. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/models_report_studio.py +0 -0
  31. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/models_vam.py +0 -0
  32. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/client/utils.py +0 -0
  33. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/__init__.py +0 -0
  34. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/__init__.py +0 -0
  35. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/assets/config.toml +0 -0
  36. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
  37. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/assets/logo.png +0 -0
  38. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/core/__init__.py +0 -0
  39. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/core/theme.py +0 -0
  40. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
  41. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/dashboards/streamlit/scaffold.py +0 -0
  42. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instrumentation/__init__.py +0 -0
  43. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instrumentation/utils.py +0 -0
  44. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/__init__.py +0 -0
  45. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/base_instrument.py +0 -0
  46. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/bond.py +0 -0
  47. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/european_option.py +0 -0
  48. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/json_codec.py +0 -0
  49. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/knockout_fx_option.py +0 -0
  50. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/position.py +0 -0
  51. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/ql_fields.py +0 -0
  52. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/instruments/vanilla_fx_option.py +0 -0
  53. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/pricing_models/__init__.py +0 -0
  54. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/pricing_models/black_scholes.py +0 -0
  55. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/pricing_models/bond_pricer.py +0 -0
  56. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/pricing_models/fx_option_pricer.py +0 -0
  57. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/pricing_models/knockout_fx_pricer.py +0 -0
  58. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/pricing_models/swap_pricer.py +0 -0
  59. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/instruments/utils.py +0 -0
  60. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/logconf.py +0 -0
  61. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/reportbuilder/__init__.py +0 -0
  62. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/reportbuilder/__main__.py +0 -0
  63. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/reportbuilder/examples/ms_template_report.py +0 -0
  64. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/reportbuilder/model.py +0 -0
  65. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/reportbuilder/slide_templates.py +0 -0
  66. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/__init__.py +0 -0
  67. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/__main__.py +0 -0
  68. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/config.py +0 -0
  69. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/data_nodes/__init__.py +0 -0
  70. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/data_nodes/build_operations.py +0 -0
  71. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/data_nodes/data_nodes.py +0 -0
  72. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/data_nodes/persist_managers.py +0 -0
  73. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/data_nodes/run_operations.py +0 -0
  74. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/data_nodes/utils.py +0 -0
  75. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/future_registry.py +0 -0
  76. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/tdag/utils.py +0 -0
  77. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/__init__.py +0 -0
  78. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/__main__.py +0 -0
  79. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/agent_interface.py +0 -0
  80. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/config_handling.py +0 -0
  81. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
  82. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/__init__.py +0 -0
  83. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +0 -0
  84. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +0 -0
  85. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +0 -0
  86. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/news_app.py +0 -0
  87. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +0 -0
  88. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +0 -0
  89. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +0 -0
  90. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +0 -0
  91. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +0 -0
  92. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +0 -0
  93. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +0 -0
  94. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +0 -0
  95. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +0 -0
  96. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +0 -0
  97. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +0 -0
  98. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +0 -0
  99. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/prices/__init__.py +0 -0
  100. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +0 -0
  101. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/prices/utils.py +0 -0
  102. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +0 -0
  103. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +0 -0
  104. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/data_nodes.py +0 -0
  105. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/enums.py +0 -0
  106. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/models.py +0 -0
  107. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/notebook_handling.py +0 -0
  108. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/portfolio_interface.py +0 -0
  109. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
  110. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/resource_factory/app_factory.py +0 -0
  111. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/resource_factory/base_factory.py +0 -0
  112. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +0 -0
  113. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +0 -0
  114. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence/virtualfundbuilder/utils.py +0 -0
  115. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence.egg-info/dependency_links.txt +0 -0
  116. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence.egg-info/entry_points.txt +0 -0
  117. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence.egg-info/requires.txt +0 -0
  118. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/mainsequence.egg-info/top_level.txt +0 -0
  119. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/setup.cfg +0 -0
  120. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/tests/test_agent.py +0 -0
  121. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/tests/test_portfolio.py +0 -0
  122. {mainsequence-2.0.2a0 → mainsequence-2.0.4}/tests/test_replicator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 2.0.2a0
3
+ Version: 2.0.4
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -38,6 +38,9 @@ def _refresh_token() -> str | None:
38
38
  return tok.get("refresh")
39
39
 
40
40
  def login(email: str, password: str) -> dict:
41
+ email = (email or "").strip()
42
+ password = (password or "").rstrip("\r\n")
43
+
41
44
  url = _full(AUTH_PATHS["obtain"])
42
45
  payload = {"email": email, "password": password} # server expects 'email'
43
46
  r = S.post(url, data=json.dumps(payload))
@@ -155,3 +158,31 @@ def add_deploy_key(project_id: int | str, key_title: str, public_key: str) -> No
155
158
  {"key_title": key_title, "public_key": public_key})
156
159
  except Exception:
157
160
  pass
161
+
162
+ def get_project_token(project_id: int | str) -> str:
163
+ """
164
+ Fetch the project's token using the current access token.
165
+ If the access token is expired or missing, authed() will refresh once.
166
+ If refresh also fails, NotLoggedIn is raised so the caller can prompt re-login.
167
+ """
168
+ r = authed("GET", f"/orm/api/pods/projects/{project_id}/get_project_token/")
169
+ if not r.ok:
170
+ # authed() already tried refresh on 401;
171
+ # at this point treat as API error with server message.
172
+ msg = r.text
173
+ try:
174
+ if r.headers.get("content-type","").startswith("application/json"):
175
+ data = r.json()
176
+ msg = data.get("detail") or data.get("message") or msg
177
+ except Exception:
178
+ pass
179
+ raise ApiError(f"Project token fetch failed ({r.status_code}). {msg}")
180
+
181
+ token = None
182
+ if r.headers.get("content-type","").startswith("application/json"):
183
+ data = r.json()
184
+ token=data["development"][0]
185
+ token = (r.text or "").strip()
186
+ if not token:
187
+ raise ApiError("Project token response did not include a token.")
188
+ return token
@@ -26,6 +26,7 @@ from .api import (
26
26
  login as api_login,
27
27
  repo_name_from_git_url,
28
28
  safe_slug,
29
+ get_project_token
29
30
  )
30
31
  from .ssh_utils import (
31
32
  ensure_key_for_repo,
@@ -128,7 +129,6 @@ def _render_projects_table(items: list[dict], links: dict, base_dir: str, org_sl
128
129
  def login(
129
130
  email: str = typer.Argument(..., help="Email/username (server expects 'email' field)"),
130
131
  password: Optional[str] = typer.Option(None, prompt=True, hide_input=True, help="Password"),
131
- export: bool = typer.Option(False, "--export", help='Print `export MAIN_SEQUENCE_USER_TOKEN=...` so you can eval it'),
132
132
  no_status: bool = typer.Option(False, "--no-status", help="Do not print projects table after login")
133
133
  ):
134
134
  """
@@ -146,9 +146,7 @@ def login(
146
146
  typer.secho(f"Signed in as {res['username']} (Backend: {res['backend']})", fg=typer.colors.GREEN)
147
147
  typer.echo(f"Projects base folder: {base}")
148
148
 
149
- tok = cfg.get_tokens().get("access", os.environ.get("MAIN_SEQUENCE_USER_TOKEN", ""))
150
- if export and tok:
151
- print(f'export MAIN_SEQUENCE_USER_TOKEN="{tok}"')
149
+
152
150
 
153
151
  if not no_status:
154
152
  try:
@@ -287,6 +285,7 @@ def project_set_up_locally(
287
285
  cfg_obj = cfg.get_config()
288
286
  base = base_dir or cfg_obj["mainsequence_path"]
289
287
 
288
+
290
289
  org_slug = _org_slug_from_profile()
291
290
 
292
291
  items = get_projects()
@@ -346,8 +345,33 @@ def project_set_up_locally(
346
345
  else:
347
346
  if env_text and not env_text.endswith("\n"): env_text += "\n"
348
347
  env_text += f"VFB_PROJECT_PATH={str(target_dir)}\n"
348
+
349
+ try:
350
+ project_token = get_project_token(project_id)
351
+ except NotLoggedIn:
352
+ typer.secho("Session expired or refresh failed. Run: mainsequence login <email>", fg=typer.colors.RED)
353
+ raise typer.Exit(1)
354
+ except ApiError as e:
355
+ typer.secho(f"Could not fetch project token: {e}", fg=typer.colors.RED)
356
+ raise typer.Exit(1)
357
+
358
+ lines = env_text.splitlines()
359
+ if any(line.startswith("MAINSEQUENCE_TOKEN=") for line in lines):
360
+ lines = [
361
+ (f"MAINSEQUENCE_TOKEN={project_token}" if line.startswith("MAINSEQUENCE_TOKEN=") else line)
362
+ for line in lines
363
+ ]
364
+ env_text = "\n".join(lines)
365
+ else:
366
+ if env_text and not env_text.endswith("\n"): env_text += "\n"
367
+ env_text += f"MAINSEQUENCE_TOKEN={project_token}\n"
368
+
369
+ # write final .env with both vars present
349
370
  (target_dir / ".env").write_text(env_text, encoding="utf-8")
350
371
 
372
+
373
+
374
+
351
375
  cfg.set_link(project_id, str(target_dir))
352
376
 
353
377
  typer.secho(f"Local folder: {target_dir}", fg=typer.colors.GREEN)
@@ -75,4 +75,8 @@ def set_env_access(access: str) -> None:
75
75
  def backend_url() -> str:
76
76
  cfg = get_config()
77
77
  url = (cfg.get("backend_url") or DEFAULTS["backend_url"]).rstrip("/")
78
+
79
+ if os.environ.get("MAIN_SEQUENCE_BACKEND_URL") is not None:
80
+ url=os.environ.get("MAIN_SEQUENCE_BACKEND_URL")
81
+
78
82
  return url
@@ -4,7 +4,7 @@ from .models_tdag import (request_to_datetime, LocalTimeSeriesDoesNotExist, Dyna
4
4
  SourceTableConfigurationDoesNotExist, LocalTimeSerieUpdateDetails,
5
5
  JSON_COMPRESSED_PREFIX, Scheduler, SchedulerDoesNotExist, LocalTimeSerie,
6
6
  DynamicTableMetaData, DynamicTableDataSource,DUCK_DB,
7
- ColumnMetaData,Artifact,TableMetaData ,DataFrequency,SourceTableConfiguration,
7
+ ColumnMetaData,Artifact,TableMetaData ,DataFrequency,SourceTableConfiguration,Constant,
8
8
  Project, UniqueIdentifierRangeMap, LocalTimeSeriesHistoricalUpdate,
9
9
  UpdateStatistics, DataSource, PodDataSource, SessionDataSource)
10
10
 
@@ -128,6 +128,7 @@ class BaseObjectOrm:
128
128
  "DynamicResource": "tdag-gpt/dynamic_resource",
129
129
  "Artifact": "pods/artifact",
130
130
  "Job": "pods/job",
131
+ "Constant":"pods/constant",
131
132
 
132
133
  #ReportBuilder
133
134
  "Presentation": "reports/presentations",
@@ -18,9 +18,10 @@ from typing import Union
18
18
  import time
19
19
  import os
20
20
  from mainsequence.logconf import logger
21
+ from threading import RLock
21
22
 
22
- from pydantic import BaseModel, Field, field_validator
23
- from typing import Optional, List, Dict, Any, TypedDict, Tuple
23
+ from pydantic import BaseModel, Field, field_validator,computed_field
24
+ from typing import Optional, List, Dict, Any, TypedDict, Tuple, ClassVar
24
25
  from .data_sources_interfaces import timescale as TimeScaleInterface
25
26
  from functools import wraps
26
27
  import math
@@ -29,6 +30,9 @@ import base64
29
30
  import numpy as np
30
31
  import concurrent.futures
31
32
 
33
+ from cachetools import TTLCache, cachedmethod
34
+ from operator import attrgetter
35
+
32
36
  _default_data_source = None # Module-level cache
33
37
 
34
38
  JSON_COMPRESSED_PREFIX = ["json_compressed", "jcomp_"]
@@ -2272,5 +2276,129 @@ class PodDataSource:
2272
2276
  return f"{self.data_source.related_resource}"
2273
2277
 
2274
2278
 
2279
+
2280
+ def _norm_value(v: Any) -> Any:
2281
+ """Normalize values into hashable, deterministic forms for the cache key."""
2282
+ # Project objects → their integer IDs (project scoped vs global)
2283
+ if Project and isinstance(v, Project):
2284
+ return getattr(v, "id", v)
2285
+
2286
+ # Common iterables → sorted tuples to ignore order in queries like name__in
2287
+ if isinstance(v, (set, list, tuple)):
2288
+ # Convert nested items too, just in case
2289
+ return tuple(sorted(_norm_value(x) for x in v))
2290
+
2291
+ # Dicts → sorted (k,v) tuples
2292
+ if isinstance(v, dict):
2293
+ return tuple(sorted((k, _norm_value(val)) for k, val in v.items()))
2294
+
2295
+ return v # primitives pass through
2296
+ def _norm_kwargs(kwargs: Dict[str, Any]) -> Tuple[Tuple[str, Any], ...]:
2297
+ """Stable, hashable key from kwargs (order-insensitive)."""
2298
+ items = []
2299
+ for k, v in kwargs.items():
2300
+ # Special-case a big `name__in` so you don’t produce huge keys.
2301
+ if k == "name__in" and isinstance(v, (list, tuple, set)):
2302
+ items.append((k, tuple(sorted(str(x) for x in v))))
2303
+ else:
2304
+ items.append((k, _norm_value(v)))
2305
+ return tuple(sorted(items))
2306
+
2307
+ class Constant(BasePydanticModel, BaseObjectOrm):
2308
+ """
2309
+ Simple scoped constant.
2310
+ - Global when project is None.
2311
+ - Project-scoped when project is set.
2312
+
2313
+ Uniqueness (enforced in DB/service layer):
2314
+ * Global: (organization_owner, name)
2315
+ * Per-project: (organization_owner, project, name)
2316
+ """
2317
+ id: Optional[int]
2318
+
2319
+ name: str = Field(
2320
+ ...,
2321
+ max_length=255,
2322
+ description="UPPER_SNAKE_CASE; optional category via double-underscore, e.g. 'CURVE__US_TREASURIES'."
2323
+ )
2324
+ value: Any = Field(
2325
+ ...,
2326
+ description="Small JSON value (string/number/bool/object/array). Keep it small (e.g., <=10KB).",
2327
+ )
2328
+ project: Optional[Union[Project,int]] = Field(
2329
+ None,
2330
+ description="Project ID; None ⇒ global."
2331
+ )
2332
+ category: Optional[str] = None
2333
+
2334
+ # Class-level cache & lock (Pydantic ignores ClassVar)
2335
+ _filter_cache: ClassVar[TTLCache] = TTLCache(maxsize=512, ttl=600)
2336
+ _get_cache: ClassVar[TTLCache] = TTLCache(maxsize=1024, ttl=600)
2337
+
2338
+
2339
+ _cache_lock: ClassVar[RLock] = RLock()
2340
+
2341
+ model_config = dict(from_attributes=True) # allows .model_validate(from_orm_obj)
2342
+
2343
+
2344
+ @classmethod
2345
+ @cachedmethod(
2346
+ lambda cls: cls._filter_cache, # <- resolves to the real TTLCache
2347
+ lock=lambda cls: cls._cache_lock,
2348
+ key=lambda cls, **kw: _norm_kwargs(kw)
2349
+ )
2350
+ def filter(cls, **kwargs):
2351
+ # Delegate to your real filter (API/DB) only on cache miss
2352
+ return super().filter(**kwargs)
2353
+
2354
+ @classmethod
2355
+ @cachedmethod(
2356
+ lambda cls: cls._get_cache,
2357
+ lock=lambda cls: cls._cache_lock,
2358
+ key=lambda cls, **kw: _norm_kwargs(kw),
2359
+ )
2360
+ def get(cls, **kwargs):
2361
+ # e.g. get(name="CURVE__M_BONOS", project=None)
2362
+ return super().get(**kwargs)
2363
+
2364
+ @classmethod
2365
+ def get_value(cls,name:str,project_id:Optional[int]=None):
2366
+ return cls.get(name=name,project_id=project_id).value
2367
+
2368
+ @classmethod
2369
+ def invalidate_filter_cache(cls) -> None:
2370
+ cls._filter_cache.clear()
2371
+
2372
+ @classmethod
2373
+ def create_constants_if_not_exist(cls,constants_to_create: dict):
2374
+ # crete global constants if not exist in backed
2375
+
2376
+ # constants_to_create=dict(
2377
+ # TIIE_28_UID = "TIIE_28",
2378
+ # TIIE_91_UID = "TIIE_91",
2379
+ # TIIE_182_UID = "TIIE_182",
2380
+ # TIIE_OVERNIGHT_UID = "TIIE_OVERNIGHT",
2381
+ #
2382
+ # CETE_28_UID = "CETE_28",
2383
+ # CETE_91_UID = "CETE_91",
2384
+ # CETE_182_UID = "CETE_182",
2385
+ #
2386
+ # # Curve identifiers
2387
+ # TIIE_28_ZERO_CURVE = "F_TIIE_28_VALMER",
2388
+ # M_BONOS_ZERO_CURVE = "M_BONOS_ZERO_OTR",
2389
+ #
2390
+ #
2391
+ # DISCOUNT_CURVES_TABLE = "discount_curves",
2392
+ # REFERENCE_RATES_FIXING_TABLE = "fixing_rates_1d",
2393
+ # )
2394
+ existing_constants = cls.filter(name__in=list(constants_to_create.keys()))
2395
+ existing_constants_names = [c.name for c in existing_constants]
2396
+ constants_to_register = {k: v for k, v in constants_to_create.items() if k not in existing_constants_names}
2397
+ created_constants=[]
2398
+ for k, v in constants_to_register.items():
2399
+ new_constant = cls.create(name=k, value=v)
2400
+ created_constants.append(new_constant)
2401
+ return created_constants
2402
+
2275
2403
  SessionDataSource = PodDataSource()
2276
2404
  SessionDataSource.set_remote_db()
@@ -0,0 +1,2 @@
1
+ from .constants import *
2
+ from .instruments import *
@@ -0,0 +1,22 @@
1
+ from .data_interface import DateInfo, MockDataInterface, MSInterface
2
+ from mainsequence.client import Constant as _C
3
+
4
+ import os
5
+
6
+ def _make_backend():
7
+ backend = os.getenv("MSI_DATA_BACKEND", "mock").lower()
8
+ return MSInterface() if backend == "mainsequence" else MockDataInterface()
9
+
10
+ # export a single, uniform instance
11
+ data_interface = _make_backend()
12
+
13
+
14
+
15
+ constants_to_create=dict(
16
+
17
+
18
+ DISCOUNT_CURVES_TABLE = "discount_curves",
19
+ REFERENCE_RATES_FIXING_TABLE = "fixing_rates_1d",
20
+ )
21
+
22
+ _C.create_constants_if_not_exist(constants_to_create)
@@ -2,12 +2,21 @@ import datetime
2
2
  from typing import Dict, Optional, TypedDict, Any
3
3
  import random
4
4
  from mainsequence.instruments.utils import to_ql_date
5
+ import mainsequence.client as msc
5
6
  import QuantLib as ql
6
7
  import os
7
8
  import pandas as pd
8
9
  from pathlib import Path
9
10
 
10
11
 
12
+ DISCOUNT_CURVES_TABLE=msc.Constant.get_or_none(name="DISCOUNT_CURVES_TABLE")
13
+ REFERENCE_RATES_FIXING_TABLE = msc.Constant.get_or_none(name="REFERENCE_RATES_FIXING_TABLE")
14
+
15
+ assert DISCOUNT_CURVES_TABLE is not None, "DISCOUNT_CURVES_TABLE not found in constants"
16
+ assert REFERENCE_RATES_FIXING_TABLE is not None, "REFERENCE_RATES_FIXING_TABLE not found in constants"
17
+
18
+
19
+
11
20
  class DateInfo(TypedDict, total=False):
12
21
  """Defines the date range for a data query."""
13
22
  start_date: Optional[datetime.datetime]
@@ -278,7 +287,6 @@ class MSInterface():
278
287
  @cachedmethod(cache=attrgetter("_curve_cache"), lock=attrgetter("_curve_cache_lock"))
279
288
  def get_historical_discount_curve(self, curve_name, target_date):
280
289
  from mainsequence.tdag import APIDataNode
281
- from mainsequence.instruments.settings import DISCOUNT_CURVES_TABLE
282
290
  data_node = APIDataNode.build_from_identifier(identifier=DISCOUNT_CURVES_TABLE)
283
291
 
284
292
 
@@ -12,6 +12,8 @@ from mainsequence.instruments.pricing_models.swap_pricer import get_swap_cashfl
12
12
  from mainsequence.instruments.pricing_models.indices import get_index
13
13
  from mainsequence.instruments.utils import to_ql_date, to_py_date
14
14
  from .base_instrument import InstrumentModel
15
+ from mainsequence.instruments.pricing_models.indices import build_zero_curve
16
+
15
17
 
16
18
  from .ql_fields import (
17
19
  QuantLibPeriod as QPeriod,
@@ -0,0 +1,17 @@
1
+ # instruments.toml — defaults for MainSequence Instruments
2
+
3
+ DISCOUNT_CURVES_TABLE = "discount_curves"
4
+ REFERENCE_RATES_FIXING_TABLE = "fixing_rates_1d"
5
+
6
+ TIIE_28_ZERO_CURVE = "F_TIIE_28_VALMER"
7
+ M_BONOS_ZERO_CURVE = "M_BONOS_ZERO_OTR"
8
+
9
+ TIIE_28_UID = "TIIE_28"
10
+ TIIE_91_UID = "TIIE_91"
11
+ TIIE_182_UID = "TIIE_182"
12
+ TIIE_OVERNIGHT_UID = "TIIE_OVERNIGHT"
13
+
14
+ CETE_28_UID = "CETE_28"
15
+ CETE_91_UID = "CETE_91"
16
+ CETE_182_UID = "CETE_182"
17
+
@@ -1,18 +1,17 @@
1
1
  # pricing_models/indices.py
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- Index factory for QuantLib (identifier-driven, no aliases/registry/tenor parsing).
4
+ Index factory for QuantLib (identifier-driven).
5
5
 
6
6
  Usage
7
7
  -----
8
8
  >>> from datetime import date
9
- >>> from mainsequence.instruments.settings import TIIE_28_UID
10
9
  >>> from mainsequence.instruments.pricing_models.indices import get_index
11
- >>> idx = get_index(TIIE_28_UID, target_date=date(2024, 6, 14)) # As of target_date
10
+ >>> idx = get_index("TIIE_28", target_date=date(2024, 6, 14))
12
11
 
13
12
  You can also supply a forwarding curve handle:
14
13
  >>> h = ql.RelinkableYieldTermStructureHandle()
15
- >>> idx = get_index(TIIE_28_UID, target_date=date(2025, 9, 16), forwarding_curve=h)
14
+ >>> idx = get_index("TIIE_28", target_date=date(2025, 9, 16), forwarding_curve=h)
16
15
 
17
16
  Notes
18
17
  -----
@@ -30,9 +29,8 @@ import QuantLib as ql
30
29
  from functools import lru_cache
31
30
 
32
31
  from mainsequence.instruments.data_interface import data_interface
33
- from mainsequence.instruments import settings
34
32
  from mainsequence.instruments.utils import to_py_date, to_ql_date
35
-
33
+ from mainsequence.client import Constant as _C
36
34
 
37
35
  # ----------------------------- Cache (ONLY by identifier + date) ----------------------------- #
38
36
 
@@ -50,8 +48,8 @@ def clear_index_cache() -> None:
50
48
  # No tenor tokens; we store the QuantLib Period directly.
51
49
 
52
50
  INDEX_CONFIGS: Dict[str, Dict] = {
53
- settings.indices.TIIE_28_UID: dict(
54
- curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
51
+ _C.get_value(name="TIIE_28_UID"): dict(
52
+ curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
55
53
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
56
54
  day_counter=ql.Actual360(),
57
55
  currency=(ql.MXNCurrency() if hasattr(ql, "MXNCurrency") else ql.USDCurrency()),
@@ -60,8 +58,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
60
58
  bdc=ql.ModifiedFollowing,
61
59
  end_of_month=False,
62
60
  ),
63
- settings.indices.TIIE_91_UID: dict(
64
- curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
61
+ _C.get_value(name="TIIE_91_UID"): dict(
62
+ curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
65
63
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
66
64
  day_counter=ql.Actual360(),
67
65
  currency=ql.MXNCurrency(),
@@ -70,8 +68,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
70
68
  bdc=ql.ModifiedFollowing,
71
69
  end_of_month=False,
72
70
  ),
73
- settings.indices.TIIE_182_UID: dict(
74
- curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
71
+ _C.get_value(name="TIIE_182_UID"): dict(
72
+ curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
75
73
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
76
74
  day_counter=ql.Actual360(),
77
75
  currency=ql.MXNCurrency(),
@@ -81,8 +79,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
81
79
  end_of_month=False,
82
80
  ),
83
81
  # Add more identifiers here as needed.
84
- settings.indices.TIIE_OVERNIGHT_UID: dict(
85
- curve_uid=settings.curves.TIIE_28_ZERO_CURVE,
82
+ _C.get_value(name="TIIE_OVERNIGHT_UID"): dict(
83
+ curve_uid=_C.get_value(name="TIIE_28_ZERO_CURVE"),
86
84
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
87
85
  day_counter=ql.Actual360(),
88
86
  currency=ql.MXNCurrency(),
@@ -91,8 +89,8 @@ INDEX_CONFIGS: Dict[str, Dict] = {
91
89
  bdc=ql.ModifiedFollowing,
92
90
  end_of_month=False,
93
91
  ),
94
- settings.indices.CETE_28_UID: dict(
95
- curve_uid=settings.curves.M_BONOS_ZERO_CURVE,
92
+ _C.get_value(name="CETE_28_UID"): dict(
93
+ curve_uid=_C.get_value(name="M_BONOS_ZERO_CURVE"),
96
94
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
97
95
  day_counter=ql.Actual360(), # BONOS accrue on Act/360
98
96
  currency=ql.MXNCurrency(),
@@ -101,9 +99,19 @@ INDEX_CONFIGS: Dict[str, Dict] = {
101
99
  bdc=ql.Following, # “next banking business day” => Following
102
100
  end_of_month=False, # Irrelevant when scheduling by days
103
101
  ),
102
+ _C.get_value(name="CETE_91_UID"): dict(
103
+ curve_uid=_C.get_value(name="M_BONOS_ZERO_CURVE"),
104
+ calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
105
+ day_counter=ql.Actual360(), # BONOS accrue on Act/360
106
+ currency=ql.MXNCurrency(),
107
+ period=ql.Period(91, ql.Days), # Coupons every 28 days
108
+ settlement_days=1, # T+1 in Mexico since May 27–28, 2024
109
+ bdc=ql.Following, # “next banking business day” => Following
110
+ end_of_month=False, # Irrelevant when scheduling by days
111
+ ),
104
112
 
105
- settings.indices.CETE_182_UID: dict(
106
- curve_uid=settings.curves.M_BONOS_ZERO_CURVE,
113
+ _C.get_value(name="CETE_182_UID"): dict(
114
+ curve_uid=_C.get_value(name="M_BONOS_ZERO_CURVE"),
107
115
  calendar=(ql.Mexico() if hasattr(ql, "Mexico") else ql.TARGET()),
108
116
  day_counter=ql.Actual360(), # BONOS accrue on Act/360
109
117
  currency=ql.MXNCurrency(),
@@ -7,6 +7,8 @@ import sys
7
7
  from pathlib import Path
8
8
  from types import SimpleNamespace
9
9
 
10
+
11
+ HERE = Path(__file__).resolve().parent
10
12
  # ---------------- App identity & app dirs ----------------
11
13
  APP_VENDOR = "mainsequence"
12
14
  APP_NAME = "instruments"
@@ -15,6 +17,8 @@ APP_ID = f"{APP_VENDOR}/{APP_NAME}"
15
17
  # All environment variables use this prefix now.
16
18
  ENV_PREFIX = "MSI" # e.g., MSI_CONFIG_FILE, MSI_DATA_BACKEND
17
19
  ENV_CONFIG_FILE = f"{ENV_PREFIX}_CONFIG_FILE"
20
+ ENV_DEFAULT_TOML_FILE = f"{ENV_PREFIX}_DEFAULT_TOML_FILE"
21
+
18
22
 
19
23
  def _user_config_root() -> Path:
20
24
  if sys.platform == "win32":
@@ -62,31 +66,22 @@ def _load_file_config() -> dict:
62
66
  pass
63
67
  return {}
64
68
 
65
- # ---------------- default TOML (written only if no config exists) ----------------
66
- DEFAULT_TOML = """# instruments.toml defaults for MainSequence Instruments
67
-
68
- DISCOUNT_CURVES_TABLE = "discount_curves"
69
- REFERENCE_RATES_FIXING_TABLE = "fixing_rates_1d"
70
-
71
- TIIE_28_ZERO_CURVE = "F_TIIE_28_VALMER"
72
- M_BONOS_ZERO_CURVE = "M_BONOS_ZERO_OTR"
73
-
74
- TIIE_28_UID = "TIIE_28"
75
- TIIE_91_UID = "TIIE_91"
76
- TIIE_182_UID = "TIIE_182"
77
- TIIE_OVERNIGHT_UID = "TIIE_OVERNIGHT"
78
-
79
- CETE_28_UID = "CETE_28"
80
- CETE_91_UID = "CETE_91"
81
- CETE_182_UID = "CETE_182"
82
-
83
- [data]
84
- backend = "mock"
85
-
86
- [files]
87
- tiie_zero_csv = ""
88
- tiie28_fixings_csv = ""
89
- """
69
+ # ---------------- default TOML (read from a real file next to this module) -----
70
+ def _read_default_toml_text() -> str | None:
71
+ """
72
+ Returns the text of the default TOML configuration.
73
+ Order of precedence:
74
+ 1) Path from MSI_DEFAULT_TOML_FILE (if set)
75
+ 2) instruments.default.toml next to this file
76
+ Returns None if no default file is found/readable.
77
+ """
78
+ candidate = Path(os.getenv(ENV_DEFAULT_TOML_FILE, HERE / "instruments.default.toml")).expanduser()
79
+ try:
80
+ if candidate.exists():
81
+ return candidate.read_text(encoding="utf-8")
82
+ except Exception:
83
+ pass
84
+ return None
90
85
 
91
86
  def _existing_config_path() -> Path | None:
92
87
  env_cfg = os.getenv(ENV_CONFIG_FILE)
@@ -108,11 +103,15 @@ def _ensure_default_config_file() -> Path | None:
108
103
  """If no config exists anywhere, create one. Never overwrites existing."""
109
104
  if _existing_config_path() is not None:
110
105
  return None
106
+ default_text = _read_default_toml_text()
107
+ if default_text is None:
108
+ return None
109
+
111
110
  target = Path(os.getenv(ENV_CONFIG_FILE, APP_ROOT / "config.toml")).expanduser()
112
111
  try:
113
112
  target.parent.mkdir(parents=True, exist_ok=True) # ensure parent dir only
114
113
  if not target.exists():
115
- target.write_text(DEFAULT_TOML, encoding="utf-8")
114
+ target.write_text(default_text, encoding="utf-8")
116
115
  except Exception:
117
116
  return None
118
117
  return target
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 2.0.2a0
3
+ Version: 2.0.4
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -37,6 +37,7 @@ mainsequence/dashboards/streamlit/pages/__init__.py
37
37
  mainsequence/instrumentation/__init__.py
38
38
  mainsequence/instrumentation/utils.py
39
39
  mainsequence/instruments/__init__.py
40
+ mainsequence/instruments/instruments.default.toml
40
41
  mainsequence/instruments/settings.py
41
42
  mainsequence/instruments/utils.py
42
43
  mainsequence/instruments/data_interface/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "2.0.2a"
7
+ version = "2.0.4"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1 +0,0 @@
1
- from .instruments import *
@@ -1,10 +0,0 @@
1
- from .data_interface import DateInfo, MockDataInterface, MSInterface
2
- from mainsequence.instruments import settings
3
-
4
- def _make_backend():
5
- if getattr(settings, "data", None) and getattr(settings.data, "backend", "mock") == "mainsequence":
6
- return MSInterface()
7
- return MockDataInterface()
8
-
9
- # export a single, uniform instance
10
- data_interface = _make_backend()
File without changes
File without changes
File without changes