ib-connect 0.2.0__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 (28) hide show
  1. ib_connect-0.2.0/LICENSE +21 -0
  2. ib_connect-0.2.0/PKG-INFO +151 -0
  3. ib_connect-0.2.0/README.md +134 -0
  4. ib_connect-0.2.0/ib_connect/__init__.py +1 -0
  5. ib_connect-0.2.0/ib_connect/gui/__init__.py +1 -0
  6. ib_connect-0.2.0/ib_connect/gui/app.py +153 -0
  7. ib_connect-0.2.0/ib_connect/shared/__init__.py +1 -0
  8. ib_connect-0.2.0/ib_connect/shared/ib_connection.py +34 -0
  9. ib_connect-0.2.0/ib_connect/skills/__init__.py +1 -0
  10. ib_connect-0.2.0/ib_connect/skills/data_upload/__init__.py +1 -0
  11. ib_connect-0.2.0/ib_connect/skills/data_upload/data_upload.py +371 -0
  12. ib_connect-0.2.0/ib_connect/skills/ib_download/__init__.py +1 -0
  13. ib_connect-0.2.0/ib_connect/skills/ib_download/download.py +63 -0
  14. ib_connect-0.2.0/ib_connect/skills/ib_download/download_service.py +308 -0
  15. ib_connect-0.2.0/ib_connect/skills/ib_download/ib_download.py +188 -0
  16. ib_connect-0.2.0/ib_connect/skills/ib_download/job_queue.py +88 -0
  17. ib_connect-0.2.0/ib_connect/skills/ib_query/__init__.py +1 -0
  18. ib_connect-0.2.0/ib_connect/skills/ib_query/ib_query.py +62 -0
  19. ib_connect-0.2.0/ib_connect/skills/ib_query/query.py +160 -0
  20. ib_connect-0.2.0/ib_connect.egg-info/PKG-INFO +151 -0
  21. ib_connect-0.2.0/ib_connect.egg-info/SOURCES.txt +26 -0
  22. ib_connect-0.2.0/ib_connect.egg-info/dependency_links.txt +1 -0
  23. ib_connect-0.2.0/ib_connect.egg-info/entry_points.txt +3 -0
  24. ib_connect-0.2.0/ib_connect.egg-info/requires.txt +5 -0
  25. ib_connect-0.2.0/ib_connect.egg-info/top_level.txt +1 -0
  26. ib_connect-0.2.0/pyproject.toml +26 -0
  27. ib_connect-0.2.0/setup.cfg +4 -0
  28. ib_connect-0.2.0/setup.py +2 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mats Bengtsson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: ib_connect
3
+ Version: 0.2.0
4
+ Summary: Modular Interactive Brokers integration toolkit — query contracts, download historical data, and upload to TimescaleDB.
5
+ Author-email: Mats Bengtsson <mats.bengtsson@outlook.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/alacazar/ib-connect
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: ib_insync
12
+ Requires-Dist: pandas
13
+ Requires-Dist: psycopg2-binary
14
+ Requires-Dist: watchdog
15
+ Requires-Dist: flask
16
+ Dynamic: license-file
17
+
18
+ # IB Connect
19
+
20
+ A modular Python toolkit for Interactive Brokers integration — query contracts, download historical market data, and upload to a TimescaleDB database.
21
+
22
+ ## Architecture
23
+
24
+ ```
25
+ ib_connect/
26
+ ├── shared/ # IB connection management (shared by all skills)
27
+ ├── skills/
28
+ │ ├── ib_query/ # CLI: qualify and inspect contracts
29
+ │ ├── ib_download/ # CLI + service: async historical data downloader
30
+ │ └── data_upload/ # Service: watch folder → PostgreSQL/TimescaleDB
31
+ └── gui/ # Flask web UI
32
+ ```
33
+
34
+ ## Prerequisites
35
+
36
+ - Python 3.9+
37
+ - [IB Gateway](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) or TWS running with API connections enabled
38
+ - Paper trading port: `7497` (default)
39
+ - Live trading port: `7496`
40
+ - For `data_upload` and `gui`: PostgreSQL with [TimescaleDB](https://www.timescale.com/)
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ git clone https://github.com/alacazar/ib-connect.git
46
+ cd ib_connect
47
+ pip install -e .
48
+ ```
49
+
50
+ The `ib-query` and `ib-download` CLI commands are registered as entry points after installation.
51
+
52
+ ## Configuration
53
+
54
+ The `ib_download` and `gui` skills require a `config.json`. Copy the provided examples and customize:
55
+
56
+ ```bash
57
+ cp ib_connect/skills/ib_download/config.example.json ib_connect/skills/ib_download/config.json
58
+ cp ib_connect/gui/config.example.json ib_connect/gui/config.json
59
+ ```
60
+
61
+ For `data_upload`, set the `PG_URI` environment variable:
62
+
63
+ ```bash
64
+ export PG_URI="postgresql://user:password@localhost:5432/dbname"
65
+ ```
66
+
67
+ ## Skills
68
+
69
+ ### ib-query
70
+
71
+ Query and qualify contract details from IB.
72
+
73
+ ```bash
74
+ ib-query --symbol AAPL --sec-type STK
75
+ ```
76
+
77
+ Output:
78
+ ```json
79
+ {"conId": 265598, "symbol": "AAPL", "secType": "STK", "exchange": "SMART", "currency": "USD", "fullName": "APPLE INC"}
80
+ ```
81
+
82
+ See [ib_connect/skills/ib_query/SKILL.md](ib_connect/skills/ib_query/SKILL.md) for all options.
83
+
84
+ ---
85
+
86
+ ### ib-download
87
+
88
+ Downloads historical OHLCV data from IB and writes CSV files. Consists of two parts:
89
+
90
+ **`download_service.py` — the background service**
91
+
92
+ IB enforces strict pacing limits: it throttles connections that issue too many data requests too quickly, and it only allows one active connection per client ID. Running a dedicated background service solves both problems — it owns a single, persistent IB connection and processes jobs one at a time, sleeping 20 seconds between data chunks to stay within IB's limits. Any number of callers can submit jobs without worrying about pacing or connection conflicts.
93
+
94
+ Start it once and keep it running (e.g. in `screen` or `tmux`, or as a system service):
95
+
96
+ ```bash
97
+ python ib_connect/skills/ib_download/download_service.py
98
+ ```
99
+
100
+ Long date ranges are split automatically into chunks sized to IB's per-request limits. Progress and errors are logged to `download_service.log` alongside the script. A PID lock file prevents duplicate instances.
101
+
102
+ **`ib-download` — the CLI client**
103
+
104
+ Submits a job to the service's SQLite queue and returns a `job_key` immediately. The caller can check status later without staying connected.
105
+
106
+ ```bash
107
+ # Submit a job
108
+ ib-download -c 265598 -s 2023-01-01 -e 2023-12-31 -b "1 min"
109
+
110
+ # Check status
111
+ ib-download --status -k <job_key>
112
+ ```
113
+
114
+ On completion the service can notify an agent via webhook (configure `webhook_url` and `token` in `config.json`).
115
+
116
+ See [ib_connect/skills/ib_download/SKILL.md](ib_connect/skills/ib_download/SKILL.md) for all options including batch mode and output format.
117
+
118
+ ---
119
+
120
+ ### data-upload
121
+
122
+ A file-watching service that validates and uploads contract JSON files and OHLCV CSV files to PostgreSQL/TimescaleDB. Drop files into the configured `input_folder` and they are processed automatically — contracts go to `finance.contracts`, OHLCV data to `finance.ohlcv_1m` or `finance.ohlcv_1d`. Processed files are moved to `processed/`; failures go to `errors/`.
123
+
124
+ ```bash
125
+ python ib_connect/skills/data_upload/data_upload.py
126
+ ```
127
+
128
+ Expected filename pattern for OHLCV: `<symbol>.<conid>.<barsize>.csv`.
129
+
130
+ See [ib_connect/skills/data_upload/SKILL.md](ib_connect/skills/data_upload/SKILL.md) for schema details.
131
+
132
+ ---
133
+
134
+ ### GUI
135
+
136
+ A Flask web interface that ties the toolkit together. From the browser you can:
137
+
138
+ - **Query contracts** — search by symbol and security type, inspect the qualified contract details returned by IB
139
+ - **Save contracts** — export selected contracts as JSON to the `data_upload` input folder, so they are picked up and stored in PostgreSQL automatically
140
+ - **Submit download jobs** — configure date range and bar size per contract and enqueue jobs with the download service
141
+ - **Monitor jobs** — check status and cancel pending jobs
142
+
143
+ ```bash
144
+ python ib_connect/gui/app.py
145
+ ```
146
+
147
+ Opens on `http://127.0.0.1:5000` by default (configurable in `gui/config.json`).
148
+
149
+ ## License
150
+
151
+ [MIT](LICENSE)
@@ -0,0 +1,134 @@
1
+ # IB Connect
2
+
3
+ A modular Python toolkit for Interactive Brokers integration — query contracts, download historical market data, and upload to a TimescaleDB database.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ib_connect/
9
+ ├── shared/ # IB connection management (shared by all skills)
10
+ ├── skills/
11
+ │ ├── ib_query/ # CLI: qualify and inspect contracts
12
+ │ ├── ib_download/ # CLI + service: async historical data downloader
13
+ │ └── data_upload/ # Service: watch folder → PostgreSQL/TimescaleDB
14
+ └── gui/ # Flask web UI
15
+ ```
16
+
17
+ ## Prerequisites
18
+
19
+ - Python 3.9+
20
+ - [IB Gateway](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) or TWS running with API connections enabled
21
+ - Paper trading port: `7497` (default)
22
+ - Live trading port: `7496`
23
+ - For `data_upload` and `gui`: PostgreSQL with [TimescaleDB](https://www.timescale.com/)
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ git clone https://github.com/alacazar/ib-connect.git
29
+ cd ib_connect
30
+ pip install -e .
31
+ ```
32
+
33
+ The `ib-query` and `ib-download` CLI commands are registered as entry points after installation.
34
+
35
+ ## Configuration
36
+
37
+ The `ib_download` and `gui` skills require a `config.json`. Copy the provided examples and customize:
38
+
39
+ ```bash
40
+ cp ib_connect/skills/ib_download/config.example.json ib_connect/skills/ib_download/config.json
41
+ cp ib_connect/gui/config.example.json ib_connect/gui/config.json
42
+ ```
43
+
44
+ For `data_upload`, set the `PG_URI` environment variable:
45
+
46
+ ```bash
47
+ export PG_URI="postgresql://user:password@localhost:5432/dbname"
48
+ ```
49
+
50
+ ## Skills
51
+
52
+ ### ib-query
53
+
54
+ Query and qualify contract details from IB.
55
+
56
+ ```bash
57
+ ib-query --symbol AAPL --sec-type STK
58
+ ```
59
+
60
+ Output:
61
+ ```json
62
+ {"conId": 265598, "symbol": "AAPL", "secType": "STK", "exchange": "SMART", "currency": "USD", "fullName": "APPLE INC"}
63
+ ```
64
+
65
+ See [ib_connect/skills/ib_query/SKILL.md](ib_connect/skills/ib_query/SKILL.md) for all options.
66
+
67
+ ---
68
+
69
+ ### ib-download
70
+
71
+ Downloads historical OHLCV data from IB and writes CSV files. Consists of two parts:
72
+
73
+ **`download_service.py` — the background service**
74
+
75
+ IB enforces strict pacing limits: it throttles connections that issue too many data requests too quickly, and it only allows one active connection per client ID. Running a dedicated background service solves both problems — it owns a single, persistent IB connection and processes jobs one at a time, sleeping 20 seconds between data chunks to stay within IB's limits. Any number of callers can submit jobs without worrying about pacing or connection conflicts.
76
+
77
+ Start it once and keep it running (e.g. in `screen` or `tmux`, or as a system service):
78
+
79
+ ```bash
80
+ python ib_connect/skills/ib_download/download_service.py
81
+ ```
82
+
83
+ Long date ranges are split automatically into chunks sized to IB's per-request limits. Progress and errors are logged to `download_service.log` alongside the script. A PID lock file prevents duplicate instances.
84
+
85
+ **`ib-download` — the CLI client**
86
+
87
+ Submits a job to the service's SQLite queue and returns a `job_key` immediately. The caller can check status later without staying connected.
88
+
89
+ ```bash
90
+ # Submit a job
91
+ ib-download -c 265598 -s 2023-01-01 -e 2023-12-31 -b "1 min"
92
+
93
+ # Check status
94
+ ib-download --status -k <job_key>
95
+ ```
96
+
97
+ On completion the service can notify an agent via webhook (configure `webhook_url` and `token` in `config.json`).
98
+
99
+ See [ib_connect/skills/ib_download/SKILL.md](ib_connect/skills/ib_download/SKILL.md) for all options including batch mode and output format.
100
+
101
+ ---
102
+
103
+ ### data-upload
104
+
105
+ A file-watching service that validates and uploads contract JSON files and OHLCV CSV files to PostgreSQL/TimescaleDB. Drop files into the configured `input_folder` and they are processed automatically — contracts go to `finance.contracts`, OHLCV data to `finance.ohlcv_1m` or `finance.ohlcv_1d`. Processed files are moved to `processed/`; failures go to `errors/`.
106
+
107
+ ```bash
108
+ python ib_connect/skills/data_upload/data_upload.py
109
+ ```
110
+
111
+ Expected filename pattern for OHLCV: `<symbol>.<conid>.<barsize>.csv`.
112
+
113
+ See [ib_connect/skills/data_upload/SKILL.md](ib_connect/skills/data_upload/SKILL.md) for schema details.
114
+
115
+ ---
116
+
117
+ ### GUI
118
+
119
+ A Flask web interface that ties the toolkit together. From the browser you can:
120
+
121
+ - **Query contracts** — search by symbol and security type, inspect the qualified contract details returned by IB
122
+ - **Save contracts** — export selected contracts as JSON to the `data_upload` input folder, so they are picked up and stored in PostgreSQL automatically
123
+ - **Submit download jobs** — configure date range and bar size per contract and enqueue jobs with the download service
124
+ - **Monitor jobs** — check status and cancel pending jobs
125
+
126
+ ```bash
127
+ python ib_connect/gui/app.py
128
+ ```
129
+
130
+ Opens on `http://127.0.0.1:5000` by default (configurable in `gui/config.json`).
131
+
132
+ ## License
133
+
134
+ [MIT](LICENSE)
@@ -0,0 +1 @@
1
+ # IB Connect Package
@@ -0,0 +1 @@
1
+ # GUI Package
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ IB Data Downloader GUI - Flask Web App
4
+ """
5
+
6
+ from flask import Flask, render_template, request, jsonify
7
+ import subprocess
8
+ import json
9
+ import os
10
+ import sys
11
+ import asyncio
12
+ import logging
13
+ from concurrent.futures import ThreadPoolExecutor
14
+ from datetime import datetime
15
+
16
+
17
+ from ib_connect.skills.ib_query.query import query_ib
18
+ from ib_connect.skills.ib_download.job_queue import JobQueue
19
+
20
+ app = Flask(__name__)
21
+ logging.getLogger('werkzeug').setLevel(logging.ERROR)
22
+
23
+ # Config
24
+ CONFIG_FILE = 'config.json'
25
+ DEFAULT_CONFIG = {
26
+ 'contracts_folder': './data/contracts',
27
+ 'ib_host': '127.0.0.1',
28
+ 'ib_port': 7497,
29
+ 'ib_client_id': 77
30
+ }
31
+
32
+ def load_config():
33
+ if os.path.exists(CONFIG_FILE):
34
+ with open(CONFIG_FILE, 'r') as f:
35
+ return json.load(f)
36
+ return DEFAULT_CONFIG
37
+
38
+ def save_config(config):
39
+ with open(CONFIG_FILE, 'w') as f:
40
+ json.dump(config, f, indent=2)
41
+
42
+ config = load_config()
43
+
44
+ @app.route('/')
45
+ def index():
46
+ return render_template('index.html')
47
+
48
+ @app.route('/query', methods=['POST'])
49
+ def query_contracts():
50
+ data = request.json
51
+ # Include configured IB params to avoid conflicts
52
+ data['host'] = config.get('ib_host', '127.0.0.1')
53
+ data['port'] = config.get('ib_port', 7497)
54
+ data['client_id'] = config.get('ib_client_id', 1)
55
+ def run_query():
56
+ return asyncio.run(query_ib(data))
57
+
58
+ try:
59
+ with ThreadPoolExecutor() as executor:
60
+ future = executor.submit(run_query)
61
+ result = future.result()
62
+ if isinstance(result, dict) and 'error' in result:
63
+ return jsonify({'success': False, 'error': result['error']})
64
+ contracts = result if isinstance(result, list) else [result] if result else []
65
+ contracts = [c for c in contracts if c is not None] # Filter out nulls
66
+ return jsonify({'success': True, 'contracts': contracts})
67
+ except Exception as e:
68
+ return jsonify({'success': False, 'error': str(e)})
69
+
70
+ @app.route('/save_contracts', methods=['POST'])
71
+ def save_contracts():
72
+ data = request.json
73
+ selected = data.get('selected', [])
74
+ # Add defaults for missing required fields
75
+ for contract in selected:
76
+ if 'time_zone_id' not in contract or not contract['time_zone_id']:
77
+ contract['time_zone_id'] = 'US/Eastern'
78
+ if 'min_tick' not in contract or not contract['min_tick']:
79
+ contract['min_tick'] = 0.01
80
+ if 'tick_value' not in contract or not contract['tick_value']:
81
+ contract['tick_value'] = contract.get('min_tick', 0.01) * contract.get('multiplier', 1)
82
+ if 'multiplier' not in contract or not contract['multiplier']:
83
+ contract['multiplier'] = 1
84
+ # Omit empty strings and zero values, keep None for db defaults
85
+ cleaned_selected = []
86
+ for contract in selected:
87
+ cleaned = {k: v for k, v in contract.items() if v not in ('', 0)}
88
+ cleaned_selected.append(cleaned)
89
+ folder = config['contracts_folder']
90
+ os.makedirs(folder, exist_ok=True)
91
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
92
+ file_path = os.path.join(folder, f'selected_contracts_{timestamp}.json')
93
+ with open(file_path, 'w') as f:
94
+ json.dump(cleaned_selected, f, indent=2)
95
+ return jsonify({'success': True, 'file': file_path})
96
+
97
+ @app.route('/download', methods=['POST'])
98
+ def download_data():
99
+ data = request.json
100
+ contracts = data.get('contracts', [])
101
+ downloads_folder = config.get('downloads_folder', './data/downloads')
102
+ job_queue_db = config.get('job_queue_db', './jobs.db')
103
+ queue = JobQueue(job_queue_db)
104
+ jobs = []
105
+ for contract in contracts:
106
+ params = {
107
+ 'conid': contract['conid'],
108
+ 'start': contract['start_date'],
109
+ 'end': contract['end_date'],
110
+ 'bar_size': contract['bar_size'],
111
+ 'show': contract.get('show', 'TRADES'),
112
+ 'msg': f"Download {contract['symbol']} data ({contract.get('show', 'TRADES')})"
113
+ }
114
+ try:
115
+ job_key = queue.submit_job(params)
116
+ jobs.append({'contract': contract['symbol'], 'job_key': job_key, 'status': 'submitted'})
117
+ except Exception as e:
118
+ jobs.append({'contract': contract['symbol'], 'error': str(e)})
119
+ return jsonify({'jobs': jobs})
120
+
121
+ @app.route('/job_status/<job_key>')
122
+ def job_status(job_key):
123
+ try:
124
+ job_queue_db = config['job_queue_db']
125
+ queue = JobQueue(job_queue_db)
126
+ status_data = queue.get_status(job_key)
127
+ if status_data['status'] == 'not_found':
128
+ return jsonify({'status': 'error', 'details': 'Job not found'})
129
+ details = f"Status: {status_data.get('status', 'unknown')}"
130
+ if status_data.get('message'):
131
+ details += f" - {status_data['message']}"
132
+ if status_data.get('error'):
133
+ details += f" - Error: {status_data['error']}"
134
+ return jsonify({'status': 'ok', 'details': details})
135
+ except Exception as e:
136
+ return jsonify({'status': 'error', 'details': str(e)})
137
+
138
+ @app.route('/cancel_job/<job_key>', methods=['POST'])
139
+ def cancel_job(job_key):
140
+ try:
141
+ job_queue_db = config['job_queue_db']
142
+ queue = JobQueue(job_queue_db)
143
+ status_data = queue.get_status(job_key)
144
+ if status_data['status'] == 'pending':
145
+ queue.remove_job(job_key)
146
+ return jsonify({'success': True})
147
+ else:
148
+ return jsonify({'success': False, 'error': 'Job not pending'})
149
+ except Exception as e:
150
+ return jsonify({'success': False, 'error': str(e)})
151
+
152
+ if __name__ == '__main__':
153
+ app.run(debug=True, host=config.get('host', '127.0.0.1'), port=config.get('port', 5000))
@@ -0,0 +1 @@
1
+ # Shared utilities for IB Connect
@@ -0,0 +1,34 @@
1
+ from ib_insync import IB
2
+
3
+ class IBConnection:
4
+ """
5
+ Connection management for Interactive Brokers using ib_insync.
6
+ Provides a connected IB instance that can be used as a context manager.
7
+ """
8
+
9
+ @staticmethod
10
+ def connect(host='127.0.0.1', port=7497, clientId=1, timeout=4.0, readonly=False, account=''):
11
+ """
12
+ Establish a connection to IB and return the connected IB instance (which is a context manager).
13
+
14
+ Args:
15
+ host (str): IB Gateway/TWS host. Default: '127.0.0.1'
16
+ port (int): IB Gateway/TWS port. Default: 7497
17
+ clientId (int): Client ID for the connection. Default: 1
18
+ timeout (float): Connection timeout in seconds. Default: 4.0
19
+ readonly (bool): Read-only connection. Default: False
20
+ account (str): Account to use (optional). Default: ''
21
+
22
+ Returns:
23
+ IB: Connected IB instance (supports context manager: with ib: ...)
24
+
25
+ Raises:
26
+ ConnectionError: If connection fails.
27
+ """
28
+ ib = IB()
29
+ try:
30
+ ib.connect(host=host, port=port, clientId=clientId, timeout=timeout, readonly=readonly, account=account)
31
+ except Exception as e:
32
+ raise ConnectionError(f"Failed to connect to Interactive Brokers: {e}") from e
33
+
34
+ return ib
@@ -0,0 +1 @@
1
+ # Skills for IB Connect
@@ -0,0 +1 @@
1
+ # Data Upload skill