diff --git a/.github/workflows/dxapi-python-build.yml b/.github/workflows/dxapi-python-build.yml new file mode 100644 index 0000000..7b83e7e --- /dev/null +++ b/.github/workflows/dxapi-python-build.yml @@ -0,0 +1,270 @@ +name: Build dxapi + +on: + workflow_dispatch: + push: + branches: [main, wip-workflow] + pull_request: + types: [opened, synchronize] + branches: [main, wip-workflow] + +jobs: + build-dxapi: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + include: + - os: macos-latest + env_os: MACOS + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Build Dxapi + run: make -C ./dxapi/. + env: + CC: clang + OS: ${{ matrix.env_os }} + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-${{ matrix.os }} + path: ./dxapi/bin/libdxapi-x64.a + + build-windows-dxapi: + runs-on: windows-2019 + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Use MSBuild + uses: microsoft/setup-msbuild@v1.1 + - name: Build Solution + run: msbuild ./dxapi/dxapi.sln /p:configuration=release /p:platform=x64 /t:rebuild + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-windows + path: ./dxapi/bin/dxapi-x64.lib + + build-dxapi-python-linux: + runs-on: ubuntu-latest + needs: [build-dxapi] + strategy: + matrix: + py: ['3.6', '3.7', '3.8', '3.9', '3.10'] + include: + - py: '3.6' + py_env: '36' + - py: '3.7' + py_env: '37' + - py: '3.8' + py_env: '38' + - py: '3.9' + py_env: '39' + - py: '3.10' + py_env: '310' + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Download dxapi-linux artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-ubuntu-latest + path: dxapi/bin + - name: install dev python 3.8, 3.9 + if: ${{ matrix.py == '3.8' || matrix.py == '3.9' }} + run: | + sudo apt-get update + sudo apt install -y python${{ matrix.py }}-dev + - name: install dev python 3.6, 3.7, 3.10 + if: ${{ matrix.py == '3.6' || matrix.py == '3.7' || matrix.py == '3.10' }} + run: | + sudo apt-get update + sudo apt-get install -yq software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.py }}-dev + - name: Build Dxapi + run: | + make -C . + cp ./dfp/lib/linux/64/libDecimalNative.so ./bin/release/linux/x64/py${{ matrix.py_env }}/ + env: + CC: clang + PYTHON_VERSION: ${{ matrix.py_env }} + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-python-linux + path: | + ./bin/release/__init__.py + ./bin/release/linux/x64/py${{ matrix.py_env }}/_dxapi.so + ./bin/release/linux/x64/py${{ matrix.py_env }}/libDecimalNative.so + + build-dxapi-python-windows: + runs-on: windows-2019 + needs: [build-windows-dxapi] + strategy: + matrix: + py: ['3.6', '3.7', '3.8', '3.9', '3.10'] + include: + - py: '3.6' + py_env: '36' + - py: '3.7' + py_env: '37' + - py: '3.8' + py_env: '38' + - py: '3.9' + py_env: '39' + - py: '3.10' + py_env: '310' + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Download dxapi-windows artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-windows + path: dxapi/bin + - uses: actions/setup-python@v2 + with: + python-version: '${{ matrix.py }}' + - name: Use MSBuild + uses: microsoft/setup-msbuild@v1.1 + - name: Build Solution + run: | + Expand-Archive -Path ./swigwin/swigwin-3.0.12.zip -DestinationPath ./swigwin -Force + $pwd = pwd + $Env:SWIG_HOME="$pwd/swigwin/swigwin-3.0.12" + $Env:SWIG_HOME + python --version + python -c "import os, sys; print(os.path.dirname(sys.executable))" + $Env:PYTHON${{ matrix.py_env }}_HOME=python -c "import os, sys; print(os.path.dirname(sys.executable))" + $Env:PYTHON${{ matrix.py_env }}_HOME + msbuild ./dxapi-python.sln /p:configuration=Release${{ matrix.py_env }} /p:platform=x64 /t:rebuild + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-python-windows + path: | + ./bin/release/__init__.py + ./bin/release/windows/x64/py${{ matrix.py_env }}/_dxapi.pyd + ./bin/release/windows/x64/py${{ matrix.py_env }}/DecimalNative.dll + + build-dxapi-python-macos: + runs-on: macos-latest + needs: [build-dxapi] + strategy: + matrix: + py: ['3.6', '3.7', '3.8', '3.9', '3.10'] + include: + - py: '3.6' + py_env: '36' + py_v: '3.6.15' + py_lib_prefix: '3.6m' + - py: '3.7' + py_env: '37' + py_v: '3.7.12' + py_lib_prefix: '3.7m' + - py: '3.8' + py_env: '38' + py_v: '3.8.12' + py_lib_prefix: '3.8' + - py: '3.9' + py_env: '39' + py_v: '3.9.10' + py_lib_prefix: '3.9' + - py: '3.10' + py_env: '310' + py_v: '3.10.2' + py_lib_prefix: '3.10' + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Download dxapi-macos artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-macos-latest + path: dxapi/bin + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.py_v }} + - name: Install SWIG + run: | + curl -OL https://sourceforge.net/projects/swig/files/swig/swig-3.0.12/swig-3.0.12.tar.gz + tar -xzvf swig-3.0.12.tar.gz + cd swig-3.0.12 + ./configure + make + make install + - name: Build Dxapi + run: | + mkdir /Library/Frameworks/Python.framework/Versions/${{ matrix.py }} + ln -s /Users/runner/hostedtoolcache/Python/${{ matrix.py_v }}/x64/include/python${{ matrix.py_lib_prefix }} /Library/Frameworks/Python.framework/Versions/${{ matrix.py }} + mv /Library/Frameworks/Python.framework/Versions/${{ matrix.py }}/python${{ matrix.py_lib_prefix }} /Library/Frameworks/Python.framework/Versions/${{ matrix.py }}/Headers + ln -s /Users/runner/hostedtoolcache/Python/${{ matrix.py_v }}/x64/lib /Library/Frameworks/Python.framework/Versions/${{ matrix.py }} + make -C . + cp ./dfp/lib/osx/64/libDecimalNative.dylib ./bin/release/darwin/x64/py${{ matrix.py_env }}/ + env: + CC: clang + OS: MACOS + PYTHON_VERSION: ${{ matrix.py_env }} + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-python-macos + path: | + ./bin/release/__init__.py + ./bin/release/darwin/x64/py${{ matrix.py_env }}/_dxapi.so + ./bin/release/darwin/x64/py${{ matrix.py_env }}/libDecimalNative.dylib + +# Tests + test-dxapi-python-linux: + runs-on: ubuntu-latest + needs: [build-dxapi-python-linux] + strategy: + matrix: + py: ['3.10'] + services: + timebase: + image: finos/timebase-ce-server + env: + JAVA_OPTS: "-DQuantServer.enableRemoteMonitoring=true -DTimeBase.version=5.0" + ports: + - 8011:8011 + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Download dxapi-python-linux artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-python-linux + path: tests/dxapi + - uses: actions/setup-python@v2 + with: + python-version: '${{ matrix.py }}' + - name: Run tests + run: | + chmod -R 777 ./tests + cd ./tests + python TestAll.py + env: + TIMEBASE_HOST: localhost + TIMEBASE_PORT: 8011 + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: test-reports-dxapi-python-linux + path: | + ./tests/reports diff --git a/.github/workflows/dxapi-python-release.yml b/.github/workflows/dxapi-python-release.yml new file mode 100644 index 0000000..4b07005 --- /dev/null +++ b/.github/workflows/dxapi-python-release.yml @@ -0,0 +1,345 @@ +name: Release dxapi + +on: + push: + branches: [release-*] + +jobs: + + prepare: + if: ${{ !contains(github.event.head_commit.message, '[skip-ci]') }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Prepare branch + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git checkout -b workflow-$GITHUB_RUN_ID + versionSnapshot=`grep 'version=' project.properties | sed 's/version=\([^-]*\)/\1/'` + versionRelease=`echo $versionSnapshot | sed 's/\([^-]*\)-SNAPSHOT/\1/'` + versionSnapshotNext=`echo $versionSnapshot | perl -pe 's/^((\d+\.)*)(\d+)(.*)$/$1.($3+1).$4/e'` + echo "$versionSnapshot -> $versionRelease -> $versionSnapshotNext" + sed -i "s/version=$versionSnapshot/version=$versionRelease/" project.properties + git commit -am "[skip-ci] Generate release version" + sed -i "s/version=$versionRelease/version=$versionSnapshotNext/" project.properties + git commit -am "[skip-ci] Generate next snapshot version" + git push origin HEAD + + build-dxapi: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + include: + - os: macos-latest + env_os: MACOS + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Build Dxapi + run: make -C ./dxapi/. + env: + CC: clang + OS: ${{ matrix.env_os }} + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-${{ matrix.os }} + path: ./dxapi/bin/libdxapi-x64.a + + build-windows-dxapi: + runs-on: windows-2019 + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Use MSBuild + uses: microsoft/setup-msbuild@v1.1 + - name: Build Solution + run: msbuild ./dxapi/dxapi.sln /p:configuration=release /p:platform=x64 /t:rebuild + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-windows + path: ./dxapi/bin/dxapi-x64.lib + + build-dxapi-python-linux: + runs-on: ubuntu-latest + needs: [build-dxapi] + strategy: + matrix: + py: ['3.6', '3.7', '3.8', '3.9', '3.10'] + include: + - py: '3.6' + py_env: '36' + - py: '3.7' + py_env: '37' + - py: '3.8' + py_env: '38' + - py: '3.9' + py_env: '39' + - py: '3.10' + py_env: '310' + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Download dxapi-linux artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-ubuntu-latest + path: dxapi/bin + - name: install dev python 3.8, 3.9 + if: ${{ matrix.py == '3.8' || matrix.py == '3.9' }} + run: | + sudo apt-get update + sudo apt install -y python${{ matrix.py }}-dev + - name: install dev python 3.6, 3.7, 3.10 + if: ${{ matrix.py == '3.6' || matrix.py == '3.7' || matrix.py == '3.10' }} + run: | + sudo apt-get update + sudo apt-get install -yq software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.py }}-dev + - name: Build Dxapi + run: | + make -C . + cp ./dfp/lib/linux/64/libDecimalNative.so ./bin/release/linux/x64/py${{ matrix.py_env }}/ + env: + CC: clang + PYTHON_VERSION: ${{ matrix.py_env }} + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-python-linux + path: | + ./bin/release/__init__.py + ./bin/release/linux/x64/py${{ matrix.py_env }}/_dxapi.so + ./bin/release/linux/x64/py${{ matrix.py_env }}/libDecimalNative.so + + build-dxapi-python-windows: + runs-on: windows-2019 + needs: [build-windows-dxapi] + strategy: + matrix: + py: ['3.6', '3.7', '3.8', '3.9', '3.10'] + include: + - py: '3.6' + py_env: '36' + - py: '3.7' + py_env: '37' + - py: '3.8' + py_env: '38' + - py: '3.9' + py_env: '39' + - py: '3.10' + py_env: '310' + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Download dxapi-windows artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-windows + path: dxapi/bin + - uses: actions/setup-python@v2 + with: + python-version: '${{ matrix.py }}' + - name: Use MSBuild + uses: microsoft/setup-msbuild@v1.1 + - name: Build Solution + run: | + Expand-Archive -Path ./swigwin/swigwin-3.0.12.zip -DestinationPath ./swigwin -Force + $pwd = pwd + $Env:SWIG_HOME="$pwd/swigwin/swigwin-3.0.12" + $Env:SWIG_HOME + python --version + python -c "import os, sys; print(os.path.dirname(sys.executable))" + $Env:PYTHON${{ matrix.py_env }}_HOME=python -c "import os, sys; print(os.path.dirname(sys.executable))" + $Env:PYTHON${{ matrix.py_env }}_HOME + msbuild ./dxapi-python.sln /p:configuration=Release${{ matrix.py_env }} /p:platform=x64 /t:rebuild + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-python-windows + path: | + ./bin/release/__init__.py + ./bin/release/windows/x64/py${{ matrix.py_env }}/_dxapi.pyd + ./bin/release/windows/x64/py${{ matrix.py_env }}/DecimalNative.dll + + build-dxapi-python-macos: + runs-on: macos-latest + needs: [build-dxapi] + strategy: + matrix: + py: ['3.6', '3.7', '3.8', '3.9', '3.10'] + include: + - py: '3.6' + py_env: '36' + py_v: '3.6.15' + py_lib_prefix: '3.6m' + - py: '3.7' + py_env: '37' + py_v: '3.7.12' + py_lib_prefix: '3.7m' + - py: '3.8' + py_env: '38' + py_v: '3.8.12' + py_lib_prefix: '3.8' + - py: '3.9' + py_env: '39' + py_v: '3.9.10' + py_lib_prefix: '3.9' + - py: '3.10' + py_env: '310' + py_v: '3.10.2' + py_lib_prefix: '3.10' + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Download dxapi-macos artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-macos-latest + path: dxapi/bin + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.py_v }} + - name: Install SWIG + run: | + curl -OL https://sourceforge.net/projects/swig/files/swig/swig-3.0.12/swig-3.0.12.tar.gz + tar -xzvf swig-3.0.12.tar.gz + cd swig-3.0.12 + ./configure + make + make install + - name: Build Dxapi + run: | + mkdir /Library/Frameworks/Python.framework/Versions/${{ matrix.py }} + ln -s /Users/runner/hostedtoolcache/Python/${{ matrix.py_v }}/x64/include/python${{ matrix.py_lib_prefix }} /Library/Frameworks/Python.framework/Versions/${{ matrix.py }} + mv /Library/Frameworks/Python.framework/Versions/${{ matrix.py }}/python${{ matrix.py_lib_prefix }} /Library/Frameworks/Python.framework/Versions/${{ matrix.py }}/Headers + ln -s /Users/runner/hostedtoolcache/Python/${{ matrix.py_v }}/x64/lib /Library/Frameworks/Python.framework/Versions/${{ matrix.py }} + make -C . + cp ./dfp/lib/osx/64/libDecimalNative.dylib ./bin/release/darwin/x64/py${{ matrix.py_env }}/ + env: + CC: clang + OS: MACOS + PYTHON_VERSION: ${{ matrix.py_env }} + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-python-macos + path: | + ./bin/release/__init__.py + ./bin/release/darwin/x64/py${{ matrix.py_env }}/_dxapi.so + ./bin/release/darwin/x64/py${{ matrix.py_env }}/libDecimalNative.dylib + + gather-artifacts: + runs-on: ubuntu-latest + needs: [build-dxapi-python-linux, build-dxapi-python-macos] + steps: + - name: Check out repository code + uses: actions/checkout@v2 + - name: Download dxapi-python-linux artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-python-linux + path: ./dxapi/ + - name: Download dxapi-python-macos artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-python-macos + path: ./dxapi/ +# - name: Download dxapi-python-windows artifacts +# uses: actions/download-artifact@v2 +# with: +# name: dxapi-python-windows +# path: ./dxapi/ + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-python + path: | + ./dxapi/__init__.py + ./dxapi/**/* + +## Tests + + release: + if: ${{ !contains(github.event.head_commit.message, '[skip-ci]') }} + needs: [gather-artifacts] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Release branch + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git fetch + git checkout -b workflow-$GITHUB_RUN_ID origin/workflow-$GITHUB_RUN_ID~1 + versionRelease=`grep 'version=' project.properties | sed 's/version=\([^-]*\)/\1/'` + echo $versionRelease + echo $versionRelease >> ./version.txt + git push origin origin/workflow-$GITHUB_RUN_ID:$GITHUB_REF + - name: Version artifact + uses: actions/upload-artifact@v2 + with: + name: version-file + path: ./version.txt + + publish: + needs: [release] + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Download dxapi-python artifacts + uses: actions/download-artifact@v2 + with: + name: dxapi-python + path: ./dxapi/ + - name: Download version-file artifacts + uses: actions/download-artifact@v2 + with: + name: version-file + - name: Build PyPi package + run: | + $Version = get-content .\version.txt + echo $Version + pip install wheel + python3 setup.py bdist_wheel --universal +# python3 -m twine upload --repository-url $Env:REPOSITORY_URL --username $Env:REPOSITORY_USER --password $Env:REPOSITORY_PASSWORD} dist/* + env: + REPOSITORY_URL: add repository url + REPOSITORY_USER: ${{ secrets.PYPI_USER }} + REPOSITORY_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + - name: Archive artifacts + uses: actions/upload-artifact@v2 + with: + name: dxapi-pypi-package + path: ./dist/ + + cleanup-release: + if: ${{ always() && !contains(github.event.head_commit.message, '[skip-ci]') }} + needs: [publish] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Cleanup + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git push origin --delete workflow-$GITHUB_RUN_ID || true diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..543cd8f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dxapi"] + path = dxapi + url = https://github.com/epam/TimeBaseClientCpp.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile index d28e7e4..f2f5da5 100644 --- a/Makefile +++ b/Makefile @@ -2,41 +2,45 @@ OS?=LINUX -DXAPI_BIN=../dxapi/bin +DXAPI_BIN=./dxapi/bin PYTHONAPI_INTERFACE=dxapi.py PYHTONAPI_LIB=_dxapi.so PYTHON_VERSION?=36 -ifeq ($(PYTHON_VERSION),27) - PYTHON=python2.7 - PYTHON_VERSION_FULL=2.7 - CONFIGURATION_SUFFIX=27 -else ifeq ($(PYTHON_VERSION),37) +ifeq ($(PYTHON_VERSION),37) PYTHON=python3.7 PYTHON_VERSION_FULL=3.7 - CONFIGURATION_SUFFIX=37 + PYTHON_LIB_SUFFIX=3.7m else ifeq ($(PYTHON_VERSION),38) PYTHON=python3.8 PYTHON_VERSION_FULL=3.8 - CONFIGURATION_SUFFIX=38 + PYTHON_LIB_SUFFIX=3.8 +else ifeq ($(PYTHON_VERSION),39) + PYTHON=python3.9 + PYTHON_VERSION_FULL=3.9 + PYTHON_LIB_SUFFIX=3.9 +else ifeq ($(PYTHON_VERSION),310) + PYTHON=python3.10 + PYTHON_VERSION_FULL=3.10 + PYTHON_LIB_SUFFIX=3.10 else PYTHON=python3.6 PYTHON_VERSION_FULL=3.6 - CONFIGURATION_SUFFIX=36 + PYTHON_LIB_SUFFIX=3.6m endif ifeq ($(OS),MACOS) - DFP_BIN=. - DFP_LIB=DecimalNative - RPATH_PARAM= + DFP_BIN=./dfp/lib/osx/64 + DFP_LIB=DecimalNative + RPATH_PARAM= THIRD_PARTY_LIBS= PYTHON_INCLUDES=/Library/Frameworks/Python.framework/Versions/$(PYTHON_VERSION_FULL)/Headers - PYTHON_LIBS=-L/Library/Frameworks/Python.framework/Versions/$(PYTHON_VERSION_FULL)/lib -lpython$(PYTHON_VERSION_FULL) + PYTHON_LIBS=-L/Library/Frameworks/Python.framework/Versions/$(PYTHON_VERSION_FULL)/lib -lpython$(PYTHON_LIB_SUFFIX) BIN_SUBFOLDER=darwin else - DFP_BIN=./dfp/lib/linux/64 - DFP_LIB=DecimalNative - RPATH_PARAM=-Wl,-rpath,'$$ORIGIN' + DFP_BIN=./dfp/lib/linux/64 + DFP_LIB=DecimalNative + RPATH_PARAM=-Wl,-rpath,'$$ORIGIN' PYTHON_INCLUDES=/usr/include/$(PYTHON) PYTHON_LIBS= BIN_SUBFOLDER=linux @@ -58,7 +62,7 @@ SRCDIR=src SRCDIRS=codecs swig swig/wrappers # Include directories -INCLUDES= $(PYTHON_INCLUDES) ../dxapi/include/native ../dxapi/include/native/dxapi ../dxapi/src/dxapi ../dxapi/src/dxapi/native $(SRCDIR) +INCLUDES= $(PYTHON_INCLUDES) ./dxapi/include/native ./dxapi/include/native/dxapi $(SRCDIR) OBJ=$(OBJ_LIB) @@ -160,7 +164,7 @@ $(OUTDIRS): # python wrapper $(WRAPPER_OBJDIR)/$(WRAPPER_OBJ).o: dxapi.i - swig -c++ -python -I../dxapi/include/native/dxapi -o $(WRAPDIR)/$(WRAPPER_OBJ).cxx -outdir $(INIT_PY_DIR) src/swig/dxapi.i + swig -c++ -python -I./dxapi/include/native/dxapi -o $(WRAPDIR)/$(WRAPPER_OBJ).cxx -outdir $(INIT_PY_DIR) src/swig/dxapi.i cp $(INIT_PY_DIR)/dxapi.py $(INIT_PY_DIR)/__init__.py $(CXX) -c $(CXXFLAGS) -o $@ $(WRAPDIR)/$(WRAPPER_OBJ).cxx diff --git a/dfp/lib/osx/64/libDecimalNative.dylib b/dfp/lib/osx/64/libDecimalNative.dylib index 23c8d96..4c6c1e9 100644 Binary files a/dfp/lib/osx/64/libDecimalNative.dylib and b/dfp/lib/osx/64/libDecimalNative.dylib differ diff --git a/dxapi b/dxapi new file mode 160000 index 0000000..960859b --- /dev/null +++ b/dxapi @@ -0,0 +1 @@ +Subproject commit 960859b2d47e5d50bbb73db30a5113f0abb43c58 diff --git a/dxapi-python.sln b/dxapi-python.sln index 1f56dd6..5ba06f6 100644 --- a/dxapi-python.sln +++ b/dxapi-python.sln @@ -1,62 +1,44 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 -VisualStudioVersion = 16.0.30503.244 +VisualStudioVersion = 16.0.30330.147 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dxapi-python", "projects\dxapi-python.vcxproj", "{F06CE5AA-B67E-4E38-8473-283FEBE7AE33}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Debug27|x64 = Debug27|x64 - Debug27|x86 = Debug27|x86 + Debug310|x64 = Debug310|x64 + Debug36|x64 = Debug36|x64 Debug37|x64 = Debug37|x64 - Debug37|x86 = Debug37|x86 Debug38|x64 = Debug38|x64 - Debug38|x86 = Debug38|x86 - Release|x64 = Release|x64 - Release|x86 = Release|x86 - Release27|x64 = Release27|x64 - Release27|x86 = Release27|x86 + Debug39|x64 = Debug39|x64 + Release310|x64 = Release310|x64 + Release36|x64 = Release36|x64 Release37|x64 = Release37|x64 - Release37|x86 = Release37|x86 Release38|x64 = Release38|x64 - Release38|x86 = Release38|x86 + Release39|x64 = Release39|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug|x64.ActiveCfg = Debug|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug|x64.Build.0 = Debug|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug|x86.ActiveCfg = Debug|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug|x86.Build.0 = Debug|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug27|x64.ActiveCfg = Debug27|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug27|x64.Build.0 = Debug27|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug27|x86.ActiveCfg = Debug27|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug27|x86.Build.0 = Debug27|Win32 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug310|x64.ActiveCfg = Debug310|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug310|x64.Build.0 = Debug310|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug36|x64.ActiveCfg = Debug36|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug36|x64.Build.0 = Debug36|x64 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug37|x64.ActiveCfg = Debug37|x64 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug37|x64.Build.0 = Debug37|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug37|x86.ActiveCfg = Debug37|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug37|x86.Build.0 = Debug37|Win32 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug38|x64.ActiveCfg = Debug38|x64 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug38|x64.Build.0 = Debug38|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug38|x86.ActiveCfg = Debug38|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug38|x86.Build.0 = Debug38|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release|x64.ActiveCfg = Release|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release|x64.Build.0 = Release|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release|x86.ActiveCfg = Release|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release|x86.Build.0 = Release|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release27|x64.ActiveCfg = Release27|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release27|x64.Build.0 = Release27|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release27|x86.ActiveCfg = Release27|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release27|x86.Build.0 = Release27|Win32 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug39|x64.ActiveCfg = Debug39|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Debug39|x64.Build.0 = Debug39|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release310|x64.ActiveCfg = Release310|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release310|x64.Build.0 = Release310|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release36|x64.ActiveCfg = Release36|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release36|x64.Build.0 = Release36|x64 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release37|x64.ActiveCfg = Release37|x64 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release37|x64.Build.0 = Release37|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release37|x86.ActiveCfg = Release37|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release37|x86.Build.0 = Release37|Win32 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release38|x64.ActiveCfg = Release38|x64 {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release38|x64.Build.0 = Release38|x64 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release38|x86.ActiveCfg = Release38|Win32 - {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release38|x86.Build.0 = Release38|Win32 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release39|x64.ActiveCfg = Release39|x64 + {F06CE5AA-B67E-4E38-8473-283FEBE7AE33}.Release39|x64.Build.0 = Release39|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/libDecimalNative.dylib b/libDecimalNative.dylib deleted file mode 100644 index 4c6c1e9..0000000 Binary files a/libDecimalNative.dylib and /dev/null differ diff --git a/project.properties b/project.properties index 159a0ee..2bcf672 100644 --- a/project.properties +++ b/project.properties @@ -1 +1 @@ -version=6.0.10 +version=6.0.15-SNAPSHOT diff --git a/projects/dxapi-python.vcxproj b/projects/dxapi-python.vcxproj index 0c8f3f4..9c2669f 100644 --- a/projects/dxapi-python.vcxproj +++ b/projects/dxapi-python.vcxproj @@ -1,68 +1,44 @@  - - Debug37 - Win32 - Debug37 x64 - - Debug38 - Win32 - Debug38 x64 - - Debug - Win32 - - - Debug27 - Win32 - - - Debug27 + + Release37 x64 - - Release27 - Win32 - - - Release27 + + Release38 x64 - - Release37 - Win32 - - - Release37 + + Debug36 x64 - - Release38 - Win32 + + Release36 + x64 - - Release38 + + Debug39 x64 - - Release - Win32 + + Release39 + x64 - - Debug + + Debug310 x64 - - Release + + Release310 x64 @@ -70,27 +46,32 @@ Document - rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) + rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" +"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) del /f "$(ProjectDir)..\bin\release\__init__.py" ren "$(ProjectDir)..\bin\release\dxapi.py" "__init__.py" rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) +"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) del /f "$(ProjectDir)..\bin\release\__init__.py" ren "$(ProjectDir)..\bin\release\dxapi.py" "__init__.py" rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) +"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) +del /f "$(ProjectDir)..\bin\release\__init__.py" +ren "$(ProjectDir)..\bin\release\dxapi.py" "__init__.py" + rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" +"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) del /f "$(ProjectDir)..\bin\release\__init__.py" ren "$(ProjectDir)..\bin\release\dxapi.py" "__init__.py" - rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) + rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" +"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\release" -Fmicrosoft %(FullPath) del /f "$(ProjectDir)..\bin\release\__init__.py" ren "$(ProjectDir)..\bin\release\dxapi.py" "__init__.py" - $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\release\__init__.py + $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\release\__init__.py $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\release\__init__.py $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\release\__init__.py - $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\release\__init__.py - rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" + $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\release\__init__.py + $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\release\__init__.py + rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" "$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\debug" -Fmicrosoft %(FullPath) del /f "$(ProjectDir)..\bin\debug\__init__.py" ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" @@ -102,23 +83,19 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" "$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\debug" -Fmicrosoft %(FullPath) del /f "$(ProjectDir)..\bin\debug\__init__.py" ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" - rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" + rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" "$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\debug" -Fmicrosoft %(FullPath) del /f "$(ProjectDir)..\bin\debug\__init__.py" ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" - $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\debug\__init__.py + rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" +"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\debug" -Fmicrosoft %(FullPath) +del /f "$(ProjectDir)..\bin\debug\__init__.py" +ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" + $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\debug\__init__.py $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\debug\__init__.py $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\debug\__init__.py - $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\debug\__init__.py - rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\$(PlatformTarget)\$(Configuration)" -Fmicrosoft %(FullPath) - rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\$(PlatformTarget)\$(Configuration)" -Fmicrosoft %(FullPath) - rm "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -"$(SWIG_HOME)\swig.exe" -c++ -python -I$(ProjectDir)..\..\dxapi\include\native\dxapi -o "$(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx" -outdir "$(ProjectDir)..\bin\$(PlatformTarget)\$(Configuration)" -Fmicrosoft %(FullPath) - $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx - $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx - $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx + $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\debug\__init__.py + $(ProjectDir)..\src\swig\wrappers\dxapi_wrap.cxx;$(ProjectDir)..\bin\debug\__init__.py @@ -145,113 +122,76 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" {F06CE5AA-B67E-4E38-8473-283FEBE7AE33} dxapipython - 8.1 + 10.0.18362.0 - - Application - true - v140 - MultiByte - - - Application - true - v140 - MultiByte - - - Application - true - v140 - MultiByte - - - Application - true - v140 - MultiByte - - - DynamicLibrary - false - v140 - true - MultiByte - - - DynamicLibrary - false - v140 - true - MultiByte - - + DynamicLibrary - false - v140 - true - MultiByte - - - Application - false - v140 - true + true + v142 MultiByte + false - + DynamicLibrary true - v140 + v142 MultiByte false - + DynamicLibrary true - v140 + v142 MultiByte false - + DynamicLibrary true - v140 + v142 MultiByte false - + DynamicLibrary true - v140 + v142 MultiByte + false - + DynamicLibrary false - v140 + v142 true MultiByte DynamicLibrary false - v140 + v142 true MultiByte DynamicLibrary false - ClangCL + v142 + true + MultiByte + + + DynamicLibrary + false + v142 true MultiByte - + DynamicLibrary false - ClangCL + v142 true MultiByte @@ -260,31 +200,7 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -293,10 +209,13 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" - + + + + - + @@ -305,11 +224,14 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" - + + + + - + $(ProjectDir)../bin/debug/windows/$(PlatformTarget)/py36 $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ _dxapi_d @@ -327,13 +249,19 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" _dxapi_d .pyd - - $(ProjectDir)../bin/debug/windows/$(PlatformTarget)/py27 + + $(ProjectDir)../bin/debug/windows/$(PlatformTarget)/py39 $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ - _dxapi + _dxapi_d .pyd - + + $(ProjectDir)../bin/debug/windows/$(PlatformTarget)/py310 + $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ + _dxapi_d + .pyd + + $(ProjectDir)../bin/release/windows/$(PlatformTarget)/py36 $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ .pyd @@ -354,65 +282,26 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" _dxapi - - $(ProjectDir)../bin/release/windows/$(PlatformTarget)/py27 + + $(ProjectDir)../bin/release/windows/$(PlatformTarget)/py39 $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ .pyd _dxapi - - $(ProjectDir)../bin/$(PlatformTarget)/$(Configuration) + + $(ProjectDir)../bin/release/windows/$(PlatformTarget)/py310 $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ - _dxapi .pyd - - - $(ProjectDir)../bin/$(PlatformTarget)/$(Configuration) - $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ _dxapi - .pyd - - - $(ProjectDir)../bin/$(PlatformTarget)/$(Configuration) - $(ProjectDir)../obj/$(PlatformTarget)/$(Configuration)/$(ProjectName)/ - _dxapi - .pyd + - - - Level3 - Disabled - true - - - - - Level3 - Disabled - true - - - - - Level3 - Disabled - true - - - - - Level3 - Disabled - true - - - + Level3 Disabled false - $(PYTHON36_HOME)\include;$(ProjectDir)..\src;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;%(AdditionalIncludeDirectories) + $(PYTHON36_HOME)\include;$(ProjectDir)..\src;$(ProjectDir)..\dxapi\include\native;$(ProjectDir)..\dxapi\include\native\dxapi;%(AdditionalIncludeDirectories) MultiThreadedDebug _DEBUG;%(PreprocessorDefinitions) @@ -437,10 +326,10 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" _DEBUG;%(PreprocessorDefinitions) - python36.lib;%(AdditionalDependencies) + python37.lib;%(AdditionalDependencies) - S:\programing\tools\python3.6\libs;%(AdditionalLibraryDirectories) + S:\programing\tools\python3.7\libs;%(AdditionalLibraryDirectories) python37.lib;dxapi-x64d.lib;dfp.lib;%(AdditionalDependencies) @@ -457,119 +346,99 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" _DEBUG;%(PreprocessorDefinitions) - python36.lib;%(AdditionalDependencies) + python38.lib;%(AdditionalDependencies) - S:\programing\tools\python3.6\libs;%(AdditionalLibraryDirectories) + S:\programing\tools\python3.8\libs;%(AdditionalLibraryDirectories) python38.lib;dxapi-x64d.lib;dfp.lib;%(AdditionalDependencies) $(ProjectDir)..\..\dfp\bin\Windows\Release\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON38_HOME)\libs;%(AdditionalLibraryDirectories) - + Level3 Disabled false - $(PYTHON27_HOME)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) + $(PYTHON39_HOME)\include;$(ProjectDir)..\src;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;%(AdditionalIncludeDirectories) MultiThreadedDebug + _DEBUG;%(PreprocessorDefinitions) - python36.lib;%(AdditionalDependencies) + python39.lib;%(AdditionalDependencies) - S:\programing\tools\python3.6\libs;%(AdditionalLibraryDirectories) + S:\programing\tools\python3.9\libs;%(AdditionalLibraryDirectories) - python27.lib;dxapi-x64d.lib;dfp.lib;%(AdditionalDependencies) - $(ProjectDir)..\..\dfp\bin\Windows\Debug\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON27_HOME)\libs;%(AdditionalLibraryDirectories) - - - - - Level3 - MaxSpeed - true - true - false - $(PYTHON_HOME_X86)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) - FastCall - - - true - true - $(ProjectDir)..\..\dxapi\bin;$(PYTHON_HOME_X86)\libs;%(AdditionalLibraryDirectories) - python3.lib;dxapi-x86.lib;%(AdditionalDependencies) - false + python38.lib;dxapi-x64d.lib;dfp.lib;%(AdditionalDependencies) + $(ProjectDir)..\..\dfp\bin\Windows\Release\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON39_HOME)\libs;%(AdditionalLibraryDirectories) - + Level3 - MaxSpeed - true - true + Disabled false - $(PYTHON_HOME_X86)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) - FastCall + $(PYTHON310_HOME)\include;$(ProjectDir)..\src;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;%(AdditionalIncludeDirectories) + MultiThreadedDebug + _DEBUG;%(PreprocessorDefinitions) + + python310.lib;%(AdditionalDependencies) + + + S:\programing\tools\python3.10\libs;%(AdditionalLibraryDirectories) + - true - true - $(ProjectDir)..\..\dxapi\bin;$(PYTHON_HOME_X86)\libs;%(AdditionalLibraryDirectories) - python3.lib;dxapi-x86.lib;%(AdditionalDependencies) - false + python310.lib;dxapi-x64d.lib;dfp.lib;%(AdditionalDependencies) + $(ProjectDir)..\..\dfp\bin\Windows\Release\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON310_HOME)\libs;%(AdditionalLibraryDirectories) - + Level3 MaxSpeed true true false - $(PYTHON_HOME_X86)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) + $(PYTHON36_HOME)\include;$(ProjectDir)..\dxapi\include\native;$(ProjectDir)..\dxapi\include\native\dxapi;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) + MultiThreadedDLL FastCall true true - $(ProjectDir)..\..\dxapi\bin;$(PYTHON_HOME_X86)\libs;%(AdditionalLibraryDirectories) - python3.lib;dxapi-x86.lib;%(AdditionalDependencies) + python36.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) + $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\dxapi\bin;$(PYTHON36_HOME)\libs;%(AdditionalLibraryDirectories) false + /NODEFAULTLIB:LIBCMT %(AdditionalOptions) + + + + + + copy /Y "$(SolutionDir)dfp\lib\windows\64\DecimalNative.dll" "$(OutputPath)DecimalNative.dll" + - - - Level3 - MaxSpeed - true - true - true - FastCall - - - true - true - - - + Level3 MaxSpeed true true false - $(PYTHON36_HOME)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) + $(PYTHON37_HOME)\include;$(ProjectDir)..\dxapi\include\native;$(ProjectDir)..\dxapi\include\native\dxapi;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) MultiThreadedDLL FastCall true true - python36.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) - $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON36_HOME)\libs;%(AdditionalLibraryDirectories) + python37.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) + $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\dxapi\bin;$(PYTHON37_HOME)\libs;%(AdditionalLibraryDirectories) false /NODEFAULTLIB:LIBCMT %(AdditionalOptions) @@ -581,22 +450,22 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" copy /Y "$(SolutionDir)dfp\lib\windows\64\DecimalNative.dll" "$(OutputPath)DecimalNative.dll" - + Level3 MaxSpeed true true false - $(PYTHON37_HOME)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) + $(PYTHON38_HOME)\include;$(ProjectDir)..\dxapi\include\native;$(ProjectDir)..\dxapi\include\native\dxapi;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) MultiThreadedDLL FastCall true true - python37.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) - $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON37_HOME)\libs;%(AdditionalLibraryDirectories) + python38.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) + $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\dxapi\bin;$(PYTHON38_HOME)\libs;%(AdditionalLibraryDirectories) false /NODEFAULTLIB:LIBCMT %(AdditionalOptions) @@ -608,22 +477,22 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" copy /Y "$(SolutionDir)dfp\lib\windows\64\DecimalNative.dll" "$(OutputPath)DecimalNative.dll" - + Level3 MaxSpeed true true false - $(PYTHON38_HOME)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) + $(PYTHON39_HOME)\include;$(ProjectDir)..\dxapi\include\native;$(ProjectDir)..\dxapi\include\native\dxapi;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) MultiThreadedDLL FastCall true true - python38.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) - $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON38_HOME)\libs;%(AdditionalLibraryDirectories) + python39.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) + $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\dxapi\bin;$(PYTHON39_HOME)\libs;%(AdditionalLibraryDirectories) false /NODEFAULTLIB:LIBCMT %(AdditionalOptions) @@ -635,22 +504,22 @@ ren "$(ProjectDir)..\bin\debug\dxapi.py" "__init__.py" copy /Y "$(SolutionDir)dfp\lib\windows\64\DecimalNative.dll" "$(OutputPath)DecimalNative.dll" - + Level3 MaxSpeed true true false - $(PYTHON27_HOME)\include;$(ProjectDir)..\..\dxapi\include\native;$(ProjectDir)..\..\dxapi\include\native\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi;$(ProjectDir)..\..\dxapi\src\dxapi\native;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) + $(PYTHON310_HOME)\include;$(ProjectDir)..\dxapi\include\native;$(ProjectDir)..\dxapi\include\native\dxapi;$(ProjectDir)..\src;%(AdditionalIncludeDirectories) MultiThreadedDLL FastCall true true - python27.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) - $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\..\dxapi\bin;$(PYTHON27_HOME)\libs;%(AdditionalLibraryDirectories) + python310.lib;dxapi-x64.lib;DecimalNative.lib;%(AdditionalDependencies) + $(ProjectDir)..\dfp\lib\windows\64;$(ProjectDir)..\dxapi\bin;$(PYTHON310_HOME)\libs;%(AdditionalLibraryDirectories) false /NODEFAULTLIB:LIBCMT %(AdditionalOptions) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8183238 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +license_files = LICENSE diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..37d5651 --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +import os.path as path +from setuptools import setup, find_packages, dist +import sys + +with open(path.join(path.abspath(path.dirname(__file__)), 'README.md')) as f: + long_description = f.read() + +project_version = open('version.txt', 'r').read() + +setup(name='dxapi', + version=project_version, + packages=find_packages(), + package_data={'dxapi': ['windows/x64/py27/*.pyd', + 'windows/x64/py36/*.pyd', + 'windows/x64/py37/*.pyd', + 'windows/x64/py38/*.pyd', + 'windows/x64/py27/*.dll', + 'windows/x64/py36/*.dll', + 'windows/x64/py37/*.dll', + 'windows/x64/py38/*.dll', + 'linux/x64/py27/*.so', + 'linux/x64/py36/*.so', + 'linux/x64/py37/*.so', + 'linux/x64/py38/*.so', + 'darwin/x64/py27/*.so', + 'darwin/x64/py36/*.so', + 'darwin/x64/py37/*.so', + 'darwin/x64/py38/*.so', + 'darwin/x64/py27/*.dylib', + 'darwin/x64/py36/*.dylib', + 'darwin/x64/py37/*.dylib', + 'darwin/x64/py38/*.dylib']}, + include_package_data=True, + zip_safe=False, + description='Python Timebase client API', + long_description='Python Timebase client API', + long_description_content_type='text/markdown', + keywords='timebase, database', + #url='', + author='Epam', + classifiers=[ + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8" + ], + python_requires='>=2.7,!=2.8.*,!=2.9.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,<3.9', + platforms=['Windows', 'Linux', 'MacOS'] +) diff --git a/src/codecs/field_codecs.h b/src/codecs/field_codecs.h index 5fb74c5..cc09831 100644 --- a/src/codecs/field_codecs.h +++ b/src/codecs/field_codecs.h @@ -11,6 +11,19 @@ #include #include + +#if defined(__APPLE__) +extern "C" double toFloat64(uint64_t value); +extern "C" uint64_t fromFloat64(double value); + +double dfp_toDouble(uint64_t value) { + return toFloat64(value); +} + +uint64_t dfp_fromDouble(double value) { + return fromFloat64(value); +} +#else extern "C" double decimal_native_toFloat64(uint64_t value); extern "C" uint64_t decimal_native_fromFloat64(double value); @@ -21,6 +34,8 @@ double dfp_toDouble(uint64_t value) { uint64_t dfp_fromDouble(double value) { return decimal_native_fromFloat64(value); } +#endif + namespace DxApiImpl { namespace Python { @@ -641,7 +656,7 @@ class Utf8FieldCodec : public FieldCodec { inline PyObject * decode(DxApi::DataReader &reader) { bool hasField = reader.readUTF8(buffer); if (hasField) { - return PyUnicode_FromString(buffer.c_str()); + return PyUnicode_DecodeUTF8(buffer.c_str(), buffer.size(), "ignore"); } else { Py_RETURN_NONE; } diff --git a/src/swig/common.i b/src/swig/common.i index 339ace2..3eae96b 100644 --- a/src/swig/common.i +++ b/src/swig/common.i @@ -160,6 +160,7 @@ public: WriteMode writeMode; bool raw; bool minLatency; + std::string space; LoadingOptions(); }; diff --git a/src/swig/dxapi.i b/src/swig/dxapi.i index eef5690..1d0d6d8 100644 --- a/src/swig/dxapi.i +++ b/src/swig/dxapi.i @@ -9,14 +9,16 @@ if _swig_python_platform.startswith('linux'): elif _swig_python_platform.startswith('darwin'): platform = 'darwin' -if _swig_python_version_info >= (2, 7) and _swig_python_version_info < (2, 8): - subdir = 'py27' -elif _swig_python_version_info >= (3, 6) and _swig_python_version_info < (3, 7): +if _swig_python_version_info >= (3, 6) and _swig_python_version_info < (3, 7): subdir = 'py36' elif _swig_python_version_info >= (3, 7) and _swig_python_version_info < (3, 8): subdir = 'py37' elif _swig_python_version_info >= (3, 8) and _swig_python_version_info < (3, 9): subdir = 'py38' +elif _swig_python_version_info >= (3, 9) and _swig_python_version_info < (3, 10): + subdir = 'py39' +elif _swig_python_version_info >= (3, 10) and _swig_python_version_info < (3, 11): + subdir = 'py310' else: raise Exception('Version of python (' + str(_swig_python_version_info) + ') is not supported') @@ -201,6 +203,46 @@ typedef int64_t TimestampNs; ///-------- +%typemap(in) const std::vector & { + + /* %typemap(in) const std::vector & */ + if ($input == NULL) { + $1 = NULL; + } else if (PyList_Check($input)) { + Py_ssize_t size = PyList_Size($input); + $1 = new std::vector(); + for (int i = 0; i < size; i++) { + std::string str; + bool type_mismatch = false; + DxApiImpl::Python::getStringValue(PyList_GetItem($input,i), str, type_mismatch); + $1->push_back(str); + if (type_mismatch) { + delete $1; + PyErr_SetString(PyExc_TypeError, "list must contain strings"); + return NULL; + } + } + } else { + if ($input == Py_None) { + $1 = NULL; + } else { + PyErr_SetString(PyExc_TypeError, "not a list"); + return NULL; + } + } +} + +%typemap(typecheck) const std::vector & { + $1 = PyList_Check($input) ? 1 : 0; +} + +%typemap(freearg) const std::vector & { + + /* %typemap(freearg) const std::vector & */ + if ($1 != NULL) + delete $1; +} + %typemap(directorin) const std::vector & { /* %typemap(directorin) const std::vector & */ @@ -215,12 +257,6 @@ typedef int64_t TimestampNs; } } -//%typemap(directorin) const std::string & { - - /* %typemap(directorin) const std::string & */ -// $input = PyUnicode_FromString($1.c_str()); -//} - ///-------- %typemap(in) const std::vector * { diff --git a/src/swig/tick_stream.i b/src/swig/tick_stream.i index 481982c..bbad82f 100644 --- a/src/swig/tick_stream.i +++ b/src/swig/tick_stream.i @@ -35,6 +35,10 @@ public: DxApi::TickCursor * createCursor(const DxApi::SelectionOptions &options) const; DxApi::TickLoader * createLoader(const DxApi::LoadingOptions &options) const; + std::vector listSpaces() const; + void renameSpace(const std::string &newName, const std::string &oldName) const; + void deleteSpaces(const std::vector& spaces) const; + static void operator delete(void* ptr, size_t sz); //not supported by swig, will be skipped ~TickStream(); @@ -84,6 +88,26 @@ protected: return result; } } + + std::vector getTimeRange(const std::string &space) { + std::vector result(2); + + //todo: refactor it + // used array for clang linux copiler + TimestampMs range[2]; + bool not_null = $self->getTimeRange(range, space); + + if (not_null) { + result[0] = range[0]; + result[1] = range[1]; + return result; + } else { + result[0] = DxApi::TIMESTAMP_UNKNOWN; + result[1] = DxApi::TIMESTAMP_UNKNOWN; + return result; + } + } + } diff --git a/src/tick_cursor.h b/src/tick_cursor.h index 2bf0331..1ce4297 100644 --- a/src/tick_cursor.h +++ b/src/tick_cursor.h @@ -4,7 +4,7 @@ #include "Python.h" #include "python_common.h" -#include "native/tickdb/http/xml/tickdb_class_descriptor.h" +#include "dxapi.h" namespace DxApiImpl { namespace Python { diff --git a/src/tick_loader.h b/src/tick_loader.h index ce7ea6b..89bd34d 100644 --- a/src/tick_loader.h +++ b/src/tick_loader.h @@ -3,10 +3,13 @@ #include "Python.h" -#include "native/tickdb/http/xml/tickdb_class_descriptor.h" #include "python_common.h" +#include "dxapi.h" +#include "schema.h" #include +#include +#include namespace DxApiImpl { namespace Python { diff --git a/swigwin/swig-3.0.12.tar.gz b/swigwin/swig-3.0.12.tar.gz new file mode 100644 index 0000000..7aa04b6 Binary files /dev/null and b/swigwin/swig-3.0.12.tar.gz differ diff --git a/swigwin/swigwin-3.0.12.zip b/swigwin/swigwin-3.0.12.zip new file mode 100644 index 0000000..7adb60e Binary files /dev/null and b/swigwin/swigwin-3.0.12.zip differ diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..d3ff2d1 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,3 @@ +reports/ +dxapi/ +__pycache__/ \ No newline at end of file diff --git a/tests/HTMLTestRunner.py b/tests/HTMLTestRunner.py new file mode 100644 index 0000000..fa5db32 --- /dev/null +++ b/tests/HTMLTestRunner.py @@ -0,0 +1,775 @@ +__version__ = "1.0.2" + +from datetime import datetime +try: + from StringIO import StringIO +except ImportError: + from io import StringIO +from unittest import TestResult, TestProgram +from xml.sax import saxutils +import sys + +py3 = sys.version.startswith("3") + +# ------------------------------------------------------------------------ +# The redirectors below are used to capture output during testing. Output +# sent to stdout and stderr are automatically captured. However +# in some cases stdout is already cached before HTMLTestRunner is +# invoked (e.g. calling logging.basicConfig). In order to capture those +# output, use the redirectors for the cached stream. +# +# e.g. +# >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) +# >>> + +def to_unicode(s): + try: + return str(s) + except UnicodeDecodeError: + # s is non ascii byte string + return s.decode('unicode_escape') + +class OutputRedirector(object): + """ Wrapper to redirect stdout or stderr """ + def __init__(self, fp): + self.fp = fp + + def write(self, s): + self.fp.write(to_unicode(s)) + + def writelines(self, lines): + lines = map(to_unicode, lines) + self.fp.writelines(lines) + + def flush(self): + self.fp.flush() + +stdout_redirector = OutputRedirector(sys.stdout) +stderr_redirector = OutputRedirector(sys.stderr) + + + +# ---------------------------------------------------------------------- +# Template + +class Template_mixin(object): + """ + Define a HTML template for report customerization and generation. + + Overall structure of an HTML report + + HTML + +------------------------+ + | | + | | + | | + | STYLESHEET | + | +----------------+ | + | | | | + | +----------------+ | + | | + | | + | | + | | + | | + | HEADING | + | +----------------+ | + | | | | + | +----------------+ | + | | + | REPORT | + | +----------------+ | + | | | | + | +----------------+ | + | | + | ENDING | + | +----------------+ | + | | | | + | +----------------+ | + | | + | | + | | + +------------------------+ + """ + + STATUS = { + 0: 'pass', + 1: 'fail', + 2: 'error', + 3: 'skip', + } + + DEFAULT_TITLE = 'Unit Test Report' + DEFAULT_DESCRIPTION = '' + + # ------------------------------------------------------------------------ + # HTML Template + + HTML_TMPL = r""" + + + + %(title)s + + + %(stylesheet)s + + + + +%(heading)s +%(report)s +%(ending)s + + + +""" + # variables: (title, generator, stylesheet, heading, report, ending) + + + # ------------------------------------------------------------------------ + # Stylesheet + # + # alternatively use a for external style sheet, e.g. + # + + STYLESHEET_TMPL = """ + +""" + + + + # ------------------------------------------------------------------------ + # Heading + # + + HEADING_TMPL = """
+

%(title)s

+%(parameters)s +

%(description)s

+
+ +""" # variables: (title, parameters, description) + + HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

+""" # variables: (name, value) + + + + # ------------------------------------------------------------------------ + # Report + # + + REPORT_TMPL = """ +

Show +Summary +Failed +All +

+ ++++++++ + + + + + + + + + +%(test_list)s + + + + + + + + + +
Test Group/Test caseCountPassSkipFailErrorView
Total%(count)s%(Pass)s%(skip)s%(fail)s%(error)s 
+""" # variables: (test_list, count, Pass, fail, error) + + REPORT_CLASS_TMPL = r""" + + %(desc)s + %(count)s + %(Pass)s + %(skip)s + %(fail)s + %(error)s + Detail + +""" # variables: (style, desc, count, Pass, fail, error, cid) + + + REPORT_TEST_WITH_OUTPUT_TMPL = r""" + +
%(desc)s
+ + + + + %(status)s + + + + + + +""" # variables: (tid, Class, style, desc, status) + + + REPORT_TEST_NO_OUTPUT_TMPL = r""" + +
%(desc)s
+ %(status)s + +""" # variables: (tid, Class, style, desc, status) + + + REPORT_TEST_OUTPUT_TMPL = r""" +%(id)s: %(output)s +""" # variables: (id, output) + + + + # ------------------------------------------------------------------------ + # ENDING + # + + ENDING_TMPL = """
 
""" + +# -------------------- The end of the Template class ------------------- + + +class _TestResult(TestResult): + # note: _TestResult is a pure representation of results. + # It lacks the output and reporting ability compares to unittest._TextTestResult. + + def __init__(self, verbosity=1): + TestResult.__init__(self) + self.outputBuffer = StringIO() + self.stdout0 = None + self.stderr0 = None + self.success_count = 0 + self.skip_count = 0 + self.failure_count = 0 + self.error_count = 0 + self.verbosity = verbosity + + # result is a list of result in 4 tuple + # ( + # result code (0: success; 1: fail; 2: error), + # TestCase object, + # Test output (byte string), + # stack trace, + # ) + self.result = [] + + + def startTest(self, test): + TestResult.startTest(self, test) + # just one buffer for both stdout and stderr + stdout_redirector.fp = self.outputBuffer + stderr_redirector.fp = self.outputBuffer + self.stdout0 = sys.stdout + self.stderr0 = sys.stderr + sys.stdout = stdout_redirector + sys.stderr = stderr_redirector + + + def complete_output(self): + """ + Disconnect output redirection and return buffer. + Safe to call multiple times. + """ + if self.stdout0: + sys.stdout = self.stdout0 + sys.stderr = self.stderr0 + self.stdout0 = None + self.stderr0 = None + return self.outputBuffer.getvalue() + + + def stopTest(self, test): + # Usually one of addSuccess, addError or addFailure would have been called. + # But there are some path in unittest that would bypass this. + # We must disconnect stdout in stopTest(), which is guaranteed to be called. + self.complete_output() + + + def addSuccess(self, test): + self.success_count += 1 + TestResult.addSuccess(self, test) + output = self.complete_output() + self.result.append((0, test, output, '')) + if self.verbosity > 1: + sys.stderr.write('ok ') + sys.stderr.write(str(test)) + sys.stderr.write('\n') + else: + sys.stderr.write('.') + + def addError(self, test, err): + self.error_count += 1 + TestResult.addError(self, test, err) + _exc_str = self.errors[-1][1] + output = self.complete_output() + self.result.append((2, test, output, _exc_str)) + if self.verbosity > 1: + sys.stderr.write('E ') + sys.stderr.write(str(test)) + sys.stderr.write('\n') + else: + sys.stderr.write('E') + + def addFailure(self, test, err): + self.failure_count += 1 + TestResult.addFailure(self, test, err) + _exc_str = self.failures[-1][1] + output = self.complete_output() + self.result.append((1, test, output, _exc_str)) + if self.verbosity > 1: + sys.stderr.write('F ') + sys.stderr.write(str(test)) + sys.stderr.write('\n') + else: + sys.stderr.write('F') + + def addSkip(self, test, err): + self.skip_count += 1 + TestResult.addSkip(self, test, err) + _exc_str = self.skipped[-1][1] + output = self.complete_output() + self.result.append((3, test, output, _exc_str)) + if self.verbosity > 1: + sys.stderr.write('S ') + sys.stderr.write(str(test)) + sys.stderr.write('\n') + else: + sys.stderr.write('S') + + +class HTMLTestRunner(Template_mixin): + """ + """ + def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): + self.stream = stream + self.verbosity = verbosity + if title is None: + self.title = self.DEFAULT_TITLE + else: + self.title = title + if description is None: + self.description = self.DEFAULT_DESCRIPTION + else: + self.description = description + + self.startTime = datetime.now().replace(microsecond=0) + + + def run(self, test): + "Run the given test case or test suite." + result = _TestResult(self.verbosity) + test(result) + self.stopTime = datetime.now().replace(microsecond=0) + self.generateReport(test, result) + print('Time Elapsed: {}'.format((self.stopTime-self.startTime))) + return result + + + def sortResult(self, result_list): + # unittest does not seems to run in any particular order. + # Here at least we want to group them together by class. + rmap = {} + classes = [] + for n,t,o,e in result_list: + cls = t.__class__ + if not cls in rmap: + rmap[cls] = [] + classes.append(cls) + rmap[cls].append((n,t,o,e)) + r = [(cls, rmap[cls]) for cls in classes] + return r + + + def getReportAttributes(self, result): + """ + Return report attributes as a list of (name, value). + Override this to add custom attributes. + """ + startTime = str(self.startTime) + duration = str(self.stopTime - self.startTime) + status = [] + if result.success_count: status.append('Pass %s' % result.success_count) + if result.skip_count: status.append('Skip %s' % result.skip_count ) + if result.failure_count: status.append('Failure %s' % result.failure_count) + if result.error_count: status.append('Error %s' % result.error_count ) + if status: + status = ' '.join(status) + else: + status = 'none' + return [ + ('Start Time', startTime), + ('Duration', duration), + ('Status', status), + ] + + + def generateReport(self, test, result): + report_attrs = self.getReportAttributes(result) + generator = 'HTMLTestRunner %s' % __version__ + stylesheet = self._generate_stylesheet() + heading = self._generate_heading(report_attrs) + report = self._generate_report(result) + ending = self._generate_ending() + output = self.HTML_TMPL % dict( + title = saxutils.escape(self.title), + generator = generator, + stylesheet = stylesheet, + heading = heading, + report = report, + ending = ending, + ) + self.stream.write(output) + + + def _generate_stylesheet(self): + return self.STYLESHEET_TMPL + + + def _generate_heading(self, report_attrs): + a_lines = [] + for name, value in report_attrs: + line = self.HEADING_ATTRIBUTE_TMPL % dict( + name = saxutils.escape(name), + value = saxutils.escape(value), + ) + a_lines.append(line) + heading = self.HEADING_TMPL % dict( + title = saxutils.escape(self.title), + parameters = ''.join(a_lines), + description = saxutils.escape(self.description), + ) + return heading + + + def _generate_report(self, result): + rows = [] + sortedResult = self.sortResult(result.result) + for cid, (cls, cls_results) in enumerate(sortedResult): + # subtotal for a class + np = ns = nf = ne = 0 + for n,t,o,e in cls_results: + if n == 0: np += 1 + elif n == 1: nf += 1 + elif n == 2: ne += 1 + elif n == 3: ns += 1 + + # format class description + if cls.__module__ == "__main__": + name = cls.__name__ + else: + name = "%s.%s" % (cls.__module__, cls.__name__) + doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" + desc = doc and '%s: %s' % (name, doc) or name + + row = self.REPORT_CLASS_TMPL % dict( + style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or ns > 0 and 'skipClass' or 'passClass', + desc = desc, + count = np+ns+nf+ne, + Pass = np, + fail = nf, + error = ne, + skip = 0, + cid = 'c%s' % (cid+1) + ) + rows.append(row) + + for tid, (n,t,o,e) in enumerate(cls_results): + self._generate_report_test(rows, cid, tid, n, t, o, e) + + report = self.REPORT_TMPL % dict( + test_list = ''.join(rows), + count = str(result.success_count+result.failure_count+result.error_count+result.skip_count), + Pass = str(result.success_count), + skip = str(result.skip_count), + fail = str(result.failure_count), + error = str(result.error_count), + ) + return report + + + def _generate_report_test(self, rows, cid, tid, n, t, o, e): + # e.g. 'pt1.1', 'ft1.1', etc + has_output = bool(o or e) + tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) + name = t.id().split('.')[-1] + doc = t.shortDescription() or "" + desc = doc and ('%s: %s' % (name, doc)) or name + tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL + + # o and e should be byte string because they are collected from stdout and stderr? + if isinstance(o,str): + # TODO: some problem with 'string_escape': it escape \n and mess up formating + # uo = unicode(o.encode('string_escape')) + if py3: + uo = bytes(o, 'utf-8').decode('ascii', errors='ignore') + else: + uo = o.decode('ascii', errors='ignore') + else: + uo = o + if isinstance(e,str): + # TODO: some problem with 'string_escape': it escape \n and mess up formating + # ue = unicode(e.encode('string_escape')) + if py3: + ue = bytes(e, 'utf-8').decode('ascii', errors='ignore') + else: + ue = e.decode('ascii', errors='ignore') + else: + ue = e + + print("OUTPUT: " + uo) + print("ERROR: " + ue) + script = self.REPORT_TEST_OUTPUT_TMPL % dict( + id = tid, + output = saxutils.escape(uo+ue), + ) + + row = tmpl % dict( + tid = tid, + Class = (n == 0 and 'hiddenRow' or 'none'), + style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), + desc = desc, + script = script, + status = self.STATUS[n], + ) + rows.append(row) + if not has_output: + return + + def _generate_ending(self): + return self.ENDING_TMPL + + +############################################################################## +# Facilities for running tests from the command line +############################################################################## + +# Note: Reuse unittest.TestProgram to launch test. In the future we may +# build our own launcher to support more specific command line +# parameters like test title, CSS, etc. +class _TestProgram(TestProgram): + """ + A variation of the unittest.TestProgram. Please refer to the base + class for command line parameters. + """ + def runTests(self): + # Pick HTMLTestRunner as the default test runner. + # base class's testRunner parameter is not useful because it means + # we have to instantiate HTMLTestRunner before we know self.verbosity. + if self.testRunner is None: + self.testRunner = HTMLTestRunner(verbosity=self.verbosity) + TestProgram.runTests(self) + +main = _TestProgram + +############################################################################## +# Executing this module from the command line +############################################################################## + +if __name__ == "__main__": + main(module=None) diff --git a/tests/TestAll.py b/tests/TestAll.py new file mode 100644 index 0000000..984b98e --- /dev/null +++ b/tests/TestAll.py @@ -0,0 +1,58 @@ +import unittest +import datetime +import sys, os + +#from HtmlTestRunner import HTMLTestRunner +import HTMLTestRunner as HTMLTestRunner + +testmodules = [ + 'TestTickDB', + 'TestStream', + 'TestLoader', + 'TestCursor', + 'TestPackageHeader', + 'TestQQL', + 'TestMemoryManagement', + 'TestNextIfAvailable', + 'TestMultithreaded' +] + +suite = unittest.TestSuite() + +for t in testmodules: + try: + # If the module defines a suite() function, call it to get the suite. + mod = __import__(t, globals(), locals(), ['suite']) + suitefn = getattr(mod, 'suite') + suite.addTest(suitefn()) + except (ImportError, AttributeError): + # else, just load all the test cases from the module. + suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t)) + +testdir = os.path.dirname(__file__) +if testdir != "": + testdir = testdir + '/' +reportsDir = testdir + 'reports' +if not os.path.exists(reportsDir): + os.makedirs(reportsDir) +reportFile = reportsDir + '/dxapi-test-report-' + datetime.datetime.now().strftime('%d%m%Y-%H%M%S') + '.html' +fileStream = open(reportFile, 'w') +version = "2" +if sys.version.startswith("3.6"): + version = "3.6" +elif sys.version.startswith("3.7"): + version = "3.7" +title = 'Dxapi test report (Python version: ' + (version) + ')' +runner = HTMLTestRunner.HTMLTestRunner(stream = fileStream, title = title) +result = runner.run(suite) + +print('Tests finished') +print('Errors: ' + str(result.error_count) + ", Failures: " + str(result.failure_count) + ", Tests OK: " + str(result.success_count)) +print('See reports in "' + reportFile + '"') + +if result.error_count > 0 or result.failure_count > 0: + sys.exit(-1) +else: + sys.exit(0) + + diff --git a/tests/TestCursor.py b/tests/TestCursor.py new file mode 100644 index 0000000..51e4226 --- /dev/null +++ b/tests/TestCursor.py @@ -0,0 +1,257 @@ +import unittest +import servertest +import testutils +import dxapi + +class CursorTest(servertest.TestWithStreams): + + def test_ReadSmoke(self): + for key in self.streamKeys: + self.assertEqual(testutils.readStream(self.db.getStream(key)), 10000) + + def test_SelectFromTo(self): + stream = self.db.getStream(self.streamKeys[0]) + options = dxapi.SelectionOptions() + options._from = 5000 + options.to = 6000 + cursor = stream.createCursor(options) + + self.assertTrue(cursor.next()) + message = cursor.getMessage() + self.assertEqual(message.timestamp, 5000000000) + + self.assertTrue(cursor.next()) + message = cursor.getMessage() + self.assertEqual(message.timestamp, 6000000000) + + self.assertFalse(cursor.next()) + + cursor.close() + + + def test_SelectFromToReverse(self): + stream = self.db.getStream(self.streamKeys[0]) + options = dxapi.SelectionOptions() + options._from = 5000 + options.to = 6000 + options.reverse = True + cursor = stream.createCursor(options) + + self.assertTrue(cursor.next()) + message = cursor.getMessage() + self.assertEqual(message.timestamp, 6000000000) + + self.assertTrue(cursor.next()) + message = cursor.getMessage() + self.assertEqual(message.timestamp, 5000000000) + + self.assertFalse(cursor.next()) + + cursor.close() + + def test_Reset(self): + stream = self.db.getStream(self.streamKeys[0]) + cursor = stream.createCursor(dxapi.SelectionOptions()) + + cursor.reset(9000) + self.assertTrue(cursor.next()) + self.assertEqual(cursor.getMessage().timestamp, self.msToNs(9000)) + + cursor.reset(5000) + self.assertTrue(cursor.next()) + self.assertEqual(cursor.getMessage().timestamp, self.msToNs(5000)) + + cursor.reset(100000) + self.checkCursorSymbols(cursor, set(self.entities.keys())) + + cursor.reset(90000, [self.entities['AAPL'], self.entities['IBM']]) + self.checkCursorSymbols(cursor, set(['AAPL', 'IBM'])) + + cursor.close() + + def test_SetTimeForNewSubscription(self): + barStream = self.db.getStream(self.streamKeys[0]) + cursor = self.db.select(0, [barStream], dxapi.SelectionOptions(), None, []) + + cursor.addEntities([self.entities['AAPL']]) + messages = self.read(cursor, 10) + for m in messages: + print(str(m.timestamp)) + self.assertGreaterEqual(m.timestamp, 0) + self.assertLessEqual(m.timestamp, 30000000000) + + cursor.removeEntities([self.entities['AAPL']]) + cursor.setTimeForNewSubscriptions(100000) + cursor.addEntities([self.entities['GOOG']]) + messages = self.read(cursor, 10) + for m in messages: + print(str(m.timestamp)) + self.assertGreaterEqual(m.timestamp, 100000000000) + self.assertLessEqual(m.timestamp, 100000000000 + 3 * 10 * 1000000000) + + cursor.removeEntities([self.entities['GOOG']]) + cursor.setTimeForNewSubscriptions(50000) + cursor.addEntities([self.entities['IBM']]) + messages = self.read(cursor, 10) + for m in messages: + print(str(m.timestamp)) + self.assertGreaterEqual(m.timestamp, 50000000000) + self.assertLessEqual(m.timestamp, 50000000000 + 3 * 10 * 1000000000) + + def test_SubscribeTypes(self): + barStream = self.db.getStream(self.streamKeys[0]) + tradeBBOStream = self.db.getStream(self.streamKeys[1]) + l2Stream = self.db.getStream(self.streamKeys[2]) + cursor = self.db.select(0, [tradeBBOStream, barStream, l2Stream], dxapi.SelectionOptions(), None, None) + + # all types + typeSet = set(self.types.values()) + self.checkCursorTypes(cursor, typeSet) + + # remove types + self.removeAll(typeSet, [self.types['bbo'], self.types['bar']]) + cursor.removeTypes([self.types['bbo'], self.types['bar']]) + self.checkCursorTypes(cursor, typeSet) + + # add types + typeSet.add(self.types['bbo']) + cursor.addTypes([self.types['bbo']]) + self.checkCursorTypes(cursor, typeSet) + + # 'trade' and 'bars' + cursor.setTypes([self.types['trade'], self.types['bar']]) + self.checkCursorTypes(cursor, set([self.types['trade'], self.types['bar']])) + + # none types + cursor.removeTypes([self.types['trade'], self.types['bar']]) + cursor.reset(0) + self.assertFalse(cursor.next()) + + # all types + cursor.subscribeToAllTypes() + cursor.reset(0) + self.checkCursorTypes(cursor, set(self.types.values())) + + cursor.close() + + def test_SubscribeEntities(self): + tradeBBOStream = self.db.getStream(self.streamKeys[0]) + barStream = self.db.getStream(self.streamKeys[1]) + cursor = self.db.select(0, [tradeBBOStream, barStream], dxapi.SelectionOptions(), None, None) + + # all entities + entitySet = set(self.entities.keys()) + self.checkCursorSymbols(cursor, entitySet) + + # remove AAPL, GOOG + self.removeAll(entitySet, ['AAPL', 'GOOG']) + cursor.removeEntities([self.entities['AAPL'], self.entities['GOOG']]) + self.checkCursorSymbols(cursor, entitySet) + + # add GOOG + entitySet.add('GOOG') + cursor.addEntity(self.entities['GOOG']) + self.checkCursorSymbols(cursor, entitySet) + + # remove IBM + entitySet.remove('IBM') + cursor.removeEntity(self.entities['IBM']) + self.checkCursorSymbols(cursor, entitySet) + + # add AAPL, IBM + self.addAll(entitySet, ['AAPL', 'IBM']) + cursor.addEntities([self.entities['AAPL'], self.entities['IBM']]) + self.checkCursorSymbols(cursor, entitySet) + + # clear all + cursor.clearAllEntities() + self.assertFalse(cursor.next()) + + # subscribe all + cursor.subscribeToAllEntities() + cursor.reset(0) + self.checkCursorSymbols(cursor, set(self.entities.keys())) + + cursor.close() + + def test_SubscribeTypeAndEntities(self): + barStream = self.db.getStream(self.streamKeys[0]) + tradeBBOStream = self.db.getStream(self.streamKeys[1]) + l2Stream = self.db.getStream(self.streamKeys[2]) + cursor = self.db.select(0, + [tradeBBOStream, barStream, l2Stream], + dxapi.SelectionOptions(), + [self.types['bbo'], self.types['trade']], + [self.entities['AAPL'], self.entities['IBM']]) + + # + typeSet = set([self.types['bbo'], self.types['trade']]) + entitySet = set(['AAPL', 'IBM']) + self.checkCursorTypesAndSymbols(cursor, typeSet, entitySet) + + # remove bbo type and ibm entity + typeSet.remove(self.types['bbo']) + entitySet.remove('IBM') + cursor.remove([self.types['bbo']], [self.entities['IBM']]) + self.checkCursorTypesAndSymbols(cursor, typeSet, entitySet) + + # remove all + cursor.remove(list(self.types.values()), list(self.entities.values())) + self.assertFalse(cursor.next()) + + # add types and some entities + cursor.add(list(self.types.values()), [self.entities['GOOG'], self.entities['IBM']]) + cursor.reset(0) + self.checkCursorTypesAndSymbols(cursor, set(self.types.values()), set(['GOOG', 'IBM'])) + + cursor.close() + + def test_SubscribeStreams(self): + barStream = self.db.getStream(self.streamKeys[0]) + tradeBBOStream = self.db.getStream(self.streamKeys[1]) + l2Stream = self.db.getStream(self.streamKeys[2]) + + cursor = self.db.select(0, [tradeBBOStream], dxapi.SelectionOptions(), None, None) + + # trade streams + typeSet = set([self.types['trade'], self.types['bbo']]) + self.checkCursorTypes(cursor, typeSet) + + # add l2 and bars streams subscription + self.addAll(typeSet, [self.types['l2'], self.types['bar']]) + cursor.addStreams([l2Stream, barStream]) + cursor.reset(0) # why we must reset here? + self.checkCursorTypes(cursor, typeSet) + + # remove tradeBBO and l2 + self.removeAll(typeSet, [self.types['trade'], self.types['bbo'], self.types['l2']]) + cursor.removeStreams([l2Stream, tradeBBOStream]) + cursor.reset(0) # why we must reset here? + self.checkCursorTypes(cursor, typeSet) + + # remove all + cursor.removeAllStreams() + cursor.reset(0) # why we must reset here? + self.assertFalse(cursor.next()) + + cursor.close() + + def test_GetCurrentStreamKey(self): + barStream = self.db.getStream(self.streamKeys[0]) + tradeBBOStream = self.db.getStream(self.streamKeys[1]) + l2Stream = self.db.getStream(self.streamKeys[2]) + + cursor = self.db.select(0, [barStream, tradeBBOStream, l2Stream], dxapi.SelectionOptions(), None, None) + while cursor.next(): + stream = cursor.getCurrentStreamKey() + typeName = cursor.getMessage().typeName + + if "L2Message" in typeName: + self.assertEqual(stream, "l2") + elif "TradeMessage" in typeName or "BestBidOfferMessage" in typeName: + self.assertEqual(stream, "tradeBBO") + elif "BarMessage" in typeName: + self.assertEqual(stream, "bars1min") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/TestEntities.py b/tests/TestEntities.py new file mode 100644 index 0000000..85a1972 --- /dev/null +++ b/tests/TestEntities.py @@ -0,0 +1,40 @@ +import unittest +import servertest +import generators +import testutils +import time +import dxapi + +class TestEntities(servertest.TBServerTest): + + def test_LoadManyEntities(self): + stream = self.createStream('bars1min', False) + loader = stream.createLoader(dxapi.LoadingOptions()) + try: + loadCount = 0 + generator = generators.BarGenerator(0, 0, 35000, ['AAPL']) + symbol = 'SYM_' + while generator.next(): + tradeMessage = generator.getMessage() + tradeMessage.symbol = symbol + str(loadCount) + loader.send(tradeMessage) + loadCount = loadCount + 1 + if loadCount % 5000 == 0: + print("Loaded " + str(loadCount) + " messages") + + print("Total loaded " + str(loadCount) + " messages") + finally: + if loader != None: + loader.close() + + cursor = stream.select(0, dxapi.SelectionOptions(), None, None) + count = 0 + while cursor.next(): + count += 1 + if count % 5000 == 0: + print("Read " + str(count) + " messages") + print("Read " + str(count) + " messages") + self.assertEqual(count, 35000) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/TestLoader.py b/tests/TestLoader.py new file mode 100644 index 0000000..cfca344 --- /dev/null +++ b/tests/TestLoader.py @@ -0,0 +1,225 @@ +import unittest +import servertest +import testutils, generators +import time +import dxapi + + +class TestLoader(servertest.TBServerTest): + + streamKeys = [ + 'bars1min', 'tradeBBO', 'l2' + ] + + def test_LoadFixed(self): + key = self.streamKeys[0] + try: + stream = self.createStreamQQL(key) + self.assertIsNotNone(stream) + self.assertEqual(self.streamCount(key), 0) # check stream is empty + + count = 12345 + loadCount = testutils.loadBars(stream, count) + self.assertEqual(count, loadCount) + readCount = self.streamCount(key) + self.assertEqual(readCount, loadCount) + finally: + self.deleteStream(key) + + def test_LoadPolymorphic(self): + key = self.streamKeys[1] + try: + stream = self.createStreamQQL(key) + self.assertIsNotNone(stream) + self.assertEqual(self.streamCount(key), 0) # check stream is empty + + count = 12354 + loadCount = testutils.loadTradeBBO(stream, count) + self.assertEqual(count * 2, loadCount) + readCount = self.streamCount(key) + self.assertEqual(readCount, loadCount) + finally: + self.deleteStream(key) + + def test_LoadL2(self): + key = self.streamKeys[2] + try: + stream = self.createStreamQQL(key) + self.assertIsNotNone(stream) + self.assertEqual(self.streamCount(key), 0) # check stream is empty + + count = 12543 + loadCount = testutils.loadL2(stream, count) + self.assertEqual(count, loadCount) + readCount = self.streamCount(key) + self.assertEqual(readCount, loadCount) + finally: + self.deleteStream(key) + + def test_registerTypesAndEntities(self): + key = self.streamKeys[1] + loader = None + cursor = None + try: + stream = self.createStreamQQL(key) + self.assertIsNotNone(stream) + self.assertEqual(self.streamCount(key), 0) # check stream is empty + + loader = stream.createLoader(dxapi.LoadingOptions()) + self.assertIsNotNone(loader) + + message = dxapi.InstrumentMessage() + + # register types + bboTypeId = loader.registerType('deltix.timebase.api.messages.BestBidOfferMessage') + tradeTypeId = loader.registerType('deltix.timebase.api.messages.TradeMessage') + + # register entities + instrument1 = loader.registerInstrument('AAAA') + instrument2 = loader.registerInstrument('BBBB') + + message.typeId = bboTypeId + + message.instrumentId = instrument1 + message.timestamp = 0 + loader.send(message) + + message.instrumentId = instrument2 + message.timestamp = 1 + loader.send(message) + + message.typeId = tradeTypeId + + message.instrumentId = instrument1 + message.timestamp = 2 + loader.send(message) + + message.instrumentId = instrument2 + message.timestamp = 3 + loader.send(message) + + loader.close() + + cursor = stream.createCursor(dxapi.SelectionOptions()) + self.assertIsNotNone(cursor) + self.assertTrue(cursor.next()) + self.assertEqual(cursor.getMessage().typeName, 'deltix.timebase.api.messages.BestBidOfferMessage') + self.assertEqual(cursor.getMessage().symbol, 'AAAA') + + self.assertTrue(cursor.next()) + self.assertEqual(cursor.getMessage().typeName, 'deltix.timebase.api.messages.BestBidOfferMessage') + self.assertEqual(cursor.getMessage().symbol, 'BBBB') + + self.assertTrue(cursor.next()) + self.assertEqual(cursor.getMessage().typeName, 'deltix.timebase.api.messages.TradeMessage') + self.assertEqual(cursor.getMessage().symbol, 'AAAA') + + self.assertTrue(cursor.next()) + self.assertEqual(cursor.getMessage().typeName, 'deltix.timebase.api.messages.TradeMessage') + self.assertEqual(cursor.getMessage().symbol, 'BBBB') + + finally: + if loader != None: + loader.close() + if cursor != None: + cursor.close() + self.deleteStream(key) + + def test_Flush(self): + key = self.streamKeys[0] + stream = self.createStreamQQL(key) + loader = stream.createLoader(dxapi.LoadingOptions()) + try: + barGenerator = generators.BarGenerator(0, 1000000000, 21, ['EPAM']) + + count = 0 + for i in range(5): + barGenerator.next() + loader.send(barGenerator.getMessage()) + time.sleep(1) + self.assertEqual(count, self.streamCount(key)) + + time.sleep(1) + self.assertEqual(count, self.streamCount(key)) + + loader.flush() + time.sleep(1) + + count += 5 + self.assertEqual(count, self.streamCount(key)) + + for i in range(5): + barGenerator.next() + loader.send(barGenerator.getMessage()) + loader.flush() + time.sleep(1) + count += 1 + self.assertEqual(count, self.streamCount(key)) + + for i in range(5): + barGenerator.next() + loader.send(barGenerator.getMessage()) + time.sleep(1) + self.assertEqual(count, self.streamCount(key)) + + time.sleep(2) + self.assertEqual(count, self.streamCount(key)) + + loader.flush() + time.sleep(1) + count += 5 + self.assertEqual(count, self.streamCount(key)) + + for i in range(100): + loader.flush() + self.assertEqual(count, self.streamCount(key)) + finally: + if loader != None: + loader.close() + self.deleteStream(key) + + def test_MassFlush(self): + key = self.streamKeys[0] + stream = self.createStreamQQL(key) + loader = stream.createLoader(dxapi.LoadingOptions()) + try: + count = 100000 + barGenerator = generators.BarGenerator(0, 1000000000, count, ['EPAM']) + while barGenerator.next(): + loader.send(barGenerator.getMessage()) + loader.flush() + self.assertEqual(count, self.streamCount(key)) + finally: + if loader != None: + loader.close() + self.deleteStream(key) + + # helpers + + def streamCount(self, key): + stream = self.db.getStream(key) + cursor = stream.createCursor(dxapi.SelectionOptions()) + cursor.reset(0) + try: + count = 0 + while cursor.next(): + count += 1 + return count + finally: + if cursor != None: + cursor.close() + + + def streamCountQQL(self, key): + cursor = self.db.executeQuery('SELECT count() as count FROM "' + key + '"') + try: + if cursor.next(): + return cursor.getMessage().COUNT + else: + return 0 + finally: + if cursor != None: + cursor.close() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/TestMemoryManagement.py b/tests/TestMemoryManagement.py new file mode 100644 index 0000000..305584d --- /dev/null +++ b/tests/TestMemoryManagement.py @@ -0,0 +1,146 @@ +import unittest +import servertest +import dxapi +import sys +import generators + +class TestTickDB(servertest.TBServerTest): + + REFCOUNT_THRESHOLD = 1000 + + streamKeys = [ + 'bars1min', 'tradeBBO', 'l2' + ] + + nStreams = 0 + + def setUp(self): + servertest.TBServerTest.setUp(self) + + for key in self.streamKeys: + stream = self.db.getStream(key) + if stream != None: + stream.deleteStream() + + self.nStreams = len(self.db.listStreams()) + + for key in self.streamKeys: + self.createStreamQQL(key) + self.assertEqual(len(self.db.listStreams()), self.nStreams + len(self.streamKeys)) + + def tearDown(self): + for key in self.streamKeys: + stream = self.db.getStream(key) + if stream != None: + stream.deleteStream() + self.assertEqual(len(self.db.listStreams()), self.nStreams) + servertest.TBServerTest.tearDown(self) + + def test_NoneTrueFalse_refcount(self): + start_refcounts = self.getNoneTrueFalseRefcount() + + self.writeL2EmptyMessages() + load_refcounts = self.getNoneTrueFalseRefcount() + self.checkRefcountsDelta(start_refcounts, load_refcounts) + + self.readL2EmptyMessages() + read_refcount = self.getNoneTrueFalseRefcount() + self.checkRefcountsDelta(start_refcounts, read_refcount) + + def printNonTrueFalseRefcounts(self, refcounts): + print("None count = " + str(refcounts[0]) + ", True count = " + str(refcounts[1]) + ", False count = " + str(refcounts[2])) + + def getNoneTrueFalseRefcount(self): + refcounts = (sys.getrefcount(None), sys.getrefcount(True), sys.getrefcount(False)) + self.printNonTrueFalseRefcounts(refcounts) + return refcounts + + def checkRefcountsDelta(self, start, end): + delta = (start[0] - end[0], start[1] - end[1], start[2] - end[2]) + self.assertLess(abs(delta[0]), self.REFCOUNT_THRESHOLD) + self.assertLess(abs(delta[1]), self.REFCOUNT_THRESHOLD) + self.assertLess(abs(delta[2]), self.REFCOUNT_THRESHOLD) + + def writeL2EmptyMessages(self): + key = self.streamKeys[2] + stream = self.db.getStream(key) + self.assertIsNotNone(stream) + + loader = stream.createLoader(dxapi.LoadingOptions()) + self.assertIsNotNone(loader) + try: + loaded = 0 + generator = L2EmptyGenerator(0, 1000000000, 1000000, ['MSFT', 'ORCL'], 5) + print("Start loading " + key) + + while generator.next(): + loader.send(generator.getMessage()) + loaded = loaded + 1 + if loaded % 200000 == 0: + print("Loaded " + str(loaded) + " messages") + loader.close() + finally: + loader.close() + + def readL2EmptyMessages(self): + key = self.streamKeys[2] + stream = self.db.getStream(key) + self.assertIsNotNone(stream) + + cursor = stream.createCursor(dxapi.SelectionOptions()) + self.assertIsNotNone(cursor) + try: + read = 0 + print('Start reading from ' + key) + while cursor.next(): + message = cursor.getMessage() + read = read + 1 + if (read % 1000000 == 0): + print('Read ' + str(read) + ' messages') + cursor.close() + finally: + cursor.close() + +class L2EmptyGenerator(generators.BaseGenerator): + + typeName = 'deltix.timebase.api.messages.L2Message' + actionTypeName = 'deltix.timebase.api.messages.Level2Action' + actions = ['INSERT', 'UPDATE', 'DELETE'] + actionsCount = 0 + + def __init__(self, time, timeInterval, count, symbols, actionsCount): + generators.BaseGenerator.__init__(self, time, timeInterval, count, symbols) + self.actionsCount = actionsCount + + def next(self): + if not generators.BaseGenerator.next(self): + return False + + self.message.typeName = self.typeName + self.message.currencyCode = 840 + self.message.exchangeId = None + self.message.isImplied = False + self.message.isSnapshot = True + + self.message.actions = [] + for i in range(self.actionsCount): + self.message.actions.append(self.newAction()) + + return True + + def newAction(self): + action = dxapi.InstrumentMessage() + action.typeName = None + action.level = None + action.isAsk = True + action.action = None + action.price = None + action.size = None + action.numOfOrders = None + action.quoteId = None + + return action + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/TestMultithreaded.py b/tests/TestMultithreaded.py new file mode 100644 index 0000000..9b00119 --- /dev/null +++ b/tests/TestMultithreaded.py @@ -0,0 +1,150 @@ +import unittest +import servertest +import testutils +import os, sys, threading, time +import dxapi + +from sys import version_info + +testdir = os.path.dirname(__file__) +if testdir != "": + testdir = testdir + '/' + +class TestMultithreaded(servertest.TestWithStreams): + + def test_NextIfAvailableWithLoader(self): + self.db.getStream("bars1min").truncate(-1) + + results = [None] * 1 + reader = threading.Thread(target = self.readStream, args = (results, 0, self.streamKeys[0], )) + writer = threading.Thread(target = self.loadBarsThread, args = ("bars1min", 3000, )) + + reader.start() + writer.start() + + writer.join() + reader.join() + + self.assertEqual(results[0], 3000) + + def test_NextIfAvailable3Cursors(self): + results = [None] * 3 + reader1 = threading.Thread(target = self.readStream, args = (results, 0, self.streamKeys[0], 1000000000, )) + reader2 = threading.Thread(target = self.readStream, args = (results, 1, self.streamKeys[1], )) + reader3 = threading.Thread(target = self.readStream, args = (results, 2, self.streamKeys[2], )) + + reader1.start() + reader2.start() + reader3.start() + + reader1.join() + reader2.join() + reader3.join() + + self.assertEqual(results[0], 0) + self.assertEqual(results[1], 10000) + self.assertEqual(results[2], 10000) + + def test_MassiveLoads(self): + # switch off for 2.7 python, it works too slow + if version_info >= (2, 7) and version_info < (2, 8): + return + + loadCount = 1000000 + timeout = 60 + + barsStreamKey = "bars1min.load" + # l2StreamKey = "l2.load" + # tradeBBOStreamKey = "tradeBBO.load" + + self.deleteStream(barsStreamKey) + # self.deleteStream(l2StreamKey) + # self.deleteStream(tradeBBOStreamKey) + + self.createStream("bars1min", barsStreamKey) + # self.createStream("l2", l2StreamKey) + # self.createStream("tradeBBO", tradeBBOStreamKey, True) + try: + results = [None] * 3 + reader1 = threading.Thread(target = self.readStream, args = (results, 0, barsStreamKey, 0, timeout, loadCount, )) + # reader2 = threading.Thread(target = self.readStream, args = (results, 1, l2StreamKey, 0, timeout, loadCount / 5, )) + # reader3 = threading.Thread(target = self.readStream, args = (results, 2, tradeBBOStreamKey, 0, timeout, loadCount, )) + + writer1 = threading.Thread(target = self.loadBarsThread, args = (barsStreamKey, loadCount, )) + # writer2 = threading.Thread(target = self.loadL2Thread, args = (l2StreamKey, loadCount / 5, )) + # writer3 = threading.Thread(target = self.loadTradeBBOThread, args = (tradeBBOStreamKey, loadCount, )) + + writer1.start() + # writer2.start() + # writer3.start() + + reader1.start() + # reader2.start() + # reader3.start() + + reader1.join() + # reader2.join() + # reader3.join() + writer1.join() + # writer2.join() + # writer3.join() + + self.assertEqual(results[0], loadCount) + # self.assertEqual(results[1], loadCount / 5) + # self.assertEqual(results[2], loadCount) + finally: + # b = 3 + self.deleteStream(barsStreamKey) + # self.deleteStream(l2StreamKey) + # self.deleteStream(tradeBBOStreamKey) + + ### helpers + def createStream(self, fileName, key, polymorphic = False): + with open(testdir + 'testdata/' + fileName + '.xml', 'r') as schemaFile: + schema = schemaFile.read() + options = dxapi.StreamOptions() + options.name(key) + options.description(key) + options.scope = dxapi.StreamScope('DURABLE') + options.distributionFactor = 1 + options.highAvailability = False + options.polymorphic = polymorphic + options.metadata(schema) + + return self.db.createStream(key, options) + + def loadBarsThread(self, key, count): + testutils.loadBars(self.db.getStream(key), count) + + def loadL2Thread(self, key, count): + testutils.loadL2(self.db.getStream(key), count) + + def loadTradeBBOThread(self, key, count): + testutils.loadTradeBBO(self.db.getStream(key), count / 2) + + def readStream(self, results, num, key, startTime = 0, timeout = 30, readUntil = 1000000000): + stream = self.db.getStream(key) + options = dxapi.SelectionOptions() + options.live = True + + cursor = stream.createCursor(options) + cursor.reset(startTime) + + tStart = time.time() + messages = 0 + while True: + if time.time() - tStart > timeout: + break + state = cursor.nextIfAvailable() + if state == dxapi.OK: + message = cursor.getMessage() + messages += 1 + if messages >= readUntil: + break + elif state == dxapi.END_OF_CURSOR: + break + print("Total read from " + key + ": " + str(messages)) + results[num] = messages + +if __name__ == '__main__': + unittest.main() diff --git a/tests/TestNextIfAvailable.py b/tests/TestNextIfAvailable.py new file mode 100644 index 0000000..b3cb6f3 --- /dev/null +++ b/tests/TestNextIfAvailable.py @@ -0,0 +1,123 @@ +import unittest +import servertest +import testutils +import copy +import dxapi + +class TestNextIfAvailable(servertest.TestWithStreams): + + def test_Smoke(self): + stream = self.db.getStream(self.streamKeys[0]) + self.assertEqual(self.readStreamIfAvailable(stream), 10000) + stream = self.db.getStream(self.streamKeys[1]) + self.assertEqual(self.readStreamIfAvailable(stream), 10000) + stream = self.db.getStream(self.streamKeys[2]) + self.assertEqual(self.readStreamIfAvailable(stream), 10000) + + def test_Multistream(self): + barStream = self.db.getStream(self.streamKeys[0]) + tradeBBOStream = self.db.getStream(self.streamKeys[1]) + l2Stream = self.db.getStream(self.streamKeys[2]) + cursor = self.db.select(0, [tradeBBOStream, barStream, l2Stream], dxapi.SelectionOptions(), None, None) + self.assertEqual(self.readCursorIfAvailable(cursor), 30000) + + def test_Reset(self): + stream = self.db.getStream(self.streamKeys[0]) + cursor = stream.createCursor(dxapi.SelectionOptions()) + + cursor.reset(9000) + self.assertEqual(self.readMessage(cursor).timestamp, self.msToNs(9000)) + + cursor.reset(5000) + self.assertEqual(self.readMessage(cursor).timestamp, self.msToNs(5000)) + + cursor.reset(100000) + self.checkCursorSymbols(cursor, set(self.entities.keys())) + + cursor.reset(90000, [self.entities['AAPL'], self.entities['IBM']]) + self.checkCursorSymbols(cursor, set(['AAPL', 'IBM'])) + + cursor.close() + + def test_SelectReverse(self): + stream = self.db.getStream(self.streamKeys[0]) + options = dxapi.SelectionOptions() + options.reverse = True + cursor = stream.createCursor(options) + + timestamp = 10000000000000 + cursor.reset(timestamp) + + while True: + message = self.readMessage(cursor) + if message == None: + break + if message.timestamp > timestamp: + self.assertFalse(True) + timestamp = message.timestamp + + cursor.close() + + ### helpers + def readMessage(self, cursor): + while True: + state = cursor.nextIfAvailable() + if state == dxapi.OK: + return cursor.getMessage() + elif state == dxapi.END_OF_CURSOR: + return None + + def readMessagesCount(self, cursor, count): + messages = [] + for i in range(count): + messages.append(copy.deepcopy(self.readMessage(cursor))) + return messages + + def readStreamIfAvailable(self, stream): + cursor = stream.createCursor(dxapi.SelectionOptions()) + try: + return self.readCursorIfAvailable(cursor) + finally: + if cursor != None: + cursor.close() + + def readCursorIfAvailable(self, cursor): + readCount = 0 + while True: + state = cursor.nextIfAvailable() + if state == dxapi.OK: + message = cursor.getMessage() + readCount += 1 + self.printReadingInfo(readCount) + elif state == dxapi.END_OF_CURSOR: + break + print("Read " + str(readCount) + " messages") + return readCount + + def printReadingInfo(self, count): + if count % 1000000 == 0: + print("Read " + str(count) + " messages") + + def checkCursorSymbols(self, cursor, symbols): + messages = self.readMessagesCount(cursor, 20) + self.checkSymbols(messages, symbols) + + def checkCursorTypesAndSymbols(self, cursor, types, symbols): + messages = self.readMessagesCount(cursor, 20) + self.checkTypes(messages, types) + self.checkSymbols(messages, symbols) + + def checkTypes(self, messages, types): + msgSet = set() + for message in messages: + msgSet.add(message.typeName) + self.assertEqual(msgSet, types) + + def checkSymbols(self, messages, symbols): + smbSet = set() + for message in messages: + smbSet.add(message.symbol) + self.assertEqual(smbSet, symbols) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/TestPackageHeader.py b/tests/TestPackageHeader.py new file mode 100644 index 0000000..9a52d27 --- /dev/null +++ b/tests/TestPackageHeader.py @@ -0,0 +1,49 @@ +import unittest +import servertest +import testutils +import dxapi + +class TestPackageHeader(servertest.TBServerTest): + + streamKey = 'universal' + + def test_Decimal64(self): + try: + stream = self.createStreamQQL(self.streamKey) + self.assertIsNotNone(stream) + self.assertEqual(self.streamCount(self.streamKey), 0) # check stream is empty + + count = 12345 + loadCount = testutils.loadUniversal(stream, count) + self.assertEqual(count, loadCount) + + readCount = 0 + currentPrice = 0 + cursor = stream.select(0, dxapi.SelectionOptions(), None, None) + while cursor.next(): + readCount += 1 + message = cursor.getMessage() + self.assertEqual(len(message.entries), 5) + for entry in message.entries: + currentPrice += 1.1 + self.assertAlmostEqual(entry.price, currentPrice) + self.assertAlmostEqual(entry.size, currentPrice) + self.assertEqual(readCount, loadCount) + finally: + self.deleteStream(self.streamKey) + + # helpers + + def streamCount(self, key): + cursor = self.db.executeQuery('SELECT count{}() as count FROM "' + key + '"') + try: + if cursor.next(): + return cursor.getMessage().COUNT + else: + return 0 + finally: + if cursor != None: + cursor.close() + +if __name__ == '__main__': + unittest.main() diff --git a/tests/TestQQL.py b/tests/TestQQL.py new file mode 100644 index 0000000..eb834a6 --- /dev/null +++ b/tests/TestQQL.py @@ -0,0 +1,44 @@ +import unittest +import servertest +import dxapi +import sys + +class TestQQL(servertest.TestWithStreams): + + def test_ExecuteQuery(self): + cursor = self.db.executeQuery("select * from tradeBBO") + count = self.readCount(cursor) + self.assertEqual(count, 10000) + cursor.close() + + def test_ExecuteQueryWithOptions(self): + options = dxapi.SelectionOptions() + options._from = 5000 + options.to = 6000 + cursor = self.db.executeQuery("select * from bars1min", options, []) + + self.assertTrue(cursor.next()) + message = cursor.getMessage() + self.assertEqual(message.timestamp, 5000000000) + self.assertTrue(cursor.next()) + message = cursor.getMessage() + self.assertEqual(message.timestamp, 6000000000) + self.assertFalse(cursor.next()) + + cursor.close() + + def test_ExecuteQueryWithInstruments(self): + cursor = self.db.executeQuery("select * from l2", dxapi.SelectionOptions(), 10000, [self.entities['IBM']], []) + self.checkCursorSymbols(cursor, set(['IBM'])) + cursor.close() + + def test_ExecuteQueryException(self): + try: + self.db.executeQuery("select * from ?") + self.assertTrue(False) + except: + print("Expected error") + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/TestSpeed.py b/tests/TestSpeed.py new file mode 100644 index 0000000..49826ff --- /dev/null +++ b/tests/TestSpeed.py @@ -0,0 +1,193 @@ +import unittest +import servertest +import generators +import time +import dxapi + +class TestSpeed(servertest.TBServerTest): + + streamKeys = [ + 'bars1min', 'tradeBBO', 'l2' + ] + + def setUp(self): + servertest.TBServerTest.setUp(self) + for key in self.streamKeys: + self.createStreamQQL(key) + self.assertEqual(len(self.db.listStreams()), len(self.streamKeys) + 1) + + def tearDown(self): + for key in self.streamKeys: + self.deleteStream(key) + self.assertEqual(len(self.db.listStreams()), 1) + servertest.TBServerTest.tearDown(self) + + def test_FixedReadWriteSpeed(self): + key = self.streamKeys[0] + stream = self.db.getStream(key) + self.assertIsNotNone(stream) + + loader = stream.createLoader(dxapi.LoadingOptions()) + self.assertIsNotNone(loader) + try: + typeId = loader.registerType('deltix.timebase.api.messages.BarMessage') + loaded = 0 + generator = generators.BarGenerator(0, 1000000000, 2000000, ['MSFT', 'ORCL']) + print("Start loading " + key) + + startMeasure = time.time() + while generator.next(): + message = generator.getMessage() + message.typeId = typeId + + loader.send(message) + loaded = loaded + 1 + if loaded % 200000 == 0: + print("Loaded " + str(loaded) + " messages") + loader.close() + + timeMeasure = (time.time() - startMeasure) + print('Total: ' + str(loaded) + ' msgs') + print('Time: ' + str(timeMeasure) + ' s') + print('Speed: ' + str(loaded / timeMeasure) + ' msg/s') + finally: + loader.close() + + cursor = stream.createCursor(dxapi.SelectionOptions()) + self.assertIsNotNone(cursor) + try: + read = 0 + startMeasure = time.time() + print('Start reading from ' + key) + while cursor.next(): + cursor.getMessage() + + read = read + 1 + if (read % 1000000 == 0): + print('Read ' + str(read) + ' messages') + cursor.close() + + timeMeasure = (time.time() - startMeasure) + print('Stream: ' + key) + print('Total: ' + str(read) + ' messages') + print('Time: ' + str(timeMeasure) + ' s') + print('Speed: ' + str(read / timeMeasure) + ' msg/s') + finally: + cursor.close() + + def test_PolymorphicReadWriteSpeed(self): + key = self.streamKeys[1] + stream = self.db.getStream(key) + self.assertIsNotNone(stream) + + loader = stream.createLoader(dxapi.LoadingOptions()) + self.assertIsNotNone(loader) + try: + tradeId = loader.registerType('deltix.timebase.api.messages.TradeMessage') + bboId = loader.registerType('deltix.timebase.api.messages.BestBidOfferMessage') + loaded = 0 + tradeGenerator = generators.TradeGenerator(0, 1000000000, 1000000, ['MSFT', 'ORCL']) + bboGenerator = generators.BBOGenerator(0, 1000000000, 1000000, ['MSFT', 'ORCL']) + print("Start loading " + key) + + startMeasure = time.time() + while tradeGenerator.next() and bboGenerator.next(): + tradeMessage = tradeGenerator.getMessage() + tradeMessage.typeId = tradeId + loader.send(tradeMessage) + loaded = loaded + 1 + + bboMessage = bboGenerator.getMessage() + bboMessage.typeId = bboId + loader.send(bboMessage) + loaded = loaded + 1 + + if loaded % 200000 == 0: + print("Loaded " + str(loaded) + " messages") + loader.close() + + timeMeasure = (time.time() - startMeasure) + print('Stream: ' + key) + print('Total: ' + str(loaded) + ' msgs') + print('Time: ' + str(timeMeasure) + ' s') + print('Speed: ' + str(loaded / timeMeasure) + ' msg/s') + finally: + loader.close() + + cursor = stream.createCursor(dxapi.SelectionOptions()) + self.assertIsNotNone(cursor) + try: + read = 0 + startMeasure = time.time() + print('Start reading from ' + key) + while cursor.next(): + cursor.getMessage() + + read = read + 1 + if (read % 1000000 == 0): + print('Read ' + str(read) + ' messages') + cursor.close() + + timeMeasure = (time.time() - startMeasure) + print('Stream: ' + key) + print('Total: ' + str(read) + ' messages') + print('Time: ' + str(timeMeasure) + ' s') + print('Speed: ' + str(read / timeMeasure) + ' msg/s') + finally: + cursor.close() + + def test_L2ReadWriteSpeed(self): + key = self.streamKeys[2] + stream = self.db.getStream(key) + self.assertIsNotNone(stream) + + loader = stream.createLoader(dxapi.LoadingOptions()) + self.assertIsNotNone(loader) + try: + typeId = loader.registerType('deltix.timebase.api.messages.L2Message') + loaded = 0 + generator = generators.L2Generator(0, 1000000000, 2000000, ['MSFT', 'ORCL'], 5) + print("Start loading " + key) + + startMeasure = time.time() + while generator.next(): + message = generator.getMessage() + message.typeId = typeId + + loader.send(message) + loaded = loaded + 1 + if loaded % 200000 == 0: + print("Loaded " + str(loaded) + " messages") + loader.close() + + timeMeasure = (time.time() - startMeasure) + print('Total: ' + str(loaded) + ' msgs') + print('Time: ' + str(timeMeasure) + ' s') + print('Speed: ' + str(loaded / timeMeasure) + ' msg/s') + finally: + loader.close() + + cursor = stream.createCursor(dxapi.SelectionOptions()) + self.assertIsNotNone(cursor) + try: + read = 0 + startMeasure = time.time() + print('Start reading from ' + key) + while cursor.next(): + cursor.getMessage() + + read = read + 1 + if (read % 1000000 == 0): + print('Read ' + str(read) + ' messages') + cursor.close() + + timeMeasure = (time.time() - startMeasure) + print('Stream: ' + key) + print('Total: ' + str(read) + ' messages') + print('Time: ' + str(timeMeasure) + ' s') + print('Speed: ' + str(read / timeMeasure) + ' msg/s') + finally: + cursor.close() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/TestStream.py b/tests/TestStream.py new file mode 100644 index 0000000..0154d6a --- /dev/null +++ b/tests/TestStream.py @@ -0,0 +1,138 @@ +import unittest +import servertest +import testutils +import time +import dxapi + +class TestStream(servertest.TestWithStreams): + + def test_StreamOptions(self): + stream = self.db.getStream(self.streamKeys[1]) + self.assertIsNotNone(stream) + + options = stream.options() + self.assertEqual(options.name(), 'tradeBBO') + self.assertEqual(options.description(), 'tradeBBO') + self.assertEqual(options.owner(), None) + self.assertEqual(options.location(), None) + self.assertEqual(options.distributionRuleName(), None) + self.assertIsNotNone(options.metadata()) + self.assertEqual(str(options.scope), 'DURABLE') + self.assertEqual(options.distributionFactor, 0) + self.assertEqual(options.duplicatesAllowed, True) + self.assertEqual(options.highAvailability, False) + self.assertEqual(options.unique, False) + self.assertEqual(options.polymorphic, True) + self.assertEqual(options.periodicity, 'IRREGULAR') + + def test_ListEntities(self): + stream = self.db.getStream(self.streamKeys[2]) + self.assertIsNotNone(stream) + + streamEntities = stream.listEntities() + self.compareEntities(streamEntities, set(self.entities.keys())) + + def test_DeleteStream(self): + keys = set() + for stream in self.db.listStreams(): + keys.add(stream.key()) + + keyToRemove = self.streamKeys[2] + keys.remove(keyToRemove) + self.db.getStream(keyToRemove).deleteStream() + + afterRemove = set() + for stream in self.db.listStreams(): + afterRemove.add(stream.key()) + + self.assertEqual(keys, afterRemove) + + def test_TruncatePurgeGetTimeRange(self): + stream = self.db.getStream(self.streamKeys[0]) + + range = stream.getTimeRange() + self.assertIsNotNone(range) + self.assertEqual(range[0], 0) + self.assertEqual(range[1], 9999000) + messages = self.readMessages(stream, 0, 9999000) + self.checkSymbols(messages, set(self.entities.keys())) + + stream.truncate(5000000, [self.entities['GOOG'], self.entities['IBM']]) + range = stream.getTimeRange() + self.assertIsNotNone(range) + self.assertTrue(0 <= range[0] <= 3000) + self.assertTrue(9995000 <= range[1] <= 9999000) # consider order of entities + + range = stream.getTimeRange([self.entities['IBM']]) + self.assertIsNotNone(range) + self.assertTrue(0 <= range[0] <= 3000) + self.assertTrue(4995000 <= range[1] <= 4999000) # consider order of entities + + messages = self.readMessages(stream, 0, 4997000) + self.checkSymbols(messages, set(self.entities.keys())) + + messages = self.readMessages(stream, 5000000, 9999000) + self.checkSymbols(messages, set(['AAPL'])) + + stream.truncate(3000000) + range = stream.getTimeRange() + self.assertIsNotNone(range) + self.assertEqual(range[0], 0) + self.assertEqual(range[1], 2999000) + + # Todo: purge must be synchronous + # stream.purge(1000000) + # range = stream.getTimeRange() + # self.assertIsNotNone(range) + # self.assertEqual(range[0], 1000000) + # self.assertEqual(range[1], 2999000) + + def test_Clear(self): + stream = self.db.getStream(self.streamKeys[0]) + + stream.clear([self.entities['GOOG'], self.entities['IBM']]) + messages = self.readMessages(stream, 0, 9999000) + self.checkSymbols(messages, set(['AAPL'])) + + stream.clear() + messages = self.readMessages(stream, 0, 9999000) + self.checkSymbols(messages, set()) + + def test_Spaces(self): + key = 'BarsWithSpaces' + try: + stream = self.createStream(key, False) + self.assertIsNotNone(stream) + + count = 12345 + testutils.loadBars(stream, count, 0, 1000000000, ['MSFT', 'ORCL'], 'SpaceX') + testutils.loadBars(stream, count, 0, 1000000000, ['MSFT', 'ORCL'], 'SpaceY') + + time.sleep(2) + + # Test Time Range of space + actualRange = stream.getTimeRange('SpaceY') + self.assertEqual(actualRange[0], 0) + self.assertEqual(actualRange[1], 12344000) + + # Test List Spaces + actualSpaces = set(stream.listSpaces()) + expectedSpaces = set(['SpaceX', 'SpaceY', '']) + self.assertEqual(len(actualSpaces.difference(expectedSpaces)), 0) + + # Test Rename + stream.renameSpace('SpaceZ', 'SpaceY') + actualSpaces = set(stream.listSpaces()) + expectedSpaces = set(['SpaceX', 'SpaceZ', '']) + self.assertEqual(len(actualSpaces.difference(expectedSpaces)), 0) + + # Test Delete Spaces + stream.deleteSpaces(['SpaceX', 'SpaceZ']) + actualSpaces = set(stream.listSpaces()) + expectedSpaces = set(['']) + self.assertEqual(len(actualSpaces.difference(expectedSpaces)), 0) + finally: + self.deleteStream(key) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/TestTickDB.py b/tests/TestTickDB.py new file mode 100644 index 0000000..9b96e0c --- /dev/null +++ b/tests/TestTickDB.py @@ -0,0 +1,117 @@ +import os +import unittest +import servertest +import dxapi + +testdir = os.path.dirname(__file__) +if testdir != "": + testdir = testdir + '/' + +class TestTickDB(servertest.TBServerTest): + + streamKeys = [ + 'bars1min', 'tradeBBO', 'l2' + ] + + def test_isOpen(self): + self.assertTrue(self.db.isOpen()) + + def test_isReadOnly(self): + self.assertFalse(self.db.isReadOnly()) + + #def test_createStream(self): + # key = self.streamKeys[1] + # try: + # with open(testdir + 'testdata/' + key + '.xml', 'r') as schemaFile: + # schema = schemaFile.read() + + # options = dxapi.StreamOptions() + # options.name(key) + # options.description(key) + # options.scope = dxapi.StreamScope('DURABLE') + # options.distributionFactor = 9 + # options.highAvailability = False + # options.polymorphic = False + # options.metadata(schema) + + # self.db.createStream(key, options) + + # stream = self.db.getStream(key) + # self.assertIsNotNone(stream) + # self.assertEqual(stream.key(), key) + # self.assertEqual(stream.name(), key) + # self.assertEqual(stream.distributionFactor(), 0) + # self.assertEqual(stream.description(), key) + # self.assertEqual(stream.highAvailability(), False) + # self.assertEqual(stream.polymorphic(), False) + # self.assertEqual(stream.periodicity(), 'IRREGULAR') + # self.assertIsNone(stream.location()) + # self.assertIsNotNone(stream.metadata()) + # self.assertEqual(str(stream.scope()), 'DURABLE') + # self.assertEqual(stream.unique(), False) + # finally: + # self.deleteStream(key) + + def test_createStreamQQL(self): + key = self.streamKeys[1] + try: + self.createStreamQQL(key) + + stream = self.db.getStream(key) + self.assertIsNotNone(stream) + self.assertEqual(stream.key(), key) + self.assertEqual(stream.name(), key) + self.assertEqual(stream.distributionFactor(), 0) + self.assertEqual(stream.description(), key) + self.assertEqual(stream.highAvailability(), False) + self.assertEqual(stream.polymorphic(), True) + self.assertEqual(stream.periodicity(), 'IRREGULAR') + self.assertIsNone(stream.location()) + self.assertIsNotNone(stream.metadata()) + self.assertEqual(str(stream.scope()), 'DURABLE') + self.assertEqual(stream.unique(), False) + finally: + self.deleteStream(key) + + def test_listStreams(self): + try: + self.createStreamsQQL() + + keySet = set() + streams = self.db.listStreams() + for stream in streams: + keySet.add(stream.key()) + + for key in self.streamKeys: + self.assertEqual(True, key in keySet) + finally: + self.deleteStreams() + + def test_removeStream(self): + key = 'l2' + try: + self.createStreamQQL(key) + stream = self.db.getStream(key) + self.assertIsNotNone(stream) + + stream.deleteStream() + stream = self.db.getStream(key) + self.assertIsNone(stream) + finally: + self.deleteStream(key) + + # helpers + def createStreams(self): + for key in self.streamKeys: + self.createStream(key) + + def createStreamsQQL(self): + for key in self.streamKeys: + self.createStreamQQL(key) + + def deleteStreams(self): + for key in self.streamKeys: + self.deleteStream(key) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/generators.py b/tests/generators.py new file mode 100644 index 0000000..52741ce --- /dev/null +++ b/tests/generators.py @@ -0,0 +1,182 @@ +import dxapi +import random + +class BaseGenerator: + + def __init__(self, time, timeInterval, count, symbols): + self.message = dxapi.InstrumentMessage() + + self.count = count + self.currentTime = time + self.timeInterval = timeInterval + self.symbols = symbols + if symbols != None and len(symbols) > 0: + self.curSymbol = 0 + + def isAtEnd(self): + return self.count == 0 + + def next(self): + if self.isAtEnd(): + return False + + self.message.timestamp = self.nextTimestamp() + self.message.symbol = self.nextSymbol() + self.message.instrumentType = 'EQUITY' + self.count = self.count - 1 + + return True + + def getMessage(self): + return self.message + + def nextTimestamp(self): + result = self.currentTime + self.currentTime = self.currentTime + self.timeInterval + return result + + def nextSymbol(self): + if self.curSymbol == -1: + return None + + symbol = self.symbols[self.curSymbol] + self.curSymbol = self.curSymbol + 1 + if self.curSymbol >= len(self.symbols): + self.curSymbol = 0 + + return symbol + +class TradeGenerator(BaseGenerator): + + typeName = 'deltix.timebase.api.messages.TradeMessage' + sides = ['BUY', 'SELL'] + + def next(self): + if not BaseGenerator.next(self): + return False + + self.message.typeName = self.typeName + self.message.currencyCode = 840 + self.message.exchangeId = 'NYSE' + self.message.price = random.uniform(0.0, 1.0) * 100 + self.message.size = random.randint(0, 1000) + self.message.netPriceChange = random.uniform(0.0, 1.0) * 100 + self.message.aggressorSide = self.sides[random.randint(0, 1)] + self.message.eventType = 'TRADE' + + return True + +class BBOGenerator(BaseGenerator): + + typeName = 'deltix.timebase.api.messages.BestBidOfferMessage' + + def next(self): + if not BaseGenerator.next(self): + return False + + self.message.typeName = self.typeName + self.message.currencyCode = 840 + if self.count % 2 == 0: + self.message.bidPrice = random.uniform(0.0, 1.0) * 100 + self.message.bidSize = random.randint(0, 1000) + self.message.bidExchangeId = 'NYSE' + else: + self.message.offerPrice = random.uniform(0.0, 1.0) * 100 + self.message.offerSize = random.randint(0, 1000) + self.message.offerExchangeId = 'NYSE' + + return True + +class BarGenerator(BaseGenerator): + + typeName = 'deltix.timebase.api.messages.BarMessage' + + def next(self): + if not BaseGenerator.next(self): + return False + + self.message.typeName = self.typeName + self.message.currencyCode = 840 + self.message.high = random.uniform(0.0, 100.0) + self.message.open = self.message.high - random.uniform(0.0, 10.0) + self.message.close = self.message.high - random.uniform(0.0, 10.0) + self.message.low = min(self.message.open, self.message.close) + random.uniform(0.0, 10.0) + self.message.volume = self.count + + return True + +class L2Generator(BaseGenerator): + + typeName = 'deltix.timebase.api.messages.L2Message' + actionTypeName = 'deltix.timebase.api.messages.Level2Action' + actions = ['INSERT', 'UPDATE', 'DELETE'] + actionsCount = 0 + + def __init__(self, time, timeInterval, count, symbols, actionsCount): + BaseGenerator.__init__(self, time, timeInterval, count, symbols) + self.actionsCount = actionsCount + + def next(self): + if not BaseGenerator.next(self): + return False + + self.message.typeName = self.typeName + self.message.currencyCode = 840 + self.message.exchangeId = 'NYSE' + self.message.isImplied = False + self.message.isSnapshot = False + + self.message.actions = [] + for i in range(self.actionsCount): + self.message.actions.append(self.newAction()) + + return True + + def newAction(self): + action = dxapi.InstrumentMessage() + action.typeName = self.actionTypeName + action.level = random.randint(0, 100) + action.isAsk = True + action.action = self.actions[random.randint(0, 2)] + action.price = random.uniform(0.0, 1.0) * 100 + action.size = random.uniform(0.0, 1.0) * 100 + action.numOfOrders = random.randint(0, 100) + action.quoteId = b'quoteId' + + return action + +class UniversalGenerator(BaseGenerator): + typeName = 'deltix.timebase.api.messages.universal.PackageHeader' + entryTypeName = 'deltix.timebase.api.messages.universal.L2EntryNew' + entriesCount = 0 + currentPrice = 0 + + def __init__(self, time, timeInterval, count, symbols, entriesCount): + BaseGenerator.__init__(self, time, timeInterval, count, symbols) + self.entriesCount = entriesCount + + def next(self): + if not BaseGenerator.next(self): + return False + + self.message.typeName = self.typeName + self.message.currencyCode = 999 + self.message.exchangeId = 'GDAX' + self.message.packageType = 'VENDOR_SNAPSHOT' + + self.message.entries = [] + for i in range(self.entriesCount): + self.message.entries.append(self.newEntry(i)) + + return True + + def newEntry(self, level): + self.currentPrice += 1.1 + entry = dxapi.InstrumentMessage() + entry.typeName = self.entryTypeName + entry.level = level + entry.side = 'BID' + entry.price = self.currentPrice + entry.size = self.currentPrice + return entry + diff --git a/tests/servertest.py b/tests/servertest.py new file mode 100644 index 0000000..7440695 --- /dev/null +++ b/tests/servertest.py @@ -0,0 +1,262 @@ +import unittest + +import sys, os, inspect, struct +from subprocess import Popen +import socket + +import copy +import time + +print("Python version: " + str(sys.version)) + +testdir = os.path.dirname(__file__) +if testdir != "": + testdir = testdir + '/' +sys.path.append(testdir + "..") + +import testutils +import dxapi + +class TBServerTest(unittest.TestCase): + + HOST = os.getenv('TIMEBASE_HOST', 'localhost') + PORT = os.getenv('TIMEBASE_PORT', 8011) + DELTIX_HOME = os.getenv('DELTIX_HOME', None) + + TIMEBASE_WAIT_TIMEOUT = 10 + + @classmethod + def setUpClass(cls): + print('TimeBase host: ' + str(cls.HOST)) + print('TimeBase port: ' + str(cls.PORT)) + print('TimeBase home: ' + str(cls.DELTIX_HOME)) + + #if not cls.DELTIX_HOME == None: + # print('Starting TimeBase...') + # socket.setdefaulttimeout(5) + # cls.server = Popen([cls.getTimeBaseCommand(), "-port", str(cls.PORT)]) + # cls.waitUntilStarted(cls.TIMEBASE_WAIT_TIMEOUT) + # print('OK') + + @classmethod + def tearDownClass(cls): + print('TimeBase host: ' + str(cls.HOST)) + print('TimeBase port: ' + str(cls.PORT)) + print('TimeBase home: ' + str(cls.DELTIX_HOME)) + #if not cls.DELTIX_HOME == None: + # try: + # print('Shutdown TimeBase...') + # s = urllib2.urlopen(cls.httpURL() + "/shutdown").read() + # time.sleep(5) # temp fix wait a little for closing server + # if (cls.server != None): + # cls.server.terminate() + # print('OK') + # except: + # #cls.printException() + # print("exception during shutdown") + # pass + + def setUp(self): + url = self.dxtickURL() + print('Connecting to ' + str(url) + "...") + self.db = dxapi.TickDb.createFromUrl(url) + self.db.open(False) + print('OK') + + def tearDown(self): + self.db.close() + + @classmethod + def getTimeBaseCommand(cls): + if cls.isLinux(): + print('Linux OS') + return testdir + 'timebase' + else: + print('Windows OS') + return testdir + 'timebase.cmd' + + @classmethod + def isLinux(cls): + if sys.platform.startswith('linux'): + return True + else: + return False + + # helpers + + @classmethod + def waitUntilStarted(cls, waitTimeout): + while waitTimeout > 0: + try: + time.sleep(1) + waitTimeout = waitTimeout - 1 + response = urllib2.urlopen(cls.httpURL()) + response.read() + return + except: + pass + + @classmethod + def dxtickURL(cls): + return "dxtick://" + cls.HOST + ":" + str(cls.PORT) + + @classmethod + def httpURL(cls): + return "http://" + cls.HOST + ":" + str(cls.PORT) + + def createStream(self, key, polymorphic = False): + self.deleteStream(key) + + with open(testdir + 'testdata/' + key + '.xml', 'r') as schemaFile: + schema = schemaFile.read() + options = dxapi.StreamOptions() + options.polymorphic = polymorphic + options.metadata(schema) + + return self.db.createStream(key, options) + + def createStreamQQL(self, key): + self.deleteStream(key) + + with open(testdir + 'testdata/' + key + '.qql', 'r') as qqlFile: + qql = qqlFile.read() + + cursor = self.db.executeQuery(qql) + self.assertTrue(cursor.next()) + reportMessage = cursor.getMessage() + print(str(reportMessage)) + #self.assertEqual(reportMessage.messageText, 'Stream created') + + return self.db.getStream(key) + + def deleteStream(self, key): + stream = self.db.getStream(key) + if stream != None: + stream.deleteStream() + + +class TestWithStreams(TBServerTest): + + streamKeys = [ + 'bars1min', 'tradeBBO', 'l2' + ] + + entities = { + 'AAPL':'AAPL', + 'GOOG':'GOOG', + 'IBM':'IBM', + } + + types = { + 'trade':'deltix.timebase.api.messages.TradeMessage', + 'bbo':'deltix.timebase.api.messages.BestBidOfferMessage', + 'bar':'deltix.timebase.api.messages.BarMessage', + 'l2':'deltix.timebase.api.messages.L2Message' + } + + nStreams = 0 + + def setUp(self): + TBServerTest.setUp(self) + + for key in self.streamKeys: + stream = self.db.getStream(key) + if stream != None: + stream.deleteStream() + + self.nStreams = len(self.db.listStreams()) + for key in self.streamKeys: + self.createStreamQQL(key) + #self.assertEqual(len(self.db.listStreams()), self.nStreams + len(self.streamKeys)) + + stream = self.db.getStream(self.streamKeys[0]) + testutils.loadBars(stream, 10000, 0, 1000000000, list(self.entities.keys())) + + stream = self.db.getStream(self.streamKeys[1]) + testutils.loadTradeBBO(stream, 5000, 0, 1000000000, list(self.entities.keys())) + + stream = self.db.getStream(self.streamKeys[2]) + testutils.loadL2(stream, 10000, 10, 0, 1000000000, list(self.entities.keys())) + + def tearDown(self): + for key in self.streamKeys: + stream = self.db.getStream(key) + if stream != None: + stream.deleteStream() + #self.assertEqual(len(self.db.listStreams()), self.nStreams) + tbStreams = len(self.db.listStreams()) + if tbStreams != self.nStreams: + print("WARNING: streams in TimeBase " + str(tbStreams) + ", but should be " + str(self.nStreams)) + TBServerTest.tearDown(self) + + # utils + + def read(self, cursor, count): + messages = [] + for i in range(count): + self.assertTrue(cursor.next()) + messages.append(copy.deepcopy(cursor.getMessage())) + return messages + + def readMessages(self, stream, _from, to): + options = dxapi.SelectionOptions() + options._from = _from + options.to = to + cursor = stream.createCursor(options) + messages = [] + try: + while cursor.next(): + messages.append(copy.deepcopy(cursor.getMessage())) + return messages + finally: + cursor.close() + + def readCount(self, cursor): + readCount = 0 + while cursor.next(): + readCount = readCount + 1 + return readCount + + def checkCursorTypes(self, cursor, types): + messages = self.read(cursor, 20) + self.checkTypes(messages, types) + + def checkCursorSymbols(self, cursor, symbols): + messages = self.read(cursor, 20) + self.checkSymbols(messages, symbols) + + def checkCursorTypesAndSymbols(self, cursor, types, symbols): + messages = self.read(cursor, 20) + self.checkTypes(messages, types) + self.checkSymbols(messages, symbols) + + def checkTypes(self, messages, types): + msgSet = set() + for message in messages: + msgSet.add(message.typeName) + self.assertEqual(msgSet, types) + + def checkSymbols(self, messages, symbols): + smbSet = set() + for message in messages: + smbSet.add(message.symbol) + self.assertEqual(smbSet, symbols) + + def compareEntities(self, entities, symbols): + symbolsSet = set(entities) + self.assertEqual(symbolsSet, symbols) + + def removeAll(self, collection, elements): + for element in elements: + collection.remove(element) + + def addAll(self, collection, elements): + for element in elements: + collection.add(element) + + def msToNs(self, ms): + return ms * 1000000 + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/BarsWithSpaces.xml b/tests/testdata/BarsWithSpaces.xml new file mode 100644 index 0000000..4c2ccc7 --- /dev/null +++ b/tests/testdata/BarsWithSpaces.xml @@ -0,0 +1,106 @@ + + + + deltix.timebase.api.messages.BarMessage + Bar Message + 64406601eb05459078wp_1i + false + 64406601eb05459078wp_1h + + exchangeId + Exchange Code + + UTF8 + true + true + + + + close + Close + + DECIMAL + true + + false + + + open + Open + + DECIMAL + true + + false + close + + + high + High + + DECIMAL + true + + false + close + + + low + Low + + DECIMAL + true + + false + close + + + volume + Volume + + DECIMAL + true + + false + + + + deltix.timebase.api.messages.MarketMessage + Market Message + 64406601eb05459078wp_1h + true + + originalTimestamp + + true + + + + currencyCode + Currency Code + + INT64 + true + + 999 + + + sequenceNumber + + + INT64 + true + + + + sourceId + + + UTF8 + true + true + + + + 64406601eb05459078wp_1i + \ No newline at end of file diff --git a/tests/testdata/bars1min.qql b/tests/testdata/bars1min.qql new file mode 100644 index 0000000..8769d4e --- /dev/null +++ b/tests/testdata/bars1min.qql @@ -0,0 +1,18 @@ +CREATE DURABLE STREAM "bars1min" 'bars1min' ( + CLASS "deltix.timebase.api.messages.MarketMessage" 'Market Message' ( + STATIC "originalTimestamp" TIMESTAMP = NULL, + STATIC "currencyCode" 'Currency Code' INTEGER = 999, + STATIC "sequenceNumber" '' INTEGER = NULL, + STATIC "sourceId" '' VARCHAR = NULL + ) NOT INSTANTIABLE; + CLASS "deltix.timebase.api.messages.BarMessage" 'Bar Message' UNDER "deltix.timebase.api.messages.MarketMessage" ( + STATIC "exchangeId" 'Exchange Code' VARCHAR = NULL, + "close" 'Close' FLOAT DECIMAL, + "open" 'Open' FLOAT DECIMAL RELATIVE TO "close", + "high" 'High' FLOAT DECIMAL RELATIVE TO "close", + "low" 'Low' FLOAT DECIMAL RELATIVE TO "close", + "volume" 'Volume' FLOAT DECIMAL + ); +) +OPTIONS (FIXEDTYPE; PERIODICITY = '1I'; HIGHAVAILABILITY = TRUE) +COMMENT 'bars1min' diff --git a/tests/testdata/bars1min.xml b/tests/testdata/bars1min.xml new file mode 100644 index 0000000..4c2ccc7 --- /dev/null +++ b/tests/testdata/bars1min.xml @@ -0,0 +1,106 @@ + + + + deltix.timebase.api.messages.BarMessage + Bar Message + 64406601eb05459078wp_1i + false + 64406601eb05459078wp_1h + + exchangeId + Exchange Code + + UTF8 + true + true + + + + close + Close + + DECIMAL + true + + false + + + open + Open + + DECIMAL + true + + false + close + + + high + High + + DECIMAL + true + + false + close + + + low + Low + + DECIMAL + true + + false + close + + + volume + Volume + + DECIMAL + true + + false + + + + deltix.timebase.api.messages.MarketMessage + Market Message + 64406601eb05459078wp_1h + true + + originalTimestamp + + true + + + + currencyCode + Currency Code + + INT64 + true + + 999 + + + sequenceNumber + + + INT64 + true + + + + sourceId + + + UTF8 + true + true + + + + 64406601eb05459078wp_1i + \ No newline at end of file diff --git a/tests/testdata/l2.qql b/tests/testdata/l2.qql new file mode 100644 index 0000000..6a8b14c --- /dev/null +++ b/tests/testdata/l2.qql @@ -0,0 +1,43 @@ +CREATE DURABLE STREAM "l2" ( + ENUM "deltix.timebase.api.messages.BookUpdateAction" 'Book Update Action' ( + "INSERT" = 0, + "UPDATE" = 1, + "DELETE" = 2 + ); + CLASS "deltix.timebase.api.messages.MarketMessage" 'Market Message' ( + "originalTimestamp" 'Original Time' TIMESTAMP COMMENT 'The timestamp of the event provided by the exchange, or by the data vendor. Some data vendors may not supply it. There is no guarantee that arriving messages are sorted in the order of original timestamps. This timestamp is commonly used by trading algorithms for stale data detection.', + "currencyCode" 'Currency Code' INTEGER SIGNED (16) COMMENT 'The currency code associated with this message. Some message types may have no price data, or have separate currency code information, in which case this field would be set to NULL. For example, the Quote (Best Bid Offer) Message, while a subclass of Market Message, has separate currency codes for the bid and offer sides.', + "sequenceNumber" 'Sequence Number' INTEGER COMMENT 'Nanotime from the timeApiServer field of MarketView event', + STATIC "sourceId" 'Source Id' VARCHAR = NULL + ) NOT INSTANTIABLE; + CLASS "deltix.timebase.api.messages.Level2Action" 'Level2 Action' ( + "level" INTEGER NOT NULL SIGNED (8), + "isAsk" BOOLEAN NOT NULL, + "action" "deltix.timebase.api.messages.BookUpdateAction" NOT NULL, + "price" FLOAT, + "size" FLOAT, + "numOfOrders" INTEGER SIGNED (32), + STATIC "quoteId" 'Quote Id' BINARY = NULL + ); + CLASS "deltix.timebase.api.messages.L2Message" 'L2 Message' UNDER "deltix.timebase.api.messages.MarketMessage" ( + "actions" ARRAY(OBJECT("deltix.timebase.api.messages.Level2Action")), + "exchangeId" 'exchangeCode' VARCHAR ALPHANUMERIC (10), + "isImplied" BOOLEAN NOT NULL, + "isSnapshot" BOOLEAN NOT NULL, + "sequenceId" INTEGER, + STATIC "maxDepth" 'Max Depth' INTEGER = NULL + ); + CLASS "deltix.timebase.api.messages.Level2Message" 'Level2 Message' UNDER "deltix.timebase.api.messages.MarketMessage" ( + "price" 'Price' FLOAT COMMENT 'Price at this level.', + "size" 'Size' FLOAT COMMENT 'Size at this level.', + "exchangeId" 'Exchange Code' VARCHAR ALPHANUMERIC (10) COMMENT 'Vendor-specific market code. This field is critically important in that it identifies the order book, along with instrument identity (symbol and type) and the Is Offer flag.', + "depth" 'Depth' INTEGER SIGNED (8) COMMENT 'The order book depth, 0-based. Level 0 is the best bid or offer.', + "isAsk" 'Is Offer' BOOLEAN NOT NULL COMMENT 'true if this is the offer side of the book; false if this is the bid side of the book.', + "action" 'Action' "deltix.timebase.api.messages.BookUpdateAction" COMMENT '
INSERT
A new price level is created. No orders previously existed at the given price.
DELETE
An existing price level is deleted. All orders at this level were filled or cancelled.
UPDATE
The total order size at an existing price level is changed.
', + "isLast" 'End Of Package' BOOLEAN NOT NULL COMMENT 'In some cases, several simultaneous changes are broadcast as a single set of updates. For example, when a Level 2 source tracks infinite book depth, but only broadcasts 10 levels, a DELETE at one price level is immediately compensated by an INSERT at the maximum output depth (9). Such increments come in a single package, all but the last one tagged with the value of false in this field. The last message in a package, as well as any independent increments are tagged with the value of true.', + "numOfOrders" 'Number Of Orders' INTEGER SIGNED (32) COMMENT 'Number Of Orders at this level.', + STATIC "quoteId" 'Quote Id' BINARY = NULL + ); +) +OPTIONS (POLYMORPHIC; PERIODICITY = 'IRREGULAR'; HIGHAVAILABILITY = FALSE) +COMMENT 'l2' diff --git a/tests/testdata/l2.xml b/tests/testdata/l2.xml new file mode 100644 index 0000000..5a0cdbb --- /dev/null +++ b/tests/testdata/l2.xml @@ -0,0 +1,283 @@ + + + + deltix.timebase.api.messages.L2Message + L2 Message + 64406601eb05459078wp_1t + false + 64406601eb05459078wp_1r + + actions + + true + + true + 64406601eb05459078wp_1s + + + false + + + exchangeId + exchangeCode + + ALPHANUMERIC(10) + true + false + + false + + + isImplied + + false + + false + + + isSnapshot + + false + + false + + + sequenceId + + INT64 + true + + false + + + maxDepth + Max Depth + + INT64 + true + + + + + deltix.timebase.api.messages.Level2Action + Level2 Action + 64406601eb05459078wp_1s + false + + level + + INT8 + false + + false + + + isAsk + + false + + false + + + action + + false + 64406601eb05459078wp_1q + + false + + + price + + IEEE64 + true + + false + + + size + + IEEE64 + true + + false + + + numOfOrders + + INT32 + true + + false + + + quoteId + Quote Id + + true + -2147483648 + 0 + + + + + deltix.timebase.api.messages.MarketMessage + Market Message + 64406601eb05459078wp_1r + true + + originalTimestamp + Original Time + The timestamp of the event provided by the exchange, or by the data vendor. Some data vendors may not supply it. There is no guarantee that arriving messages are sorted in the order of original timestamps. This timestamp is commonly used by trading algorithms for stale data detection. + + true + + false + + + currencyCode + Currency Code + The currency code associated with this message. Some message types may have no price data, or have separate currency code information, in which case this field would be set to <b>NULL</b>. For example, the Quote (Best Bid Offer) Message, while a subclass of Market Message, has separate currency codes for the bid and offer sides. + + INT16 + true + + false + + + sequenceNumber + Sequence Number + Nanotime from the timeApiServer field of MarketView event + + INT64 + true + + false + + + sourceId + Source Id + + UTF8 + true + true + + + + + deltix.timebase.api.messages.BookUpdateAction + Book Update Action + 64406601eb05459078wp_1q + + INSERT + 0 + + + UPDATE + 1 + + + DELETE + 2 + + false + + + deltix.timebase.api.messages.Level2Message + Level2 Message + 64406601eb05459078wp_1u + false + 64406601eb05459078wp_1r + + price + Price + Price at this level. + + IEEE64 + true + + false + + + size + Size + Size at this level. + + IEEE64 + true + + false + + + exchangeId + Exchange Code + Vendor-specific market code. This field is critically important in that it identifies the order book, along with instrument identity (symbol and type) and the <b>Is Offer</b> flag. + + ALPHANUMERIC(10) + true + false + + false + + + depth + Depth + The order book depth, 0-based. Level 0 is the best bid or offer. + + INT8 + true + + false + + + isAsk + Is Offer + <b>true</b> if this is the offer side of the book; <b>false</b> if this is the bid side of the book. + + false + + false + + + action + Action + <dl> <dt>INSERT</dt><dd>A new price level is created. No orders previously existed at the given price.</dd><dt>DELETE</dt><dd>An existing price level is deleted. All orders at this level were filled or cancelled.</dd><dt>UPDATE</dt><dd>The total order size at an existing price level is changed.</dd></dl> + + true + 64406601eb05459078wp_1q + + false + + + isLast + End Of Package + In some cases, several simultaneous changes are broadcast as a single set of updates. For example, when a Level 2 source tracks infinite book depth, but only broadcasts 10 levels, a DELETE at one price level is immediately compensated by an INSERT at the maximum output depth (9). Such increments come in a single package, all but the last one tagged with the value of <b>false</b> in this field. The last message in a package, as well as any independent increments are tagged with the value of <b>true</b>. + + false + + false + + + numOfOrders + Number Of Orders + Number Of Orders at this level. + + INT32 + true + + false + + + quoteId + Quote Id + + true + -2147483648 + 0 + + + + 64406601eb05459078wp_1s + 64406601eb05459078wp_1t + 64406601eb05459078wp_1u + \ No newline at end of file diff --git a/tests/testdata/tradeBBO.qql b/tests/testdata/tradeBBO.qql new file mode 100644 index 0000000..9927f5b --- /dev/null +++ b/tests/testdata/tradeBBO.qql @@ -0,0 +1,70 @@ +CREATE DURABLE STREAM "tradeBBO" ( + ENUM "deltix.timebase.api.messages.AggressorSide" 'Aggressor Side' ( + "BUY" = 0, + "SELL" = 1 + ); + CLASS "deltix.timebase.api.messages.MarketMessage" ( + STATIC "originalTimestamp" 'Original Time' TIMESTAMP = NULL, + "currencyCode" 'Currency Code' INTEGER SIGNED (16), + "sequenceNumber" 'Sequence Number' INTEGER + ) + NOT INSTANTIABLE; + CLASS "deltix.timebase.api.messages.BestBidOfferMessage" UNDER "deltix.timebase.api.messages.MarketMessage" ( + STATIC "isNational" 'National BBO' BOOLEAN = true, + "bidPrice" 'Bid Price' FLOAT DECIMAL, + "bidSize" 'Bid Size' FLOAT DECIMAL, + "bidExchangeId" 'Bid Exchange' VARCHAR ALPHANUMERIC (10), + "offerPrice" 'Offer Price' FLOAT DECIMAL, + "offerSize" 'Offer Size' FLOAT DECIMAL, + "offerExchangeId" 'Offer Exchange' VARCHAR ALPHANUMERIC (10) + ); + ENUM "deltix.timebase.api.messages.MarketEventType" 'Market Event Type' ( + "BID" = 0, + "OFFER" = 1, + "TRADE" = 2, + "INDEX_VALUE" = 3, + "OPENING_PRICE" = 4, + "CLOSING_PRICE" = 5, + "SETTLEMENT_PRICE" = 6, + "TRADING_SESSION_HIGH_PRICE" = 7, + "TRADING_SESSION_LOW_PRICE" = 8, + "TRADING_SESSION_VWAP_PRICE" = 9, + "IMBALANCE" = 10, + "TRADE_VOLUME" = 11, + "OPEN_INTEREST" = 12, + "COMPOSITE_UNDERLYING_PRICE" = 13, + "SIMULATED_SELL_PRICE" = 14, + "SIMULATED_BUY_PRICE" = 15, + "MARGIN_RATE" = 16, + "MID_PRICE" = 17, + "EMPTY_BOOK" = 18, + "SETTLE_HIGH_PRICE" = 19, + "SETTLE_LOW_PRICE" = 20, + "PRIOR_SETTLE_PRICE" = 21, + "SESSION_HIGH_BID" = 22, + "SESSION_LOW_OFFER" = 23, + "EARLY_PRICE" = 24, + "AUCTION_CLEARING_PRICE" = 25, + "SWAP_VALUE_FACTOR" = 26, + "VALUE_ADJ_LONG" = 27, + "CUMMULATIVE_VALUE_ADJ_LONG" = 28, + "DAILY_VALUE_ADJ_SHORT" = 29, + "CUMMULATIVE_VALUE_ADJ_SHORT" = 30, + "FIXING_PRICE" = 31, + "CASH_RATE" = 32, + "RECOVERY_RATE" = 33, + "RECOVERY_RATE_LONG" = 34, + "RECOVERY_RATE_SHORT" = 35 + ); + CLASS "deltix.timebase.api.messages.TradeMessage" UNDER "deltix.timebase.api.messages.MarketMessage" ( + "exchangeId" 'Exchange Code' VARCHAR ALPHANUMERIC (10), + "price" 'Price' FLOAT DECIMAL, + "size" 'Size' FLOAT DECIMAL, + "condition" 'Trade Condition' VARCHAR, + "aggressorSide" 'Aggressor Side' "deltix.timebase.api.messages.AggressorSide", + "netPriceChange" 'Net Price Change' FLOAT DECIMAL, + "eventType" 'Event Type' "deltix.timebase.api.messages.MarketEventType" + ); +) +OPTIONS (POLYMORPHIC; PERIODICITY = 'IRREGULAR'; HIGHAVAILABILITY = FALSE) +COMMENT 'tradeBBO' diff --git a/tests/testdata/tradeBBO.xml b/tests/testdata/tradeBBO.xml new file mode 100644 index 0000000..7f27ea5 --- /dev/null +++ b/tests/testdata/tradeBBO.xml @@ -0,0 +1,340 @@ + + + + deltix.timebase.api.messages.MarketMessage + 64406601eb05459078wp_1l + true + + originalTimestamp + Original Time + + true + + + + currencyCode + Currency Code + + INT16 + true + + false + + + sequenceNumber + Sequence Number + + INT64 + true + + false + + + + deltix.timebase.api.messages.AggressorSide + Aggressor Side + 64406601eb05459078wp_1k + + BUY + 0 + + + SELL + 1 + + false + + + deltix.timebase.api.messages.TradeMessage + 64406601eb05459078wp_1o + false + 64406601eb05459078wp_1l + + exchangeId + Exchange Code + + ALPHANUMERIC(10) + true + false + + false + + + price + Price + + DECIMAL + true + + false + + + size + Size + + DECIMAL + true + + false + + + condition + Trade Condition + + UTF8 + true + false + + false + + + aggressorSide + Aggressor Side + + true + 64406601eb05459078wp_1k + + false + + + netPriceChange + Net Price Change + + DECIMAL + true + + false + + + eventType + Event Type + + true + 64406601eb05459078wp_1n + + false + + + + deltix.timebase.api.messages.MarketEventType + Market Event Type + 64406601eb05459078wp_1n + + BID + 0 + + + OFFER + 1 + + + TRADE + 2 + + + INDEX_VALUE + 3 + + + OPENING_PRICE + 4 + + + CLOSING_PRICE + 5 + + + SETTLEMENT_PRICE + 6 + + + TRADING_SESSION_HIGH_PRICE + 7 + + + TRADING_SESSION_LOW_PRICE + 8 + + + TRADING_SESSION_VWAP_PRICE + 9 + + + IMBALANCE + 10 + + + TRADE_VOLUME + 11 + + + OPEN_INTEREST + 12 + + + COMPOSITE_UNDERLYING_PRICE + 13 + + + SIMULATED_SELL_PRICE + 14 + + + SIMULATED_BUY_PRICE + 15 + + + MARGIN_RATE + 16 + + + MID_PRICE + 17 + + + EMPTY_BOOK + 18 + + + SETTLE_HIGH_PRICE + 19 + + + SETTLE_LOW_PRICE + 20 + + + PRIOR_SETTLE_PRICE + 21 + + + SESSION_HIGH_BID + 22 + + + SESSION_LOW_OFFER + 23 + + + EARLY_PRICE + 24 + + + AUCTION_CLEARING_PRICE + 25 + + + SWAP_VALUE_FACTOR + 26 + + + VALUE_ADJ_LONG + 27 + + + CUMMULATIVE_VALUE_ADJ_LONG + 28 + + + DAILY_VALUE_ADJ_SHORT + 29 + + + CUMMULATIVE_VALUE_ADJ_SHORT + 30 + + + FIXING_PRICE + 31 + + + CASH_RATE + 32 + + + RECOVERY_RATE + 33 + + + RECOVERY_RATE_LONG + 34 + + + RECOVERY_RATE_SHORT + 35 + + false + + + deltix.timebase.api.messages.BestBidOfferMessage + 64406601eb05459078wp_1m + false + 64406601eb05459078wp_1l + + isNational + National BBO + + true + + true + + + bidPrice + Bid Price + + DECIMAL + true + + false + + + bidSize + Bid Size + + DECIMAL + true + + false + + + bidExchangeId + Bid Exchange + + ALPHANUMERIC(10) + true + false + + false + + + offerPrice + Offer Price + + DECIMAL + true + + false + + + offerSize + Offer Size + + DECIMAL + true + + false + + + offerExchangeId + Offer Exchange + + ALPHANUMERIC(10) + true + false + + false + + + 64406601eb05459078wp_1m + 64406601eb05459078wp_1o + \ No newline at end of file diff --git a/tests/testdata/universal.qql b/tests/testdata/universal.qql new file mode 100644 index 0000000..1c42dc0 --- /dev/null +++ b/tests/testdata/universal.qql @@ -0,0 +1,121 @@ +CREATE DURABLE STREAM "universal" ( + ENUM "deltix.timebase.api.messages.AggressorSide" 'Aggressor Side' ( + "BUY" = 0, + "SELL" = 1 + ); + ENUM "deltix.timebase.api.messages.BookUpdateAction" 'Book Update Action' ( + "INSERT" = 0, + "UPDATE" = 1, + "DELETE" = 2 + ); + CLASS "deltix.timebase.api.messages.DacMessage" 'Data Access Control Message' ( + STATIC "entitlementId" 'Entitlement ID' BINARY = NULL COMMENT 'Nullable field that defines what users can read this message.This field is a BLOB containing implementation-specific ID that describes source of data.Different users can have different permissions to see this data.Per-message permissions are checked on TimeBase server side.This field was added first to support Bloomberg B-PIPE Data Level Access control requirements.' + ) + COMMENT 'Extension for Market Message that carries Entitlement ID of this message.This field can be used by TimeBase server to implement per-message data access control.'; + ENUM "deltix.timebase.api.messages.DataModelType" ( + "LEVEL_ONE" = 0, + "LEVEL_TWO" = 1, + "LEVEL_THREE" = 2, + "MAX" = 3 + ); + CLASS "deltix.timebase.api.messages.MarketMessage" 'Market Message' UNDER "deltix.timebase.api.messages.DacMessage" ( + "currencyCode" 'Currency Code' INTEGER SIGNED (16) COMMENT 'Currency code represented as short. Use {currencyCodec} or{link #setCurrencyCode} and {link #getCurrencyCode} toconvert this value to a three-character code.', + "originalTimestamp" 'Original Timestamp' TIMESTAMP COMMENT 'Exchange Time is measured in milliseconds that passed since January 1, 1970 UTC', + "sequenceNumber" 'Sequence Number' INTEGER COMMENT 'Market specific identifier of the given event in a sequence of market events.', + "sourceId" 'Source Id' VARCHAR ALPHANUMERIC (10) COMMENT 'Identifies market data source. Different sessions of same connectorto a same data provider should have different id.' + ) + COMMENT 'Most financial market-related messages subclass this abstract class.'; + ENUM "deltix.timebase.api.messages.QuoteSide" ( + "BID" = 0, + "ASK" = 1 + ); + ENUM "deltix.timebase.api.messages.QuoteUpdateAction" ( + "CANCEL" = 0, + "MODIFY" = 1, + "REPLACE" = 2 + ); + ENUM "deltix.timebase.api.messages.TradeType" ( + "REGULAR_TRADE" = 0, + "AUCTION_CLEARING_PRICE" = 1, + "CORRECTION" = 2, + "CANCELLATION" = 3, + "UNKNOWN" = 4 + ); + CLASS "deltix.timebase.api.messages.universal.BaseEntry" 'Base Entry' ( + "contractId" 'Contract ID' VARCHAR ALPHANUMERIC (10) COMMENT 'Special field designed to store multiple derivative instruments\' updatesinto single package. Most of the time should be static null.', + "exchangeId" 'Exchange Code' VARCHAR ALPHANUMERIC (10) COMMENT 'Exchange code compressed to long using ALPHANUMERIC(10) encoding.see #getExchange()', + "isImplied" 'Is Implied' BOOLEAN COMMENT 'True, if quote (or trade) comes from an implied Order book.' + ) + COMMENT 'Base class for market data entry to be included in package (PackageHeader).'; + CLASS "deltix.timebase.api.messages.universal.BasePriceEntry" 'Base Price Entry' UNDER "deltix.timebase.api.messages.universal.BaseEntry" ( + "numberOfOrders" 'Number Of Orders' INTEGER COMMENT 'Numbers of orders.', + "participantId" 'Participant' VARCHAR COMMENT 'Id of participant (or broker ID).', + "price" 'Price' FLOAT DECIMAL64 COMMENT 'Ask, Bid or Trade price.', + "quoteId" 'Quote ID' VARCHAR COMMENT 'Quote ID. In Forex market, for example, quote ID can be referenced inTradeOrders (to identify market maker\'s quote/rate we want to deal with).Each market maker usually keeps this ID unique per session per day. Thisis a alpha-numeric text text field that can reach 64 characters or more,', + "size" 'Size' FLOAT DECIMAL64 COMMENT 'Ask, Bid or Trade quantity.' + ) + COMMENT 'This is base class for price entry.'; + CLASS "deltix.timebase.api.messages.universal.BookResetEntry" 'Book Reset Entry' UNDER "deltix.timebase.api.messages.universal.BaseEntry" ( + "modelType" 'Model Type' "deltix.timebase.api.messages.DataModelType" NOT NULL COMMENT 'Data Model Type to identify what book we should reset.', + "side" 'Side' "deltix.timebase.api.messages.QuoteSide" COMMENT 'Deprecated as of November 2019. No longer used. BookResetEntry now applies to both sides of Order Book and must be used only in snapshot package types.Previous meaning: Quote side. Bid or Ask.Ask = Sell limit order.Bid = Buy limit order.' + ) + COMMENT 'This entry used for empty snapshot indication. It is necessary because PackageHeader has no field exchangeId.You should use this entry only for empty snapshot (both sides are empty) case.'; + ENUM "deltix.timebase.api.messages.universal.InsertType" 'Insert Type' ( + "ADD_BACK" = 0, + "ADD_FRONT" = 1, + "ADD_BEFORE" = 2 + ); + CLASS "deltix.timebase.api.messages.universal.L1Entry" 'L1Entry' UNDER "deltix.timebase.api.messages.universal.BasePriceEntry" ( + "isNational" 'Is National' BOOLEAN COMMENT 'return 1 if this BBO quote represents the national best, 0 if this BBO is regionaland BooleanDataType.NULL if the property is undefined. In case of NBBO you can inspect {#getExchangeId()}to see what exchange/ECN has the national best price.', + "side" 'Side' "deltix.timebase.api.messages.QuoteSide" NOT NULL COMMENT 'Quote side. Bid or Ask.Ask = Sell limit order.Bid = Buy limit order.' + ) + COMMENT 'This class may represent both exchange-local top of the book (BBO) as well as National Best Bid Offer (NBBO).You can use method {getIsNational()} to filter out NBBO messages.This is always a one side quote, unlike old BestBidOfferMessage which is two-side (with nullable properties).'; + CLASS "deltix.timebase.api.messages.universal.L2EntryNew" 'L2EntryNew' UNDER "deltix.timebase.api.messages.universal.BasePriceEntry" ( + "level" 'Level Index' INTEGER NOT NULL SIGNED (16) COMMENT 'Market Depth / Price Level.This value is zero-based (top of the book will have depth=0).', + "side" 'Side' "deltix.timebase.api.messages.QuoteSide" NOT NULL COMMENT 'Quote side. Bid or Ask.Ask = Sell limit order.Bid = Buy limit order.' + ) + COMMENT 'Reports incremental L2-updates: insert, delete or update of one line in Order Book either on ask or bid side.It also can encode L2-snapshot entry. Note L2 is level oriented depth-of-the-book format and should be usedwhenever price or integer index is used to locate book changes. It does support individual quotes book ofarbitrary depth. But if incremental changes key is a quoteId L3Entry should be used instead.'; + CLASS "deltix.timebase.api.messages.universal.L2EntryUpdate" 'L2EntryUpdate' UNDER "deltix.timebase.api.messages.universal.BasePriceEntry" ( + "action" 'Action' "deltix.timebase.api.messages.BookUpdateAction" NOT NULL COMMENT 'Directs how to update an Order Book

symbol, instrumentType, exchangeCode, marketMakerCode, depth fields constitute the composite keyto identify the order book record.

.', + "level" 'Level Index' INTEGER NOT NULL SIGNED (16) COMMENT 'Market Depth / Price Level.This value is zero-based (top of the book will have depth=0).', + "side" 'Side' "deltix.timebase.api.messages.QuoteSide" COMMENT 'Quote side. Bid or Ask.Ask = Sell limit order.Bid = Buy limit order.' + ) + COMMENT 'Reports incremental L2-updates: insert, delete or update of one line in Order Book either on ask or bid side.It also can encode L2-snapshot entry. Note L2 is level oriented depth-of-the-book format and should be usedwhenever price or integer index is used to locate book changes. It does support individual quotes book ofarbitrary depth. But if incremental changes key is a quoteId L3Entry should be used instead.'; + CLASS "deltix.timebase.api.messages.universal.L3EntryNew" 'L3EntryNew' UNDER "deltix.timebase.api.messages.universal.BasePriceEntry" ( + "insertBeforeQuoteId" 'Insert Before Quote Id' VARCHAR COMMENT 'In case of InsertType = ADD_BEFORE represents the id of the quote that should be after inserted.', + "insertType" 'Insert Type' "deltix.timebase.api.messages.universal.InsertType" COMMENT 'Insert type. Add front or Add back.', + "side" 'Side' "deltix.timebase.api.messages.QuoteSide" NOT NULL COMMENT 'Quote side. Bid or Ask.Ask = Sell limit order.Bid = Buy limit order.' + ) + COMMENT 'Reports incremental L3-updates: new, cancel, modify and replace of one quote in Order Book either on ask or bid side.It also can encode L3-snapshot entry. Note L3 is quote oriented depth-of-the-book format and should be usedwhenever quoteId is used to locate book changes.'; + CLASS "deltix.timebase.api.messages.universal.L3EntryUpdate" 'L3 Entry Update' UNDER "deltix.timebase.api.messages.universal.BasePriceEntry" ( + "action" 'Action' "deltix.timebase.api.messages.QuoteUpdateAction" NOT NULL COMMENT 'Directs how to update an Order Book

symbol, instrumentType, exchangeCode, quoteId fields constitute the composite keyto identify the order book record.

.', + "side" 'Side' "deltix.timebase.api.messages.QuoteSide" COMMENT 'Quote side. Bid or Ask.Ask = Sell limit order.Bid = Buy limit order.' + ) + COMMENT 'Reports incremental L3-updates: new, cancel, modify and replace of one quote in Order Book either on ask or bid side.It also can encode L3-snapshot entry. Note L3 is quote oriented depth-of-the-book format and should be usedwhenever quoteId is used to locate book changes.'; + CLASS "deltix.timebase.api.messages.universal.TradeEntry" 'Trade Entry' UNDER "deltix.timebase.api.messages.universal.BaseEntry" ( + "buyerNumberOfOrders" 'Buyer Number Of Orders' INTEGER COMMENT 'Buyer number of orders involved in match.', + "buyerOrderId" 'Buyer Order ID' VARCHAR COMMENT 'ID of buyer order.', + "buyerParticipantId" 'Buyer Participant ID' VARCHAR COMMENT 'Buyer participant ID (or broker ID) for trader that submit buying order.', + "condition" 'Condition' VARCHAR COMMENT 'Market specific trade condition.', + "matchId" 'Match ID' VARCHAR COMMENT 'Id of particular execution event (ExecutionId, TradeId, MatchId)', + "price" 'Price' FLOAT DECIMAL64 COMMENT 'Ask, Bid or Trade price.', + "sellerNumberOfOrders" 'Seller Number Of Orders' INTEGER COMMENT 'Seller number of orders involved in match.', + "sellerOrderId" 'Seller Order ID' VARCHAR COMMENT 'ID of seller order.', + "sellerParticipantId" 'Seller Participant ID' VARCHAR COMMENT 'Seller participant ID (or broker ID) for trader that submit selling order.', + "side" 'Side' "deltix.timebase.api.messages.AggressorSide" COMMENT 'Trade side. Sell or Buy.For Trade it\'s aggressor side, i.e. side from where market order has came.', + "size" 'Size' FLOAT DECIMAL64 COMMENT 'Ask, Bid or Trade quantity.', + "tradeType" 'Trade Type' "deltix.timebase.api.messages.TradeType" COMMENT 'Explains the meaning of the given price and/or size.The value is null for regular trades.' + ) + COMMENT 'Basic information about a market trade.'; + ENUM "deltix.timebase.api.messages.universal.PackageType" 'Package Type' ( + "VENDOR_SNAPSHOT" = 0, + "PERIODICAL_SNAPSHOT" = 1, + "INCREMENTAL_UPDATE" = 2 + ); + CLASS "deltix.timebase.api.messages.universal.PackageHeader" 'Package Header' UNDER "deltix.timebase.api.messages.MarketMessage" ( + "entries" 'Entries' ARRAY(OBJECT("deltix.timebase.api.messages.universal.TradeEntry", "deltix.timebase.api.messages.universal.L1Entry", "deltix.timebase.api.messages.universal.L2EntryNew", "deltix.timebase.api.messages.universal.L2EntryUpdate", "deltix.timebase.api.messages.universal.L3EntryNew", "deltix.timebase.api.messages.universal.L3EntryUpdate", "deltix.timebase.api.messages.universal.BookResetEntry") NOT NULL) NOT NULL COMMENT 'Message package content. Array of individual entries.Typical entries classes are L1Entry, L2Entry, L3Entry, TradeEntry.', + "packageType" 'Package Type' "deltix.timebase.api.messages.universal.PackageType" NOT NULL COMMENT 'Package type needs to distinguish between incremental changes and different types of snapshot.' + ) + COMMENT 'Represents market data package.'; +) +OPTIONS (POLYMORPHIC; PERIODICITY = 'IRREGULAR'; DF = 1; HIGHAVAILABILITY = FALSE) diff --git a/tests/testdata/universal.xml b/tests/testdata/universal.xml new file mode 100644 index 0000000..5be8673 --- /dev/null +++ b/tests/testdata/universal.xml @@ -0,0 +1,736 @@ + + + + deltix.timebase.api.messages.universal.PackageHeader + Package Header + c0a83801f46b36i2q6or_1d + false + c0a83801f46b36i2q6or_1c + + entries + Entries + + + false + + false + c0a83801f49d36i2r1gm_3g + c0a83801f49d36i2r1gm_3j + c0a83801f49d36i2r1gm_3k + c0a83801f49d36i2r1gm_3m + c0a83801f49d36i2r1gm_3o + c0a83801f49d36i2r1gm_3q + c0a83801f49d36i2r1gm_3s + + + false + false + + + packageType + Package Type + + + false + c0a83801f49d36i2r1gm_3t + + false + false + + + + deltix.timebase.api.messages.AggressorSide + Aggressor Side + + c0a83801f49d36i2r1gm_3e + + BUY + 0 + + + SELL + 1 + + false + + + deltix.timebase.api.messages.BookUpdateAction + Book Update Action + + c0a83801f49d36i2r1gm_3l + + INSERT + 0 + + + UPDATE + 1 + + + DELETE + 2 + + false + + + deltix.timebase.api.messages.DataModelType + deltix.timebase.api.messages.DataModelType + + c0a83801f49d36i2r1gm_3r + + LEVEL_ONE + 0 + + + LEVEL_TWO + 1 + + + LEVEL_THREE + 2 + + false + + + deltix.timebase.api.messages.MarketMessage + Market Message + c0a83801f46b36i2q6or_1c + false + + currencyCode + currencyCode + + + INT16 + true + + false + false + + + originalTimestamp + originalTimestamp + + + true + + false + false + + + sequenceNumber + sequenceNumber + + + INT64 + true + + false + false + + + sourceId + sourceId + + + ALPHANUMERIC(10) + true + true + + false + false + + + + deltix.timebase.api.messages.QuoteSide + deltix.timebase.api.messages.QuoteSide + + c0a83801f49d36i2r1gm_3i + + BID + 0 + + + ASK + 1 + + false + + + deltix.timebase.api.messages.QuoteUpdateAction + deltix.timebase.api.messages.QuoteUpdateAction + + c0a83801f49d36i2r1gm_3p + + CANCEL + 0 + + + MODIFY + 1 + + + REPLACE + 2 + + false + + + deltix.timebase.api.messages.TradeType + deltix.timebase.api.messages.TradeType + + c0a83801f49d36i2r1gm_3f + + REGULAR_TRADE + 0 + + + AUCTION_CLEARING_PRICE + 1 + + + CORRECTION + 2 + + + CANCELLATION + 3 + + + UNKNOWN + 4 + + false + + + deltix.timebase.api.messages.universal.BaseEntry + Base Entry + + c0a83801f49d36i2r1gm_3d + false + + contractId + Contract ID + + + ALPHANUMERIC(10) + true + true + + false + false + + + exchangeId + Exchange Code + + + ALPHANUMERIC(10) + true + true + + false + false + + + isImplied + Is Implied + + + true + + false + false + + + + deltix.timebase.api.messages.universal.BasePriceEntry + Base Price Entry + + c0a83801f49d36i2r1gm_3h + false + c0a83801f49d36i2r1gm_3d + + numberOfOrders + Number Of Orders + + + INT64 + true + + false + false + + + participantId + Participant + + + UTF8 + true + false + + false + false + + + price + Price + + + DECIMAL64 + true + + false + false + + + quoteId + Quote ID + + + UTF8 + true + false + + false + false + + + size + Size + + + DECIMAL64 + true + + false + false + + + + deltix.timebase.api.messages.universal.BookResetEntry + Book Reset Entry + + c0a83801f49d36i2r1gm_3s + false + c0a83801f49d36i2r1gm_3d + + modelType + Model Type + + + false + c0a83801f49d36i2r1gm_3r + + false + false + + + side + Side + + + false + c0a83801f49d36i2r1gm_3i + + false + false + + + + deltix.timebase.api.messages.universal.InsertType + Insert Type + + c0a83801f49d36i2r1gm_3n + + ADD_BACK + 0 + + + ADD_FRONT + 1 + + + ADD_BEFORE + 2 + + false + + + deltix.timebase.api.messages.universal.L1Entry + L1Entry + + c0a83801f49d36i2r1gm_3j + false + c0a83801f49d36i2r1gm_3h + + isNational + Is National + + + true + + false + false + + + side + Side + + + false + c0a83801f49d36i2r1gm_3i + + false + false + + + + deltix.timebase.api.messages.universal.L2EntryNew + L2EntryNew + + c0a83801f49d36i2r1gm_3k + false + c0a83801f49d36i2r1gm_3h + + level + Level Index + + + INT16 + false + + false + false + + + side + Side + + + false + c0a83801f49d36i2r1gm_3i + + false + false + + + + deltix.timebase.api.messages.universal.L2EntryUpdate + L2EntryUpdate + + c0a83801f49d36i2r1gm_3m + false + c0a83801f49d36i2r1gm_3h + + action + Action + + + false + c0a83801f49d36i2r1gm_3l + + false + false + + + level + Level Index + + + INT16 + false + + false + false + + + side + Side + + + true + c0a83801f49d36i2r1gm_3i + + false + false + + + + deltix.timebase.api.messages.universal.L3EntryNew + L3EntryNew + + c0a83801f49d36i2r1gm_3o + false + c0a83801f49d36i2r1gm_3h + + insertBeforeQuoteId + insertBeforeQuoteId + + + UTF8 + true + false + + false + false + + + insertType + insertType + + + true + c0a83801f49d36i2r1gm_3n + + false + false + + + side + Side + + + false + c0a83801f49d36i2r1gm_3i + + false + false + + + + deltix.timebase.api.messages.universal.L3EntryUpdate + L3 Entry Update + + c0a83801f49d36i2r1gm_3q + false + c0a83801f49d36i2r1gm_3h + + action + Action + + + false + c0a83801f49d36i2r1gm_3p + + false + false + + + side + Side + + + true + c0a83801f49d36i2r1gm_3i + + false + false + + + + deltix.timebase.api.messages.universal.PackageHeader + Package Header + c0a83801f46b36i2q6or_1d + false + c0a83801f46b36i2q6or_1c + + entries + Entries + + + false + + false + c0a83801f49d36i2r1gm_3g + c0a83801f49d36i2r1gm_3j + c0a83801f49d36i2r1gm_3k + c0a83801f49d36i2r1gm_3m + c0a83801f49d36i2r1gm_3o + c0a83801f49d36i2r1gm_3q + c0a83801f49d36i2r1gm_3s + + + false + false + + + packageType + Package Type + + + false + c0a83801f49d36i2r1gm_3t + + false + false + + + + deltix.timebase.api.messages.universal.PackageType + Package Type + + c0a83801f49d36i2r1gm_3t + + VENDOR_SNAPSHOT + 0 + + + PERIODICAL_SNAPSHOT + 1 + + + INCREMENTAL_UPDATE + 2 + + false + + + deltix.timebase.api.messages.universal.TradeEntry + Trade Entry + + c0a83801f49d36i2r1gm_3g + false + c0a83801f49d36i2r1gm_3d + + buyerNumberOfOrders + Buyer Number Of Orders + + + INT64 + true + + false + false + + + buyerOrderId + Buyer Order ID + + + UTF8 + true + false + + false + false + + + buyerParticipantId + Buyer Participant ID + + + UTF8 + true + false + + false + false + + + condition + Condition + + + UTF8 + true + false + + false + false + + + matchId + Match ID + + + UTF8 + true + false + + false + false + + + price + Price + + + DECIMAL64 + true + + false + false + + + sellerNumberOfOrders + Seller Number Of Orders + + + INT64 + true + + false + false + + + sellerOrderId + Seller Order ID + + + UTF8 + true + false + + false + false + + + sellerParticipantId + Seller Participant ID + + + UTF8 + true + false + + false + false + + + side + Side + + + true + c0a83801f49d36i2r1gm_3e + + false + false + + + size + Size + + + DECIMAL64 + true + + false + false + + + tradeType + Trade Type + + + true + c0a83801f49d36i2r1gm_3f + + false + false + + + c0a83801f46b36i2q6or_1d + \ No newline at end of file diff --git a/tests/testutils.py b/tests/testutils.py new file mode 100644 index 0000000..160f791 --- /dev/null +++ b/tests/testutils.py @@ -0,0 +1,109 @@ +import dxapi +import generators + +def loadTradeBBO(stream, count, startTime = 0, timeInterval = 1000000000, symbols = ['MSFT', 'ORCL']): + loader = stream.createLoader(dxapi.LoadingOptions()) + try: + loadCount = 0 + tradeGenerator = generators.TradeGenerator(startTime, timeInterval, count, symbols) + bboGenerator = generators.BBOGenerator(startTime, timeInterval, count, symbols) + while tradeGenerator.next() and bboGenerator.next(): + tradeMessage = tradeGenerator.getMessage() + loader.send(tradeMessage) + loadCount = loadCount + 1 + #printLoadingInfo(loadCount, tradeMessage) + + bboMessage = bboGenerator.getMessage() + loader.send(bboMessage) + loadCount = loadCount + 1 + #printLoadingInfo(loadCount, bboMessage) + + print("Total loaded " + str(loadCount) + " messages") + return loadCount + finally: + if loader != None: + loader.close() + +def loadBars(stream, count, startTime = 0, timeInterval = 1000000000, symbols = ['MSFT', 'ORCL'], space = None): + options = dxapi.LoadingOptions() + if space != None: + options.space = space + loader = stream.createLoader(options) + try: + loadCount = 0 + barGenerator = generators.BarGenerator(startTime, timeInterval, count, symbols) + while barGenerator.next(): + message = barGenerator.getMessage() + loader.send(message) + loadCount = loadCount + 1 + #printLoadingInfo(loadCount, message) + + print("Total loaded " + str(loadCount) + " messages") + return loadCount + finally: + if loader != None: + loader.close() + +def loadL2(stream, count, actionsCount = 5, startTime = 0, timeInterval = 1000000000, symbols = ['MSFT', 'ORCL']): + loader = stream.createLoader(dxapi.LoadingOptions()) + try: + loadCount = 0 + barGenerator = generators.L2Generator(startTime, timeInterval, count, symbols, actionsCount) + while barGenerator.next(): + message = barGenerator.getMessage() + loader.send(message) + loadCount = loadCount + 1 + #printLoadingInfo(loadCount, message) + + print("Total loaded " + str(loadCount) + " messages") + return loadCount + finally: + if loader != None: + loader.close() + +def loadUniversal(stream, count, entriesCount = 5, startTime = 0, timeInterval = 1000000000, symbols = ['BTCUSD']): + loader = stream.createLoader(dxapi.LoadingOptions()) + try: + loadCount = 0 + universalGenerator = generators.UniversalGenerator(startTime, timeInterval, count, symbols, entriesCount) + while universalGenerator.next(): + message = universalGenerator.getMessage() + loader.send(message) + loadCount = loadCount + 1 + #printLoadingInfo(loadCount, message) + + print("Total loaded " + str(loadCount) + " messages") + return loadCount + finally: + if loader != None: + loader.close() + +def readStream(stream): + cursor = stream.createCursor(dxapi.SelectionOptions()) + try: + readCount = 0 + while cursor.next(): + readCount = readCount + 1 + printReadingInfo(readCount, cursor.getMessage()) + + print("Read " + str(readCount) + " messages") + return readCount + finally: + if cursor != None: + cursor.close() + +def printLoadingInfo(count, message): + if count < 10: + print("Load message: " + str(message)) + elif count == 10: + print('...') + if count % 100000 == 0: + print("Loaded " + str(count) + " messages") + +def printReadingInfo(count, message): + if count < 10: + print("Read message: " + str(message)) + elif count == 10: + print('...') + if count % 1000000 == 0: + print("Read " + str(count) + " messages")