trade-database-manager 0.0.6.dev0__tar.gz → 0.0.8__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 (29) hide show
  1. {trade_database_manager-0.0.6.dev0/trade_database_manager.egg-info → trade_database_manager-0.0.8}/PKG-INFO +25 -3
  2. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/README.md +20 -0
  3. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/pyproject.toml +3 -2
  4. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/kdb/kdbmanager.py +2 -0
  5. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/sql/sqlmanager.py +145 -56
  6. trade_database_manager-0.0.8/trade_database_manager/core/sql/utils.py +35 -0
  7. trade_database_manager-0.0.8/trade_database_manager/manager/__init__.py +11 -0
  8. trade_database_manager-0.0.8/trade_database_manager/manager/fields_data_type.py +71 -0
  9. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/manager/metadata_sql.py +77 -18
  10. trade_database_manager-0.0.8/trade_database_manager/manager/metadata_sql_cb.py +109 -0
  11. trade_database_manager-0.0.8/trade_database_manager/manager/metadata_sql_fut.py +43 -0
  12. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/manager/typedefs.py +1 -1
  13. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8/trade_database_manager.egg-info}/PKG-INFO +25 -3
  14. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager.egg-info/SOURCES.txt +3 -0
  15. trade_database_manager-0.0.6.dev0/trade_database_manager/manager/__init__.py +0 -9
  16. trade_database_manager-0.0.6.dev0/trade_database_manager/manager/fields_data_type.py +0 -33
  17. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/LICENSE +0 -0
  18. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/setup.cfg +0 -0
  19. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/__init__.py +0 -0
  20. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/config.py +0 -0
  21. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/__init__.py +0 -0
  22. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/kdb/__init__.py +0 -0
  23. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/sql/__init__.py +0 -0
  24. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/sql/sqlreader.py +0 -0
  25. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/sql/sqlwriter.py +0 -0
  26. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager/core/typedefs.py +0 -0
  27. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager.egg-info/dependency_links.txt +0 -0
  28. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager.egg-info/requires.txt +0 -0
  29. {trade_database_manager-0.0.6.dev0 → trade_database_manager-0.0.8}/trade_database_manager.egg-info/top_level.txt +0 -0
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: trade_database_manager
3
- Version: 0.0.6.dev0
3
+ Version: 0.0.8
4
4
  Summary: A wrapper of kdb and sql for convenient trade data management.
5
- Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
5
+ Author-email: "Y.Q. Tsui" <qianyun210603@hotmail.com>
6
6
  Classifier: Operating System :: POSIX :: Linux
7
7
  Classifier: Operating System :: Microsoft :: Windows
8
8
  Classifier: Development Status :: 3 - Alpha
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
15
16
  Description-Content-Type: text/markdown
16
17
  License-File: LICENSE
17
18
  Requires-Dist: pandas>=2.0.0
@@ -19,6 +20,7 @@ Requires-Dist: numpy
19
20
  Requires-Dist: ruamel.yaml
20
21
  Requires-Dist: sqlalchemy
21
22
  Requires-Dist: psycopg2
23
+ Dynamic: license-file
22
24
 
23
25
  <table border=1 cellpadding=10>
24
26
  <tr>
@@ -75,3 +77,23 @@ then the system will send an email with the license file and a base64 key (Eithe
75
77
  - `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
76
78
 
77
79
  - `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
80
+
81
+ ### MetaData Initialization
82
+
83
+ Allowed instruments types are given in "Instrument Types" section of [meta_enumerations.md](doc/meta_enumerations.md).
84
+
85
+ ### Data Initialization for Instrument Type(s)
86
+ ```python
87
+ from trade_database_manager.manager import MetadataSql
88
+
89
+ metadatalib = MetadataSql()
90
+ metadatalib.initialize(for_inst_types="CB")
91
+ ```
92
+
93
+ This will try to create two tables, `instruments` and `instruments_cb` in the database if not yet exists. The `instruments` table will store the common information of all instruments, and the `instruments_cb` table will store the type-specific information of the instruments of type `CB`.
94
+
95
+ The table fields are listed in the [data_organization.md](doc/data_organization.md) file.
96
+
97
+
98
+
99
+
@@ -53,3 +53,23 @@ then the system will send an email with the license file and a base64 key (Eithe
53
53
  - `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
54
54
 
55
55
  - `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
56
+
57
+ ### MetaData Initialization
58
+
59
+ Allowed instruments types are given in "Instrument Types" section of [meta_enumerations.md](doc/meta_enumerations.md).
60
+
61
+ ### Data Initialization for Instrument Type(s)
62
+ ```python
63
+ from trade_database_manager.manager import MetadataSql
64
+
65
+ metadatalib = MetadataSql()
66
+ metadatalib.initialize(for_inst_types="CB")
67
+ ```
68
+
69
+ This will try to create two tables, `instruments` and `instruments_cb` in the database if not yet exists. The `instruments` table will store the common information of all instruments, and the `instruments_cb` table will store the type-specific information of the instruments of type `CB`.
70
+
71
+ The table fields are listed in the [data_organization.md](doc/data_organization.md) file.
72
+
73
+
74
+
75
+
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trade_database_manager"
7
- version = "0.0.6.dev0"
7
+ version = "0.0.8"
8
8
  description = "A wrapper of kdb and sql for convenient trade data management."
9
9
  readme = "README.md"
10
10
  authors = [
11
- {name = "Y.Q. Cui", email = "qianyun210603@hotmail.com"},
11
+ {name = "Y.Q. Tsui", email = "qianyun210603@hotmail.com"},
12
12
  ]
13
13
  dependencies = ["pandas>=2.0.0", "numpy", "ruamel.yaml", "sqlalchemy", "psycopg2"]
14
14
  classifiers = [
@@ -21,6 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.9",
22
22
  "Programming Language :: Python :: 3.10",
23
23
  "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
24
25
  ]
25
26
 
26
27
  [tool.setuptools.packages.find]
@@ -4,8 +4,10 @@
4
4
  # @File : kdbmanager.py
5
5
  # @Purpose :
6
6
  import os.path
7
+
7
8
  import pandas as pd
8
9
  import pykx
10
+
9
11
  from ...config import CONFIG
10
12
 
11
13
 
@@ -7,15 +7,30 @@
7
7
  import re
8
8
  from collections.abc import Container
9
9
  from functools import partial, reduce
10
- from typing import Any, Literal, Sequence, Union
10
+ from typing import Any, Callable, Literal, Sequence, Union
11
11
 
12
12
  import pandas as pd
13
- from sqlalchemy import Index, MetaData, Table, Column, create_engine, inspect, select, sql, text
14
- from sqlalchemy.types import TypeEngine
13
+ from sqlalchemy import (
14
+ Column,
15
+ Executable,
16
+ Index,
17
+ MetaData,
18
+ PrimaryKeyConstraint,
19
+ Table,
20
+ and_,
21
+ create_engine,
22
+ func,
23
+ inspect,
24
+ select,
25
+ text,
26
+ )
15
27
  from sqlalchemy.dialects.postgresql import insert
28
+ from sqlalchemy.orm import aliased, sessionmaker
29
+ from sqlalchemy.types import TypeEngine
16
30
 
17
31
  from ...config import CONFIG
18
32
  from ..typedefs import FILTERFIELD_TYPE, QUERYFIELD_TYPE
33
+ from .utils import infer_sql_type
19
34
 
20
35
 
21
36
  def _insert_on_conflict_update(table, conn, keys, data_iter, indexes):
@@ -38,10 +53,13 @@ class SqlManager:
38
53
  This class is used to manage SQL operations.
39
54
 
40
55
  :ivar sqlalchemy.engine.Engine engine: An instance of the SQLAlchemy Engine class for executing SQL operations.
56
+ :ivar sqlalchemy.orm.Session _session: An instance of the SQLAlchemy Session class for executing SQL operations.
57
+ :ivar sqlalchemy.engine.reflection.Inspector _inspector: An instance of the SQLAlchemy Inspector class for inspecting the database.
41
58
  """
42
59
 
43
60
  def __init__(self):
44
61
  self.engine = create_engine(CONFIG["sqlconnstr"])
62
+ self._session = sessionmaker(bind=self.engine)() # TODO: is it better to be a class attribute?
45
63
  self._inspector = None
46
64
 
47
65
  @property
@@ -50,7 +68,7 @@ class SqlManager:
50
68
  self._inspector = inspect(self.engine)
51
69
  return self._inspector
52
70
 
53
- def _execute(self, sql_executable: Union[str, sql.base.Executable]) -> Any:
71
+ def _execute(self, sql_executable: Union[str, Executable]) -> Any:
54
72
  if isinstance(sql_executable, str):
55
73
  sql_executable = text(sql_executable)
56
74
  with self.engine.begin() as conn:
@@ -134,9 +152,9 @@ class SqlManager:
134
152
  def create_table(
135
153
  self,
136
154
  table_name: str,
137
- table_columns: list[tuple[str, TypeEngine]],
155
+ table_columns: list[tuple[str, TypeEngine | type | tuple[type, tuple]]],
138
156
  unique_index_columns: Sequence[str] = (),
139
- primary_key: Union[str, set[str]] = set(),
157
+ primary_key: str | set[str] = set(),
140
158
  ):
141
159
  """
142
160
  Creates a table.
@@ -148,12 +166,16 @@ class SqlManager:
148
166
  :param unique_index_columns: Columns to enforce unique values on. Defaults to an empty sequence.
149
167
  :type unique_index_columns: Sequence[str]
150
168
  :param primary_key: The primary key(s) of the table. Defaults to an empty set.
151
- :type primary_key: Union[str,set[str]]
169
+ :type primary_key: Union[str, set[str|tuple[str]]]
170
+
152
171
  """
153
172
  table_meta = MetaData()
154
- if isinstance(primary_key, str):
155
- primary_key = {primary_key}
156
- columns = [Column(name, col_type, primary_key=name in primary_key) for name, col_type in table_columns]
173
+ columns: list[Column|PrimaryKeyConstraint] = [Column(name, infer_sql_type(col_type)) for name, col_type in table_columns]
174
+ if isinstance(primary_key, str) and primary_key != "":
175
+ columns.append(PrimaryKeyConstraint(primary_key))
176
+ elif primary_key:
177
+ columns.append(PrimaryKeyConstraint(*primary_key, name=f"pk_{table_name}"))
178
+
157
179
  table = Table(table_name, table_meta, *columns)
158
180
  table.create(self.engine)
159
181
  for column in unique_index_columns:
@@ -197,7 +219,7 @@ class SqlManager:
197
219
  """
198
220
  # check if any index is referring to the column
199
221
  sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
200
- index_refering_column = dict(self.engine.execute(sql_code).fetchall())
222
+ index_refering_column = dict(self._execute(sql_code).fetchall())
201
223
 
202
224
  # drop indexes referring to the column
203
225
  for index_name in index_refering_column:
@@ -219,8 +241,26 @@ class SqlManager:
219
241
  query_fields="*",
220
242
  start_time: pd.Timestamp = None,
221
243
  end_time: pd.Timestamp = None,
244
+ time_column: str = "timestamp",
222
245
  filter_fields=None,
223
246
  ):
247
+ """
248
+ Read data from a table within a time range.
249
+
250
+ :param table_name:
251
+ :type str
252
+ :param query_fields:
253
+ :type QUERYFIELD_TYPE
254
+ :param start_time:
255
+ :type pd.Timestamp
256
+ :param end_time:
257
+ :type pd.Timestamp
258
+ :param time_column:
259
+ :type str
260
+ :param filter_fields:
261
+ :type FILTERFIELD_TYPE
262
+ :return:
263
+ """
224
264
  meta = MetaData()
225
265
  table = Table(table_name, meta, autoload_with=self.engine)
226
266
 
@@ -233,9 +273,9 @@ class SqlManager:
233
273
 
234
274
  conditions = []
235
275
  if start_time:
236
- conditions.append(getattr(table.c, "timestamp", table.c.end_time) >= start_time)
276
+ conditions.append(getattr(table.c, time_column, table.c.end_time) >= start_time)
237
277
  if end_time:
238
- conditions.append(getattr(table.c, "timestamp", table.c.start_time) <= end_time)
278
+ conditions.append(getattr(table.c, time_column, table.c.start_time) <= end_time)
239
279
  if filter_fields:
240
280
  conditions.extend(
241
281
  [
@@ -247,11 +287,11 @@ class SqlManager:
247
287
  for field, filter_values in filter_fields.items()
248
288
  ]
249
289
  )
250
- stmt = stmt.where(sql.and_(*conditions))
290
+ stmt = stmt.where(and_(*conditions))
251
291
  res = self._execute(stmt)
252
292
  return pd.DataFrame(res.fetchall(), columns=res.keys())
253
293
 
254
- def read_data(self, table_name: str, query_fields: QUERYFIELD_TYPE = "*", filter_fields=None):
294
+ def read_data(self, table_name: str, query_fields: QUERYFIELD_TYPE = "*", filter_fields=None, unique=False):
255
295
  """
256
296
  Reads data from a table.
257
297
 
@@ -275,6 +315,9 @@ class SqlManager:
275
315
  query_fields = table
276
316
  stmt = select(table)
277
317
 
318
+ if unique:
319
+ stmt = stmt.distinct()
320
+
278
321
  if filter_fields:
279
322
  conditions = [
280
323
  (
@@ -284,7 +327,7 @@ class SqlManager:
284
327
  )
285
328
  for field, filter_values in filter_fields.items()
286
329
  ]
287
- stmt = stmt.where(sql.and_(*conditions))
330
+ stmt = stmt.where(and_(*conditions))
288
331
 
289
332
  # cannot use pandas.read_sql here as it discards timezone info
290
333
  res = self._execute(stmt)
@@ -296,6 +339,7 @@ class SqlManager:
296
339
  joined_columns: Sequence[str],
297
340
  query_fields: QUERYFIELD_TYPE = "*",
298
341
  filter_fields: FILTERFIELD_TYPE = None,
342
+ unique = False,
299
343
  ):
300
344
  """
301
345
  Reads data from multiple tables.
@@ -315,7 +359,7 @@ class SqlManager:
315
359
  tables = {table_name: Table(table_name, meta, autoload_with=self.engine) for table_name in table_names}
316
360
 
317
361
  joined_table = reduce(
318
- lambda x, y: x.join(y, sql.and_(*[x.columns[col] == y.columns[col] for col in joined_columns])),
362
+ lambda x, y: x.join(y, and_(*[x.columns[col] == y.columns[col] for col in joined_columns])),
319
363
  tables.values(),
320
364
  )
321
365
 
@@ -331,6 +375,9 @@ class SqlManager:
331
375
 
332
376
  stmt = select(*query_fields_rel).select_from(joined_table)
333
377
 
378
+ if unique:
379
+ stmt = stmt.distinct()
380
+
334
381
  if filter_fields:
335
382
  conditions = []
336
383
  for table_name, colnames in filter_fields.items():
@@ -342,55 +389,97 @@ class SqlManager:
342
389
  else table.columns[field] == filter_values
343
390
  )
344
391
 
345
- stmt = stmt.where(sql.and_(*conditions))
392
+ stmt = stmt.where(and_(*conditions))
346
393
 
347
394
  # cannot use pandas.read_sql here as it discards timezone info
348
395
  res = self._execute(stmt)
349
396
  return pd.DataFrame(res.fetchall(), columns=res.keys())
350
397
 
351
- def create_ts_table(
398
+ def _read_extremum_in_group(
352
399
  self,
353
400
  table_name: str,
354
- time_column: str,
355
- table_columns: dict[str, str],
356
- time_chunk_interval: str = "1 year",
357
- other_chunkers: dict[str, int] = None,
401
+ target_column: str | list[str],
402
+ group_column: str | list[str],
403
+ extremum_column: str,
404
+ method: str | Callable,
405
+ filter_fields=None,
358
406
  ):
359
- """
360
- Creates a table with a timestamp column.
407
+ if isinstance(target_column, str):
408
+ target_column = [target_column]
409
+ if isinstance(group_column, str):
410
+ group_column = [group_column]
411
+ target_column_c = [Column(col) for col in target_column]
412
+ group_column_c = [Column(col) for col in group_column]
413
+ extremum_column_c = Column(extremum_column)
361
414
 
362
- :param table_name: The name of the table to create.
363
- :type table_name: str
364
- :param time_column: The name of the timestamp column.
365
- :type time_column: str
366
- :param table_columns: The names of the columns to create with their data types. The keys are the column names and the values are the data types in PostgreSQL.
367
- :type table_columns: Dict[str, str]
368
- :param time_chunk_interval: The interval to chunk the time column by. Defaults to "1 year".
369
- :type time_chunk_interval: str
370
- :param other_chunkers: Other columns to chunk by. The keys are the column names and the values are the num of hashes. Defaults to None.
371
- :type other_chunkers: Dict[str, int], optional
372
- """
373
- query_create_sensordata_table = f"""
374
- CREATE TABLE {table_name} (
375
- {time_column} TIMESTAMP NOT NULL,
376
- {", ".join([f"{column_name} {column_type}{' NOT NULL' if column_name in other_chunkers else ''}" for column_name, column_type in table_columns.items()])}
377
- );
378
- """
379
- query_create_sensordata_hypertable = (
380
- f"SELECT create_hypertable('{table_name}', by_range('{time_column}', INTERVAL '{time_chunk_interval}'));"
415
+ table_meta = MetaData()
416
+
417
+ full_table = (
418
+ Table(table_name, table_meta, *target_column_c, *group_column_c)
419
+ if extremum_column in target_column
420
+ else Table(table_name, table_meta, *target_column_c, *group_column_c, extremum_column_c)
381
421
  )
382
- if other_chunkers is not None:
383
- for column_name, num_hash in other_chunkers.items():
384
- query_create_sensordata_hypertable += (
385
- f"\nSELECT add_dimension('{table_name}', by_hash('{column_name}', {num_hash}));"
422
+
423
+ if isinstance(method, str):
424
+ method = getattr(func, method)
425
+
426
+ if filter_fields:
427
+ conditions = [
428
+ (
429
+ full_table.columns[field].in_(filter_values)
430
+ if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
431
+ else full_table.columns[field] == filter_values
386
432
  )
433
+ for field, filter_values in filter_fields.items()
434
+ ]
435
+ subquery = (
436
+ self._session.query(*group_column_c, method(extremum_column_c).label("ColExtremum"))
437
+ .where(and_(*conditions))
438
+ .group_by(*group_column_c)
439
+ .subquery()
440
+ )
441
+ else:
442
+ subquery = (
443
+ self._session.query(*group_column_c, method(extremum_column_c).label("ColExtremum"))
444
+ .group_by(*group_column_c)
445
+ .subquery()
446
+ )
447
+
448
+ t_full = aliased(full_table)
449
+ t_sub = aliased(subquery)
450
+
451
+ cols_res = [getattr(t_full.c, col) for col in group_column + target_column]
452
+
453
+ query = self._session.query(*cols_res).join(
454
+ t_sub,
455
+ and_(
456
+ getattr(t_full.c, extremum_column) == t_sub.c.ColExtremum,
457
+ *[getattr(t_full.c, col) == getattr(t_sub.c, col) for col in group_column],
458
+ ),
459
+ )
460
+
461
+ return pd.DataFrame(query.all(), columns=group_column + target_column)
387
462
 
388
- uidx_name = f"uix_{table_name}_{'_'.join(other_chunkers.keys())}_{time_column}"
389
- query_unique_index = (
390
- f"CREATE UNIQUE INDEX {uidx_name} ON {table_name} ({', '.join(other_chunkers.keys())}, {time_column});"
463
+ def read_max_in_group(
464
+ self,
465
+ table_name: str,
466
+ target_column: str | list[str],
467
+ group_column: str | list[str],
468
+ extremum_column: str,
469
+ filter_fields=None,
470
+ ):
471
+ return self._read_extremum_in_group(
472
+ table_name, target_column, group_column, extremum_column, "max", filter_fields
473
+ )
474
+
475
+ def read_min_in_group(
476
+ self,
477
+ table_name: str,
478
+ target_column: str | list[str],
479
+ group_column: str | list[str],
480
+ extremum_column: str,
481
+ filter_fields=None,
482
+ ):
483
+ return self._read_extremum_in_group(
484
+ table_name, target_column, group_column, extremum_column, "min", filter_fields
391
485
  )
392
- with self.engine.connect() as conn:
393
- conn.execute(text(query_create_sensordata_table))
394
- conn.execute(text(query_create_sensordata_hypertable))
395
- conn.execute(text(query_unique_index))
396
- conn.commit()
@@ -0,0 +1,35 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/10/31 21:07
3
+ # @Author : YQ Tsui
4
+ # @File : utils.py
5
+ # @Purpose : utility functions for sqlmanager
6
+
7
+ from datetime import date, datetime
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from sqlalchemy.types import DOUBLE_PRECISION, Date, DateTime, Integer, String, Text, TypeEngine
12
+
13
+
14
+ def infer_sql_type(input_type):
15
+ if isinstance(input_type, TypeEngine):
16
+ return input_type
17
+ if isinstance(input_type, (tuple, list)):
18
+ input_type, *args = input_type
19
+ else:
20
+ args = ()
21
+ if issubclass(input_type, str):
22
+ if args:
23
+ return String(*args)
24
+ return Text()
25
+ if issubclass(input_type, (int, np.signedinteger)):
26
+ return Integer()
27
+ if issubclass(input_type, (np.floating, float)):
28
+ return DOUBLE_PRECISION()
29
+ if issubclass(input_type, (bool, np.bool_)):
30
+ return Integer()
31
+ if issubclass(input_type, (np.datetime64, pd.Timestamp, datetime)):
32
+ return Date() if args and args[0] else DateTime()
33
+ if issubclass(input_type, date):
34
+ return Date()
35
+ raise ValueError(f"Unsupported type {input_type}")
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/4/21 12:41
3
+ # @Author : YQ Tsui
4
+ # @File : __init__.py
5
+ # @Purpose :
6
+
7
+ from .metadata_sql import MetadataSql
8
+ from .metadata_sql_cb import CBMetadataSql
9
+ from .metadata_sql_fut import FutMetadataSql
10
+
11
+ __all__ = ("MetadataSql", "CBMetadataSql", "FutMetadataSql")
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/4/19 16:44
3
+ # @Author : YQ Tsui
4
+ # @File : fields_data_type.py
5
+ # @Purpose :
6
+
7
+ from sqlalchemy import DOUBLE_PRECISION, Date, Integer, String, Text, DateTime, CHAR
8
+
9
+ FIELD_DATA_TYPE_SQL = {
10
+ "ticker": String(20),
11
+ "name": String(128),
12
+ "trading_code": String(20),
13
+ "inst_type": String(20),
14
+ "currency": String(6),
15
+ "exchange": String(10),
16
+ "timezone": String(30),
17
+ "tick_size": DOUBLE_PRECISION(),
18
+ "lot_size": DOUBLE_PRECISION(),
19
+ "min_lots": DOUBLE_PRECISION(),
20
+ "market_tplus": Integer(),
21
+ "listed_date": Date(),
22
+ "delisted_date": Date(),
23
+ "country": String(6),
24
+ "state": String(36),
25
+ # STK
26
+ "sector": String(30),
27
+ "industry": String(36),
28
+ "board_type": String(200),
29
+ # LOF & ETF
30
+ "issuer": String(60),
31
+ "current_mgr": String(60),
32
+ "custodian": String(60),
33
+ "issuer_country": String(6),
34
+ "fund_type": String(20),
35
+ "benchmark": String(60),
36
+ # Convertible Bond (CB)
37
+ "stock_ticker": String(20),
38
+ "stock_exchange": String(10),
39
+ "maturity_date": Date(),
40
+ "issue_price": DOUBLE_PRECISION(),
41
+ "total_issue_size": DOUBLE_PRECISION(),
42
+ "par_value": DOUBLE_PRECISION(),
43
+ "redeem_price": DOUBLE_PRECISION(),
44
+ "conversion_start_date": Date(),
45
+ "conversion_end_date": Date(),
46
+ "callback_terms": Text(),
47
+ "callback_type": String(20),
48
+ "adjust_terms": Text(),
49
+ "adjust_type": String(20),
50
+ "putback_terms": Text(),
51
+ "putback_type": String(20),
52
+ "callback_level": DOUBLE_PRECISION(),
53
+ # Futures (FUT)
54
+ "contract_unit": String(20),
55
+ "contract_multiplier": DOUBLE_PRECISION(),
56
+ "expiry_time": DateTime(),
57
+ "delivery_date": Date(),
58
+ "settlement_method": String(20),
59
+ "underlying_code": String(20),
60
+ "underlying_exchange": String(10),
61
+ "underlying_type": String(20),
62
+ "margin_method": String(20),
63
+ # Options (OPT), mainly similar to Futures
64
+ "option_type": String(20), # 'C' for Call, 'P' for Put
65
+ "strike": DOUBLE_PRECISION(),
66
+ "exercise_style": String(20), # 'A' for American, 'E' for European
67
+ }
68
+
69
+ DATE_TIME_COLS = {"listed_date", "delisted_date", "maturity_date", "conversion_start_date", "conversion_end_date", "expiry_date", "delivery_date"}
70
+
71
+ BASE_COLUMNS = [("ticker", String(36)), ("exchange", String(10))]
@@ -10,9 +10,8 @@ from typing import Union, cast
10
10
  import pandas as pd
11
11
 
12
12
  from ..core.sql.sqlmanager import SqlManager
13
+ from .fields_data_type import BASE_COLUMNS, DATE_TIME_COLS, FIELD_DATA_TYPE_SQL
13
14
  from .typedefs import EXCHANGE_LITERALS, INST_TYPE_LITERALS, Opt_T_SeqT, T_DictT
14
- from .fields_data_type import FIELD_DATA_TYPE_SQL, String
15
-
16
15
 
17
16
  COMMON_METADATA_COLUMNS = [
18
17
  "name",
@@ -31,6 +30,40 @@ TYPE_METADATA_COLUMNS = {
31
30
  "STK": ["country", "state", "board_type", "issue_price"],
32
31
  "ETF": ["issuer", "current_mgr", "custodian", "issuer_country", "fund_type", "benchmark"],
33
32
  "LOF": ["issuer", "current_mgr", "custodian", "issuer_country", "fund_type", "benchmark"],
33
+ "CB": [
34
+ "country",
35
+ "state",
36
+ "stock_ticker",
37
+ "stock_exchange",
38
+ "maturity_date",
39
+ "issue_price",
40
+ "total_issue_size",
41
+ "par_value",
42
+ "redemption_price",
43
+ "conversion_start_date",
44
+ "conversion_end_date",
45
+ "callback_terms",
46
+ "callback_type",
47
+ "adjust_terms",
48
+ "adjust_type",
49
+ "putback_terms",
50
+ "putback_type",
51
+ ],
52
+ "FUT": ["contract_unit", "contract_multiplier", "expiry_time", "delivery_date", "settlement_method", "underlying_code", "underlying_exchange", "underlying_type", "margin_method", "sector"],
53
+ "OPT": [
54
+ "strike",
55
+ "option_type",
56
+ "exercise_style",
57
+ "contract_unit",
58
+ "contract_multiplier",
59
+ "expiry_time",
60
+ "delivery_date",
61
+ "settlement_method",
62
+ "underlying_code",
63
+ "underlying_exchange",
64
+ "underlying_type",
65
+ "margin_method",
66
+ ],
34
67
  }
35
68
 
36
69
 
@@ -55,18 +88,22 @@ class MetadataSql:
55
88
  for_inst_types = list(TYPE_METADATA_COLUMNS.keys())
56
89
  elif isinstance(for_inst_types, str):
57
90
  for_inst_types = [for_inst_types]
58
- columns_base = [("ticker", String(10)), ("exchange", String(10))]
59
91
  if not self._manager.table_exists("instruments"):
60
- columns = columns_base + [(col, FIELD_DATA_TYPE_SQL.get(col, String())) for col in COMMON_METADATA_COLUMNS]
92
+ columns = BASE_COLUMNS + [(col, FIELD_DATA_TYPE_SQL[col]) for col in COMMON_METADATA_COLUMNS]
61
93
  self._manager.create_table("instruments", columns, {"primary_key": ["ticker", "exchange"]})
62
94
  for inst_type in for_inst_types:
63
95
  if not self._manager.table_exists(f"instruments_{inst_type.lower()}"):
64
- columns = columns_base + [
65
- (col, FIELD_DATA_TYPE_SQL.get(col, String())) for col in TYPE_METADATA_COLUMNS[inst_type]
66
- ]
67
- self._manager.create_table(
68
- f"instruments_{inst_type.lower()}", columns, primary_key={"ticker", "exchange"}
69
- )
96
+ if inst_type not in TYPE_METADATA_COLUMNS:
97
+ raise ValueError(f"Invalid instrument type: {inst_type}")
98
+ if inst_type in {"FUT", "OPT"}:
99
+ columns = BASE_COLUMNS + [(col, FIELD_DATA_TYPE_SQL[col]) for col in (COMMON_METADATA_COLUMNS + TYPE_METADATA_COLUMNS[inst_type])]
100
+ self._manager.create_table(
101
+ f"instruments_{inst_type.lower()}", columns, primary_key={"ticker", "exchange"})
102
+ else:
103
+ columns = BASE_COLUMNS + [(col, FIELD_DATA_TYPE_SQL[col]) for col in TYPE_METADATA_COLUMNS[inst_type]]
104
+ self._manager.create_table(
105
+ f"instruments_{inst_type.lower()}", columns, primary_key={"ticker", "exchange"}
106
+ )
70
107
 
71
108
  def update_instrument_metadata(self, data: Union[pd.DataFrame, list[dict], dict]):
72
109
  """
@@ -75,11 +112,8 @@ class MetadataSql:
75
112
  :param data: The data to be updated. It can be a DataFrame, a list of dictionaries, or a single dictionary.
76
113
  :type data: Union[pd.DataFrame, list[dict], dict]
77
114
  """
78
- type_specific_columns = set(data.columns) - set(COMMON_METADATA_COLUMNS)
79
- if "inst_type" not in data.columns and bool(type_specific_columns):
80
- raise ValueError(
81
- f"Non-common columns found ({','.join(type_specific_columns)}) but inst_type column not provided."
82
- )
115
+ if "inst_type" not in data.columns:
116
+ raise ValueError("`inst_type` column not provided.")
83
117
  if isinstance(data, dict):
84
118
  data = [data]
85
119
  if isinstance(data, list):
@@ -88,6 +122,19 @@ class MetadataSql:
88
122
  data.set_index(["ticker", "exchange"], inplace=True)
89
123
  else:
90
124
  assert set(data.index.names) == {"ticker", "exchange"}, "Index names must be 'ticker' and 'exchange'."
125
+
126
+ if "FUT" in data["inst_type"].unique():
127
+ columns = data.columns.intersection(COMMON_METADATA_COLUMNS + TYPE_METADATA_COLUMNS["FUT"])
128
+ data_fut = data.loc[data["inst_type"] == "FUT", columns]
129
+ self._manager.insert("instruments_fut", data_fut, upsert=True)
130
+ data.drop(index = data[data["inst_type"] == "FUT"].index, inplace=True)
131
+
132
+ if "OPT" in data["inst_type"].unique():
133
+ columns = data.columns.intersection(COMMON_METADATA_COLUMNS + TYPE_METADATA_COLUMNS["OPT"])
134
+ data_opt = data.loc[data["inst_type"] == "OPT", columns]
135
+ self._manager.insert("instruments_opt", data_opt, upsert=True)
136
+ data.drop(index = data[data["inst_type"] == "OPT"].index, inplace=True)
137
+
91
138
  data_common = data[data.columns.intersection(COMMON_METADATA_COLUMNS)]
92
139
 
93
140
  self._manager.insert("instruments", data_common, upsert=True)
@@ -99,11 +146,13 @@ class MetadataSql:
99
146
  if not columns.empty:
100
147
  self._manager.insert(f"instruments_{inst_type.lower()}", data_type_df[columns], upsert=True)
101
148
 
102
- def _convert_datetime_columns(self, data: pd.DataFrame):
103
- for col in ["listed_date", "delisted_date"]:
149
+ @staticmethod
150
+ def _convert_datetime_columns(data: pd.DataFrame):
151
+ for col in DATE_TIME_COLS:
104
152
  if col in data.columns:
105
153
  data[col] = data[col].apply(pd.to_datetime)
106
154
 
155
+ # TODO: Add support for Futures and Options as they don't use the common metadata columns
107
156
  def read_metadata(
108
157
  self,
109
158
  ticker: Opt_T_SeqT[str] = None,
@@ -213,6 +262,8 @@ class MetadataSql:
213
262
 
214
263
  if query_fields == "*":
215
264
  query_fields_cross = "*"
265
+ query_fields_type = []
266
+ query_fields_common = query_fields
216
267
  else:
217
268
  query_fields_common = ["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
218
269
  query_fields_type = [f for f in query_fields if f in TYPE_METADATA_COLUMNS.get(inst_type, [])]
@@ -226,7 +277,15 @@ class MetadataSql:
226
277
  }
227
278
  filter_fields_type = {k: v for k, v in filter_fields.items() if k in TYPE_METADATA_COLUMNS.get(inst_type, [])}
228
279
 
229
- if (query_fields != "*" or inst_type not in TYPE_METADATA_COLUMNS) and not bool(query_fields_type) and not bool(filter_fields_type):
280
+ if inst_type in {"FUT", "OPT"}:
281
+ df = self._manager.read_data(
282
+ f"instruments_{inst_type.lower()}", query_fields=query_fields, filter_fields=filter_fields
283
+ )
284
+ elif (
285
+ (query_fields != "*" or inst_type not in TYPE_METADATA_COLUMNS)
286
+ and not bool(query_fields_type)
287
+ and not bool(filter_fields_type)
288
+ ):
230
289
  df = self._manager.read_data(
231
290
  "instruments", query_fields=query_fields_common, filter_fields=filter_fields_common
232
291
  )
@@ -0,0 +1,109 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/11/12 13:49
3
+ # @Author : YQ Tsui
4
+ # @File : metadata_sql_cb.py
5
+ # @Purpose :
6
+
7
+ import pandas as pd
8
+
9
+ from .metadata_sql import MetadataSql
10
+ from .typedefs import EXCHANGE_LITERALS, Opt_T_SeqT, T_SeqT
11
+
12
+
13
+ class CBMetadataSql(MetadataSql):
14
+
15
+ def read_latest_conversion_price(
16
+ self,
17
+ fields: T_SeqT[str] = ("conversion_price", "effective_date"),
18
+ tickers: Opt_T_SeqT[str] = None,
19
+ exchanges: Opt_T_SeqT[EXCHANGE_LITERALS] = None,
20
+ latest_by: str = "announcement_date",
21
+ ) -> pd.DataFrame:
22
+ """
23
+ Reads the convert price metadata from the database.
24
+
25
+ :param fields: The fields to query.
26
+ :type fields: T_SeqT[str]
27
+ :param tickers: The tickers to query, None for all.
28
+ :type tickers: Opt_T_SeqT[str]
29
+ :param exchanges: The exchanges to query, None for all.
30
+ :type exchanges: Opt_T_SeqT[EXCHANGE_LITERALS]
31
+ :param latest_by: The field to use for latest by.
32
+ :type latest_by: str
33
+ """
34
+ if isinstance(fields, str):
35
+ fields = [fields]
36
+ elif isinstance(fields, tuple):
37
+ fields = list(fields)
38
+ if tickers is None and exchanges is None:
39
+ filter_fields = None
40
+ else:
41
+ filter_fields = {}
42
+ if tickers is not None:
43
+ filter_fields["ticker"] = tickers
44
+ if exchanges is not None:
45
+ filter_fields["exchange"] = exchanges
46
+
47
+ df = self._manager.read_max_in_group(
48
+ "cb_convert_price_history", fields, ["ticker", "exchange"], latest_by, filter_fields=filter_fields
49
+ )
50
+ return df.set_index(["ticker", "exchange"])
51
+
52
+ def update_conversion_price(self, data: pd.DataFrame):
53
+ """
54
+ Updates the convert price metadata in the database.
55
+
56
+ :param data: The data to update.
57
+ :type data: pd.DataFrame
58
+ """
59
+ self._manager.insert("cb_convert_price_history", data, upsert=True)
60
+
61
+ def read_bond_coupon(
62
+ self, fields: T_SeqT[str], tickers: Opt_T_SeqT[str] = None, exchanges: Opt_T_SeqT[EXCHANGE_LITERALS] = None
63
+ ) -> pd.DataFrame:
64
+ """
65
+ Reads the convert coupon metadata from the database.
66
+
67
+ :param fields: The fields to query. available fields: ["pay_date", "coupon", "coupon_type", "period_start", "period_end", "remaining_principle", "principle_repayment"]
68
+ :type fields: T_SeqT[str]
69
+ :param tickers: The tickers to query, None for all.
70
+ :type tickers: Opt_T_SeqT[str]
71
+ :param exchanges: The exchanges to query, None for all.
72
+ :type exchanges: Opt_T_SeqT[EXCHANGE_LITERALS]
73
+ """
74
+ filter_fields = {}
75
+ if tickers is not None:
76
+ filter_fields["ticker"] = tickers
77
+ if exchanges is not None:
78
+ filter_fields["exchange"] = exchanges
79
+
80
+ df = self._manager.read_data("cb_coupon_schedule", query_fields=fields, filter_fields=filter_fields)
81
+ return df.set_index(["ticker", "exchange"])
82
+
83
+ def update_bond_coupon(self, data):
84
+ """
85
+ Updates the convert coupon metadata in the database.
86
+
87
+ :param data: The data to update.
88
+ :type data: pd.DataFrame
89
+ """
90
+ self._manager.insert("cb_coupon_schedule", data, upsert=True)
91
+
92
+ def update_convert_bond_cashflow(self, data):
93
+ """
94
+ Updates the convert bond cashflow metadata in the database.
95
+
96
+ :param data: The data to update.
97
+ :type data: pd.DataFrame
98
+ """
99
+ self._manager.insert("cb_realized_cash_flow", data, upsert=True)
100
+
101
+ def update_auxiliary(self, auxiliary_type: str, data: pd.DataFrame):
102
+ """
103
+ Updates the auxiliary data in the database.
104
+
105
+ :param auxiliary_type: ["RangerBinary"]
106
+ :param data: The data to update.
107
+ :type data: pd.DataFrame
108
+ """
109
+ self._manager.insert("cb_auxiliary_" + auxiliary_type, data, upsert=True)
@@ -0,0 +1,43 @@
1
+ # -*- coding: utf-8 -*-
2
+ # @Time : 2024/11/19 17:42
3
+ # @Author : YQ Tsui
4
+ # @File : metadata_sql_fut.py
5
+ # @Purpose :
6
+
7
+ import pandas as pd
8
+
9
+ from .metadata_sql import MetadataSql
10
+ from .typedefs import EXCHANGE_LITERALS, Opt_T_SeqT, T_SeqT
11
+
12
+ class FutMetadataSql(MetadataSql):
13
+
14
+ def get_all_underlying_codes(self) -> pd.Series:
15
+ """
16
+ Get all underlying codes
17
+ """
18
+ return self._manager.read_data("instruments_fut", ["underlying_code"], unique=True)
19
+
20
+ def read_latest_daily_auxiliary_data(self, fields: T_SeqT[str] = "date",
21
+ tickers: Opt_T_SeqT[str] = None,
22
+ exchanges: Opt_T_SeqT[EXCHANGE_LITERALS] = None,
23
+ ) -> pd.DataFrame:
24
+ """
25
+ Read latest daily auxiliary data
26
+ """
27
+ if isinstance(fields, str):
28
+ fields = [fields]
29
+ elif isinstance(fields, tuple):
30
+ fields = list(fields)
31
+ if tickers is None and exchanges is None:
32
+ filter_fields = None
33
+ else:
34
+ filter_fields = {}
35
+ if tickers is not None:
36
+ filter_fields["ticker"] = tickers
37
+ if exchanges is not None:
38
+ filter_fields["exchange"] = exchanges
39
+
40
+ df = self._manager.read_max_in_group(
41
+ "fut_daily_auxiliary_data", fields, ["underlying_code", "exchange"], "date", filter_fields=filter_fields
42
+ )
43
+ return df.set_index(["underlying_code", "exchange"])
@@ -6,7 +6,7 @@
6
6
 
7
7
  from typing import Dict, Literal, Optional, Sequence, TypeVar, Union
8
8
 
9
- INST_TYPE_LITERALS = Literal["STK", "FUT", "OPT", "IDX", "ETF", "FND", "BND", "CASH", "CRYPTO"]
9
+ INST_TYPE_LITERALS = Literal["STK", "FUT", "OPT", "IDX", "ETF", "LOF", "FUND", "BOND", "CASH", "CRYPTO", "CB"]
10
10
  EXCHANGE_LITERALS = Literal[
11
11
  "SSE",
12
12
  "SZSE",
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: trade_database_manager
3
- Version: 0.0.6.dev0
3
+ Version: 0.0.8
4
4
  Summary: A wrapper of kdb and sql for convenient trade data management.
5
- Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
5
+ Author-email: "Y.Q. Tsui" <qianyun210603@hotmail.com>
6
6
  Classifier: Operating System :: POSIX :: Linux
7
7
  Classifier: Operating System :: Microsoft :: Windows
8
8
  Classifier: Development Status :: 3 - Alpha
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
15
16
  Description-Content-Type: text/markdown
16
17
  License-File: LICENSE
17
18
  Requires-Dist: pandas>=2.0.0
@@ -19,6 +20,7 @@ Requires-Dist: numpy
19
20
  Requires-Dist: ruamel.yaml
20
21
  Requires-Dist: sqlalchemy
21
22
  Requires-Dist: psycopg2
23
+ Dynamic: license-file
22
24
 
23
25
  <table border=1 cellpadding=10>
24
26
  <tr>
@@ -75,3 +77,23 @@ then the system will send an email with the license file and a base64 key (Eithe
75
77
  - `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
76
78
 
77
79
  - `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
80
+
81
+ ### MetaData Initialization
82
+
83
+ Allowed instruments types are given in "Instrument Types" section of [meta_enumerations.md](doc/meta_enumerations.md).
84
+
85
+ ### Data Initialization for Instrument Type(s)
86
+ ```python
87
+ from trade_database_manager.manager import MetadataSql
88
+
89
+ metadatalib = MetadataSql()
90
+ metadatalib.initialize(for_inst_types="CB")
91
+ ```
92
+
93
+ This will try to create two tables, `instruments` and `instruments_cb` in the database if not yet exists. The `instruments` table will store the common information of all instruments, and the `instruments_cb` table will store the type-specific information of the instruments of type `CB`.
94
+
95
+ The table fields are listed in the [data_organization.md](doc/data_organization.md) file.
96
+
97
+
98
+
99
+
@@ -16,7 +16,10 @@ trade_database_manager/core/sql/__init__.py
16
16
  trade_database_manager/core/sql/sqlmanager.py
17
17
  trade_database_manager/core/sql/sqlreader.py
18
18
  trade_database_manager/core/sql/sqlwriter.py
19
+ trade_database_manager/core/sql/utils.py
19
20
  trade_database_manager/manager/__init__.py
20
21
  trade_database_manager/manager/fields_data_type.py
21
22
  trade_database_manager/manager/metadata_sql.py
23
+ trade_database_manager/manager/metadata_sql_cb.py
24
+ trade_database_manager/manager/metadata_sql_fut.py
22
25
  trade_database_manager/manager/typedefs.py
@@ -1,9 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # @Time : 2024/4/21 12:41
3
- # @Author : YQ Tsui
4
- # @File : __init__.py
5
- # @Purpose :
6
-
7
- from .metadata_sql import MetadataSql
8
-
9
- __all__ = ("MetadataSql",)
@@ -1,33 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # @Time : 2024/4/19 16:44
3
- # @Author : YQ Tsui
4
- # @File : fields_data_type.py
5
- # @Purpose :
6
-
7
- from sqlalchemy import DOUBLE_PRECISION, Integer, String, Date
8
-
9
- FIELD_DATA_TYPE_SQL = {
10
- "ticker": String(20),
11
- "name": String(20),
12
- "currency": String(6),
13
- "exchange": String(10),
14
- "timezone": String(30),
15
- "tick_size": DOUBLE_PRECISION(),
16
- "lot_size": DOUBLE_PRECISION(),
17
- "min_lots": DOUBLE_PRECISION(),
18
- "market_tplus": Integer(),
19
- "listed_date": Date(),
20
- "delisted_date": Date(),
21
- "country": String(6),
22
- # STK
23
- "sector": String(30),
24
- "industry": String(36),
25
- "board_type": String(200),
26
- # LOF & ETF
27
- "issuer": String(60),
28
- "current_mgr": String(60),
29
- "custodian": String(60),
30
- "issuer_country": String(6),
31
- "fund_type": String(20),
32
- "benchmark": String(60),
33
- }