trade-database-manager 0.0.1.dev2__tar.gz → 0.0.1.dev4__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 (24) hide show
  1. {trade_database_manager-0.0.1.dev2/trade_database_manager.egg-info → trade_database_manager-0.0.1.dev4}/PKG-INFO +1 -1
  2. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/pyproject.toml +5 -1
  3. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/sql/sqlmanager.py +97 -9
  4. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/manager/__init__.py +4 -0
  5. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/manager/metadata_sql.py +79 -14
  6. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4/trade_database_manager.egg-info}/PKG-INFO +1 -1
  7. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/LICENSE +0 -0
  8. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/README.md +0 -0
  9. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/setup.cfg +0 -0
  10. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/setup.py +0 -0
  11. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/__init__.py +0 -0
  12. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/config.py +0 -0
  13. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/__init__.py +0 -0
  14. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/kdb/__init__.py +0 -0
  15. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/sql/__init__.py +0 -0
  16. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/sql/sqlreader.py +0 -0
  17. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/sql/sqlwriter.py +0 -0
  18. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/core/typedefs.py +0 -0
  19. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/manager/fields_data_type.py +0 -0
  20. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager/manager/typedefs.py +0 -0
  21. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager.egg-info/SOURCES.txt +0 -0
  22. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager.egg-info/dependency_links.txt +0 -0
  23. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager.egg-info/requires.txt +0 -0
  24. {trade_database_manager-0.0.1.dev2 → trade_database_manager-0.0.1.dev4}/trade_database_manager.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: trade_database_manager
3
- Version: 0.0.1.dev2
3
+ Version: 0.0.1.dev4
4
4
  Summary: A wrapper of kdb and sql for convenient trade data management.
5
5
  Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
6
6
  Classifier: Operating System :: POSIX :: Linux
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trade_database_manager"
7
- version = "0.0.1.dev2"
7
+ version = "0.0.1.dev4"
8
8
  description = "A wrapper of kdb and sql for convenient trade data management."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -43,3 +43,7 @@ target-version = ['py310']
43
43
  [tool.isort]
44
44
  profile = "black"
45
45
  line_length = 120
46
+
47
+ [tool.flake8]
48
+ max-line-length = 120
49
+ ignore = "E203,E501,W503"
@@ -4,7 +4,7 @@ from functools import partial, reduce
4
4
  from typing import Any, Literal, Sequence, Type, Union
5
5
 
6
6
  import pandas as pd
7
- from sqlalchemy import Float, Index, Integer, MetaData, String, Table, create_engine, inspect, select, sql, text
7
+ from sqlalchemy import REAL, Index, Integer, MetaData, String, Table, create_engine, inspect, select, sql, text
8
8
  from sqlalchemy.dialects.postgresql import insert
9
9
 
10
10
  from ...config import CONFIG
@@ -27,6 +27,12 @@ def _insert_on_conflict_nothing(table, conn, keys, data_iter):
27
27
 
28
28
 
29
29
  class SqlManager:
30
+ """
31
+ This class is used to manage SQL operations.
32
+
33
+ :ivar sqlalchemy.engine.Engine engine: An instance of the SQLAlchemy Engine class for executing SQL operations.
34
+ """
35
+
30
36
  def __init__(self):
31
37
  self.engine = create_engine(CONFIG["sqlconnstr"])
32
38
 
@@ -37,12 +43,20 @@ class SqlManager:
37
43
  return conn.execute(sql_executable)
38
44
 
39
45
  def add_index(self, table_name: str, columns: Union[str, list[str]], unique: bool = True):
46
+ """
47
+ Adds an index to a table.
48
+
49
+ :param table_name: The name of the table to add the index to.
50
+ :type table_name: str
51
+ :param columns: The column(s) to include in the index. It can be a single column name or a list of column names.
52
+ :type columns: Union[str, list[str]]
53
+ :param unique: Whether the index should enforce unique values. Defaults to True.
54
+ :type unique: bool
55
+ """
40
56
  if isinstance(columns, str):
41
57
  columns = [columns]
42
58
 
43
59
  index_name = f"uix_{table_name}_{'_'.join(columns)}"
44
- columns_str = ", ".join(columns)
45
- unique_str = "UNIQUE" if unique else ""
46
60
  table_meta = MetaData()
47
61
  table = Table(table_name, table_meta, autoload_with=self.engine)
48
62
  columns = [getattr(table.c, colname) for colname in columns if colname in table.c.keys()]
@@ -57,6 +71,22 @@ class SqlManager:
57
71
  other_unique_index_columns: Sequence[str] = (),
58
72
  other_non_unique_index_columns: Sequence[str] = (),
59
73
  ):
74
+ """
75
+ Inserts data into a table.
76
+
77
+ :param table_name: The name of the table to insert data into.
78
+ :type table_name: str
79
+ :param df: The data to insert. It should be a DataFrame where the column names match the table columns.
80
+ :type df: pd.DataFrame
81
+ :param upsert: Whether to update the table if the data already exists. Defaults to True.
82
+ :type upsert: bool
83
+ :param other_unique_index_columns: Other columns to enforce unique values on. Defaults to an empty sequence.
84
+ :type other_unique_index_columns: Sequence[str]
85
+ :param other_non_unique_index_columns: Other columns to add non-unique indexes to. Defaults to an empty sequence.
86
+ :type other_non_unique_index_columns: Sequence[str]
87
+ :return: The number of rows inserted.
88
+ :rtype: int
89
+ """
60
90
  if_exists: Literal["replace", "append"] = "append"
61
91
  inspector = inspect(self.engine)
62
92
  new_table = not inspector.has_table(table_name)
@@ -78,25 +108,55 @@ class SqlManager:
78
108
  return num_rows
79
109
 
80
110
  def _convert_to_sqlalchemy_type(self, column_type: Type, **kwargs):
81
- if isinstance(column_type, str):
82
- column_type = type(column_type)
83
- if column_type == str:
111
+ if isinstance(column_type, type):
112
+ column_type = column_type.__name__
113
+ if column_type == "str":
84
114
  return String(**kwargs)
85
- if column_type == int:
115
+ if column_type == "int":
86
116
  return Integer()
87
- if column_type == float:
88
- return Float()
117
+ if column_type == "float":
118
+ return REAL()
89
119
  raise ValueError(f"Unsupported column type {column_type}")
90
120
 
91
121
  def insert_column(self, table_name: str, column_name: str, column_type: Union[str, Type], type_kwargs: dict = None):
122
+ """
123
+ Inserts a new column into a table.
124
+
125
+ :param table_name: The name of the table to insert the column into.
126
+ :type table_name: str
127
+ :param column_name: The name of the new column.
128
+ :type column_name: str
129
+ :param column_type: The data type of the new column. It can be a string or a Python type.
130
+ :type column_type: Union[str, Type]
131
+ :param type_kwargs: Additional keyword arguments for the data type. Defaults to None.
132
+ :type type_kwargs: dict, optional
133
+ """
92
134
  type_kwargs = type_kwargs or {}
93
135
  sql_code = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {str(self._convert_to_sqlalchemy_type(column_type, **type_kwargs))}"
94
136
  self._execute(sql_code)
95
137
 
96
138
  def delete_column(self, table_name: str, column_name: str):
139
+ """
140
+ Deletes a column from a table.
141
+
142
+ :param table_name: The name of the table to delete the column from.
143
+ :type table_name: str
144
+ :param column_name: The name of the column to delete.
145
+ :type column_name: str
146
+ """
97
147
  self._execute(f"ALTER TABLE {table_name} DROP COLUMN {column_name}")
98
148
 
99
149
  def rename_column(self, table_name: str, old_column_name: str, new_column_name: str):
150
+ """
151
+ Renames a column in a table.
152
+
153
+ :param table_name: The name of the table containing the column to rename.
154
+ :type table_name: str
155
+ :param old_column_name: The current name of the column.
156
+ :type old_column_name: str
157
+ :param new_column_name: The new name for the column.
158
+ :type new_column_name: str
159
+ """
100
160
  # check if any index is referring to the column
101
161
  sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
102
162
  index_refering_column = dict(self.engine.execute(sql_code).fetchall())
@@ -154,6 +214,19 @@ class SqlManager:
154
214
  return pd.DataFrame(res.fetchall(), columns=res.keys())
155
215
 
156
216
  def read_data(self, table_name: str, query_fields: QUERYFIELD_TYPE = "*", filter_fields=None):
217
+ """
218
+ Reads data from a table.
219
+
220
+ :param table_name: The name of the table to read data from.
221
+ :type table_name: str
222
+ :param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
223
+ :type query_fields: QUERYFIELD_TYPE, optional
224
+ :param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
225
+ :type filter_fields: dict, optional
226
+ :return: A DataFrame containing the queried data.
227
+ :rtype: pd.DataFrame
228
+ """
229
+
157
230
  meta = MetaData()
158
231
  table = Table(table_name, meta, autoload_with=self.engine)
159
232
 
@@ -177,6 +250,7 @@ class SqlManager:
177
250
 
178
251
  # cannot use pandas.read_sql here as it discards timezone info
179
252
  res = self._execute(stmt)
253
+ pd.read_sql_query()
180
254
  return pd.DataFrame(res.fetchall(), columns=res.keys())
181
255
 
182
256
  def read_data_across_tables(
@@ -186,6 +260,20 @@ class SqlManager:
186
260
  query_fields: QUERYFIELD_TYPE = "*",
187
261
  filter_fields: FILTERFIELD_TYPE = None,
188
262
  ):
263
+ """
264
+ Reads data from multiple tables.
265
+
266
+ :param table_names: The names of the tables to read data from.
267
+ :type table_names: Sequence[str]
268
+ :param joined_columns: The columns to join the tables on.
269
+ :type joined_columns: Sequence[str]
270
+ :param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
271
+ :type query_fields: QUERYFIELD_TYPE, optional
272
+ :param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
273
+ :type filter_fields: FILTERFIELD_TYPE, optional
274
+ :return: A DataFrame containing the queried data.
275
+ :rtype: pd.DataFrame
276
+ """
189
277
  meta = MetaData()
190
278
  tables = {table_name: Table(table_name, meta, autoload_with=self.engine) for table_name in table_names}
191
279
 
@@ -3,3 +3,7 @@
3
3
  # @Author : YQ Tsui
4
4
  # @File : __init__.py
5
5
  # @Purpose :
6
+
7
+ from .metadata_sql import MetadataSql
8
+
9
+ __all__ = ("MetadataSql",)
@@ -5,7 +5,7 @@
5
5
  # @Purpose : Instrument metadata stored in SQL database
6
6
 
7
7
  from collections.abc import Container
8
- from typing import Union
8
+ from typing import Union, cast
9
9
 
10
10
  import pandas as pd
11
11
 
@@ -14,6 +14,7 @@ from .typedefs import EXCHANGE_LITERALS, INST_TYPE_LITERALS, Opt_T_SeqT, T_DictT
14
14
 
15
15
  COMMON_METADATA_COLUMNS = [
16
16
  "name",
17
+ "trading_code",
17
18
  "inst_type",
18
19
  "currency",
19
20
  "timezone",
@@ -25,15 +26,33 @@ COMMON_METADATA_COLUMNS = [
25
26
  "delisted_date",
26
27
  ]
27
28
  TYPE_METADATA_COLUMNS = {
28
- "STK": ["sector", "industry", "country", "state", "board_type"],
29
+ "STK": ["sector", "industry", "country", "state", "board_type", "issue_price"],
29
30
  }
30
31
 
31
32
 
32
33
  class MetadataSql:
33
- def __init__(self):
34
- self.manager = SqlManager()
34
+ """
35
+ This class is used to manage instrument metadata stored in an SQL database.
36
+
37
+ This is a singleton class. Just call MetadataSql() to get the instance.
38
+ """
39
+
40
+ _instance = None
41
+ _manager = None
42
+
43
+ def __new__(cls):
44
+ if not isinstance(cls._instance, cls):
45
+ cls._instance = super(MetadataSql, cls).__new__(cls)
46
+ cls._manager = SqlManager()
47
+ return cls._instance
35
48
 
36
49
  def update_instrument_metadata(self, data: Union[pd.DataFrame, list[dict], dict]):
50
+ """
51
+ Updates the instrument metadata in the database.
52
+
53
+ :param data: The data to be updated. It can be a DataFrame, a list of dictionaries, or a single dictionary.
54
+ :type data: Union[pd.DataFrame, list[dict], dict]
55
+ """
37
56
  type_specific_columns = set(data.columns) - set(COMMON_METADATA_COLUMNS)
38
57
  if "inst_type" not in data.columns and bool(type_specific_columns):
39
58
  raise ValueError(
@@ -43,15 +62,23 @@ class MetadataSql:
43
62
  data = [data]
44
63
  if isinstance(data, list):
45
64
  data = pd.DataFrame(data)
46
- data.set_index(["ticker", "exchange"], inplace=True)
65
+ if "ticker" in data.columns and "exchange" in data.columns:
66
+ data.set_index(["ticker", "exchange"], inplace=True)
67
+ else:
68
+ assert set(data.index.names) == {"ticker", "exchange"}, "Index names must be 'ticker' and 'exchange'."
47
69
  data_common = data[data.columns.intersection(COMMON_METADATA_COLUMNS)]
48
70
 
49
- self.manager.insert("instruments", data_common, upsert=True)
71
+ self._manager.insert("instruments", data_common, upsert=True)
50
72
  if "inst_type" in data.columns:
51
73
  for inst_type, columns in TYPE_METADATA_COLUMNS.items():
52
74
  data_type_df = data.loc[data.inst_type == inst_type, data.columns.intersection(columns)]
53
75
  if not data_type_df.empty:
54
- self.manager.insert(f"instruments_{inst_type.lower()}", data_type_df, upsert=True)
76
+ self._manager.insert(f"instruments_{inst_type.lower()}", data_type_df, upsert=True)
77
+
78
+ def _convert_datetime_columns(self, data: pd.DataFrame):
79
+ for col in ["listed_date", "delisted_date"]:
80
+ if col in data.columns:
81
+ data[col] = data[col].apply(pd.to_datetime)
55
82
 
56
83
  def read_metadata(
57
84
  self,
@@ -60,11 +87,25 @@ class MetadataSql:
60
87
  query_fields="*",
61
88
  filter_fields=None,
62
89
  ) -> T_DictT[pd.DataFrame]:
90
+ """
91
+ Reads metadata from the database based on the provided filters.
92
+
93
+ :param ticker: The ticker(s) to filter by. It can be a single ticker or a sequence of tickers. Defaults to None.
94
+ :type ticker: Opt_T_SeqT[str], optional
95
+ :param exchange: The exchange(s) to filter by. It can be a single exchange or a sequence of exchanges. Defaults to None.
96
+ :type exchange: Opt_T_SeqT[EXCHANGE_LITERALS], optional
97
+ :param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
98
+ :type query_fields: str, optional
99
+ :param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
100
+ :type filter_fields: dict, optional
101
+ :return: A dictionary of DataFrames containing the queried metadata.
102
+ :rtype: T_DictT[pd.DataFrame]
103
+ """
63
104
  filter_fields = filter_fields or {}
64
105
  if ticker is not None:
65
106
  filter_fields["ticker"] = ticker
66
107
  if exchange is not None:
67
- if not isinstance(exchange, str) and isinstance(exchange, Container):
108
+ if not isinstance(exchange, str) and isinstance(exchange, Container) and ticker is not None:
68
109
  assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
69
110
  filter_fields["exchange"] = exchange
70
111
  query_fields_common = (
@@ -82,11 +123,15 @@ class MetadataSql:
82
123
  )
83
124
  if not (query_fields == "*" or "inst_type" in query_fields):
84
125
  query_fields_common.append("inst_type")
85
- common_df = self.manager.read_data("instruments", query_fields=query_fields_common, filter_fields=filter_fields)
126
+ common_df = self._manager.read_data(
127
+ "instruments", query_fields=query_fields_common, filter_fields=filter_fields
128
+ )
129
+ self._convert_datetime_columns(common_df)
86
130
  if common_df.empty:
87
131
  return {}
88
132
  res = {}
89
133
  for inst_type, common_df_by_type in common_df.groupby("inst_type"):
134
+ inst_type = cast(INST_TYPE_LITERALS, inst_type)
90
135
  if all_fields_common:
91
136
  res[inst_type] = common_df_by_type
92
137
  continue
@@ -98,7 +143,7 @@ class MetadataSql:
98
143
  filter_fields_type = {k: v for k, v in filter_fields.items() if k in TYPE_METADATA_COLUMNS[inst_type]}
99
144
  filter_fields_type["ticker"] = common_df_by_type["ticker"].to_list()
100
145
  filter_fields_type["exchange"] = common_df_by_type["exchange"].to_list()
101
- type_df = self.manager.read_data(
146
+ type_df = self._manager.read_data(
102
147
  f"instruments_{inst_type.lower()}", query_fields=query_fields_type, filter_fields=filter_fields_type
103
148
  )
104
149
  type_df = common_df_by_type.merge(type_df, on=["ticker", "exchange"], how="inner")
@@ -113,12 +158,28 @@ class MetadataSql:
113
158
  query_fields="*",
114
159
  filter_fields=None,
115
160
  ) -> pd.DataFrame:
161
+ """
162
+ Reads metadata for a specific instrument type from the database based on the provided filters.
163
+
164
+ :param inst_type: The instrument type to filter by.
165
+ :type inst_type: INST_TYPE_LITERALS
166
+ :param ticker: The ticker(s) to filter by. It can be a single ticker or a sequence of tickers. Defaults to None.
167
+ :type ticker: Opt_T_SeqT[str], optional
168
+ :param exchange: The exchange(s) to filter by. It can be a single exchange or a sequence of exchanges. Defaults to None.
169
+ :type exchange: Opt_T_SeqT[EXCHANGE_LITERALS], optional
170
+ :param query_fields: The fields to query. By default, it queries all fields. Defaults to "*".
171
+ :type query_fields: str, optional
172
+ :param filter_fields: Additional fields to filter by. The keys are the field names and the values are the filter values. Defaults to None.
173
+ :type filter_fields: dict, optional
174
+ :return: A DataFrame containing the queried metadata for the specified instrument type.
175
+ :rtype: pd.DataFrame
176
+ """
116
177
  filter_fields = filter_fields or {}
117
178
  filter_fields["inst_type"] = inst_type
118
179
  if ticker is not None:
119
180
  filter_fields["ticker"] = ticker
120
181
  if exchange is not None:
121
- if not isinstance(exchange, str) and isinstance(exchange, Container):
182
+ if not isinstance(exchange, str) and isinstance(exchange, Container) and ticker is not None:
122
183
  assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
123
184
  filter_fields["exchange"] = exchange
124
185
 
@@ -144,12 +205,16 @@ class MetadataSql:
144
205
  "instruments": filter_fields_common,
145
206
  f"instruments_{inst_type.lower()}": filter_fields_type,
146
207
  }
147
- df = self.manager.read_data_across_tables(
208
+ df = self._manager.read_data_across_tables(
148
209
  ["instruments", f"instruments_{inst_type.lower()}"],
149
210
  joined_columns=["ticker", "exchange"],
150
211
  query_fields=query_fields_cross,
151
212
  filter_fields=filter_fields_cross,
152
213
  )
214
+
153
215
  if isinstance(df.columns, pd.Index):
154
- return df.loc[:, ~df.columns.duplicated()].set_index(["ticker", "exchange"])
155
- return df.set_index(["ticker", "exchange"])
216
+ df = df.loc[:, ~df.columns.duplicated()].set_index(["ticker", "exchange"])
217
+ else:
218
+ df = df.set_index(["ticker", "exchange"])
219
+ self._convert_datetime_columns(df)
220
+ return df
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: trade_database_manager
3
- Version: 0.0.1.dev2
3
+ Version: 0.0.1.dev4
4
4
  Summary: A wrapper of kdb and sql for convenient trade data management.
5
5
  Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
6
6
  Classifier: Operating System :: POSIX :: Linux