Compare commits
125 Commits
add-spice-
...
JOSS-paper
Author | SHA1 | Date | |
---|---|---|---|
|
2be3ff6bcb | ||
|
457b8d8761 | ||
|
70dfa1435b | ||
|
98fc5b6e65 | ||
|
c126c26501 | ||
|
2159059b87 | ||
|
f0f4faca71 | ||
|
0bc647dbd9 | ||
|
e3efb72efe | ||
|
a9ef63cb20 | ||
|
3b0daff2a2 | ||
|
67bd4f08e6 | ||
|
4c2d291eaf | ||
|
85df7faa98 | ||
|
8d3ed03184 | ||
|
9cb4832c87 | ||
|
5cfadc689b | ||
|
936ca24482 | ||
|
9c73411ac2 | ||
|
8c7f478724 | ||
|
52b774b5eb | ||
|
292d72d593 | ||
|
cf882fa84e | ||
|
ab9d781b06 | ||
|
048cb95bd6 | ||
|
09e834fa21 | ||
|
6e74d46660 | ||
|
7ef56e3029 | ||
|
555cc42630 | ||
|
dcf6ebe273 | ||
|
83343dc2f1 | ||
|
099137adac | ||
|
9e36b0d2ea | ||
|
caa47a2f47 | ||
|
f5870a7540 | ||
|
647200e8a7 | ||
|
77c360b264 | ||
|
9c361f4422 | ||
|
95121550ef | ||
|
f7dd3045f7 | ||
|
f5cd8f62c6 | ||
|
1c56fa034f | ||
|
7295ba0fb2 | ||
|
f6e9753c99 | ||
|
eeebb78a5c | ||
|
ea8e34e192 | ||
|
7b1d409c98 | ||
|
d056d766ed | ||
|
025b98decd | ||
|
3b97b3d5c8 | ||
|
8aac644009 | ||
|
48140bff91 | ||
|
81417cb795 | ||
|
69b3fcfd32 | ||
|
27dce20b29 | ||
|
240b529533 | ||
|
2493e0c8a5 | ||
|
1a8e1362a1 | ||
|
67cddae756 | ||
|
af8f308584 | ||
|
7766350c15 | ||
|
8c313b431d | ||
|
baa4f8e3d0 | ||
|
cdc550da9a | ||
|
d31926efdf | ||
|
3199eb453b | ||
|
05ccebf9a1 | ||
|
94cfc8e63f | ||
|
d1bee29b1e | ||
|
a61821e1c6 | ||
|
bd870e2331 | ||
|
c0cee5df07 | ||
|
b708134c1a | ||
|
b26ed7dea4 | ||
|
280a1dc3f8 | ||
|
f9a49744e6 | ||
|
a2a4bc05db | ||
|
29f0e01c4a | ||
|
d88a0dbf82 | ||
|
8b3a8234ac | ||
|
8cd4daad0a | ||
|
3eb897c2f8 | ||
|
4b9499e321 | ||
|
4baa36bdcf | ||
|
f95602f6bd | ||
|
5d4e5e69fe | ||
|
7962a1439b | ||
|
81b5aa66e8 | ||
|
45218faeb0 | ||
|
d55092ff17 | ||
|
74e4fd0633 | ||
|
b90da46b1b | ||
|
2080ff86ed | ||
|
16cec7dfbd | ||
|
0475b7cb18 | ||
|
d60a166fbf | ||
|
dd382dd370 | ||
|
69d542d3e2 | ||
|
e5df39e891 | ||
|
bf7ceba958 | ||
|
57c488a6f1 | ||
|
48bb51b458 | ||
|
b1fc5a06ca | ||
|
6d8e838a8f | ||
|
acf3484e88 | ||
|
cf0731095f | ||
|
1c81ec6016 | ||
|
13cd18dc9a | ||
|
926023935f | ||
|
096533bcb9 | ||
|
718c9d0440 | ||
|
9c78e6c26f | ||
|
6048f60f13 | ||
|
d4db5c3281 | ||
|
91683e1dca | ||
|
ecd1f55abc | ||
|
70b25461f0 | ||
|
9b895500b3 | ||
|
cd3fe44424 | ||
|
01232e9a1f | ||
|
8eeaab2746 | ||
|
ec813434f5 | ||
|
2f4d73eb06 | ||
|
c1e7db3130 | ||
|
05ed1b544f |
23
.github/workflows/draft-pdf.yml
vendored
Normal file
23
.github/workflows/draft-pdf.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
paper:
|
||||
runs-on: ubuntu-latest
|
||||
name: Paper Draft
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build draft PDF
|
||||
uses: openjournals/openjournals-draft-action@master
|
||||
with:
|
||||
journal: joss
|
||||
# This should be the path to the paper within your repo.
|
||||
paper-path: docs/JOSS_paper/paper.md
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: paper
|
||||
# This is the output path where Pandoc will write the compiled
|
||||
# PDF. Note, this should be the same directory as the input
|
||||
# paper.md
|
||||
path: docs/JOSS_paper/paper.pdf
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -113,3 +113,4 @@ target/
|
||||
!config_examples/config_full.example.json
|
||||
!config_examples/config_kraken.example.json
|
||||
!config_examples/config_freqai.example.json
|
||||
!config_examples/config_freqai-rl.example.json
|
||||
|
BIN
docs/JOSS_paper/assets/freqai_algo.jpg
Normal file
BIN
docs/JOSS_paper/assets/freqai_algo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 345 KiB |
BIN
docs/JOSS_paper/assets/freqai_algorithm-diagram.jpg
Normal file
BIN
docs/JOSS_paper/assets/freqai_algorithm-diagram.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 490 KiB |
15
docs/JOSS_paper/note_to_editors.txt
Normal file
15
docs/JOSS_paper/note_to_editors.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
Dear Editors,
|
||||
We present a paper for ``FreqAI`` a machine learning sandbox for researchers and citizen scientists alike.
|
||||
There are a large number of authors, however all have contributed in a significant way to this paper.
|
||||
For clarity the contribution of each author is outlined:
|
||||
|
||||
- Robert Caulk : Conception and software development
|
||||
- Elin Tornquist : Theoretical brainstorming, data analysis, tool dev
|
||||
- Matthias Voppichler : Software architecture and code review
|
||||
- Andrew R. Lawless : Extensive testing, feature brainstorming
|
||||
- Ryan McMullan : Extensive testing, feature brainstorming
|
||||
- Wagner Costa Santos : Major backtesting developments, extensive testing
|
||||
- Pascal Schmidt : Extensive testing, feature brainstorming
|
||||
- Timothy C. Pogue : Webhooks forecast sharing
|
||||
- Stefan P. Gehring : Extensive testing, feature brainstorming
|
||||
- Johan van der Vlugt : Extensive testing, feature brainstorming
|
207
docs/JOSS_paper/paper.bib
Normal file
207
docs/JOSS_paper/paper.bib
Normal file
@@ -0,0 +1,207 @@
|
||||
@article{scikit-learn,
|
||||
title={Scikit-learn: Machine Learning in {P}ython},
|
||||
author={Pedregosa, F. and Varoquaux, G. and Gramfort, A. and Michel, V.
|
||||
and Thirion, B. and Grisel, O. and Blondel, M. and Prettenhofer, P.
|
||||
and Weiss, R. and Dubourg, V. and Vanderplas, J. and Passos, A. and
|
||||
Cournapeau, D. and Brucher, M. and Perrot, M. and Duchesnay, E.},
|
||||
journal={Journal of Machine Learning Research},
|
||||
volume={12},
|
||||
pages={2825--2830},
|
||||
year={2011}
|
||||
}
|
||||
|
||||
@inproceedings{catboost,
|
||||
author = {Prokhorenkova, Liudmila and Gusev, Gleb and Vorobev, Aleksandr and Dorogush, Anna Veronika and Gulin, Andrey},
|
||||
title = {CatBoost: Unbiased Boosting with Categorical Features},
|
||||
year = {2018},
|
||||
publisher = {Curran Associates Inc.},
|
||||
address = {Red Hook, NY, USA},
|
||||
abstract = {This paper presents the key algorithmic techniques behind CatBoost, a new gradient boosting toolkit. Their combination leads to CatBoost outperforming other publicly available boosting implementations in terms of quality on a variety of datasets. Two critical algorithmic advances introduced in CatBoost are the implementation of ordered boosting, a permutation-driven alternative to the classic algorithm, and an innovative algorithm for processing categorical features. Both techniques were created to fight a prediction shift caused by a special kind of target leakage present in all currently existing implementations of gradient boosting algorithms. In this paper, we provide a detailed analysis of this problem and demonstrate that proposed algorithms solve it effectively, leading to excellent empirical results.},
|
||||
booktitle = {Proceedings of the 32nd International Conference on Neural Information Processing Systems},
|
||||
pages = {6639–6649},
|
||||
numpages = {11},
|
||||
location = {Montr\'{e}al, Canada},
|
||||
series = {NIPS'18}
|
||||
}
|
||||
|
||||
|
||||
@article{lightgbm,
|
||||
title={Lightgbm: A highly efficient gradient boosting decision tree},
|
||||
author={Ke, Guolin and Meng, Qi and Finley, Thomas and Wang, Taifeng and Chen, Wei and Ma, Weidong and Ye, Qiwei and Liu, Tie-Yan},
|
||||
journal={Advances in neural information processing systems},
|
||||
volume={30},
|
||||
pages={3146--3154},
|
||||
year={2017}
|
||||
}
|
||||
|
||||
@inproceedings{xgboost,
|
||||
author = {Chen, Tianqi and Guestrin, Carlos},
|
||||
title = {{XGBoost}: A Scalable Tree Boosting System},
|
||||
booktitle = {Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining},
|
||||
series = {KDD '16},
|
||||
year = {2016},
|
||||
isbn = {978-1-4503-4232-2},
|
||||
location = {San Francisco, California, USA},
|
||||
pages = {785--794},
|
||||
numpages = {10},
|
||||
url = {http://doi.acm.org/10.1145/2939672.2939785},
|
||||
doi = {10.1145/2939672.2939785},
|
||||
acmid = {2939785},
|
||||
publisher = {ACM},
|
||||
address = {New York, NY, USA},
|
||||
keywords = {large-scale machine learning},
|
||||
}
|
||||
|
||||
@article{stable-baselines3,
|
||||
author = {Antonin Raffin and Ashley Hill and Adam Gleave and Anssi Kanervisto and Maximilian Ernestus and Noah Dormann},
|
||||
title = {Stable-Baselines3: Reliable Reinforcement Learning Implementations},
|
||||
journal = {Journal of Machine Learning Research},
|
||||
year = {2021},
|
||||
volume = {22},
|
||||
number = {268},
|
||||
pages = {1-8},
|
||||
url = {http://jmlr.org/papers/v22/20-1364.html}
|
||||
}
|
||||
|
||||
@misc{openai,
|
||||
title={OpenAI Gym},
|
||||
author={Greg Brockman and Vicki Cheung and Ludwig Pettersson and Jonas Schneider and John Schulman and Jie Tang and Wojciech Zaremba},
|
||||
year={2016},
|
||||
eprint={1606.01540},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.LG}
|
||||
}
|
||||
|
||||
@misc{tensorflow,
|
||||
title={ {TensorFlow}: Large-Scale Machine Learning on Heterogeneous Systems},
|
||||
url={https://www.tensorflow.org/},
|
||||
note={Software available from tensorflow.org},
|
||||
author={
|
||||
Mart\'{i}n~Abadi and
|
||||
Ashish~Agarwal and
|
||||
Paul~Barham and
|
||||
Eugene~Brevdo and
|
||||
Zhifeng~Chen and
|
||||
Craig~Citro and
|
||||
Greg~S.~Corrado and
|
||||
Andy~Davis and
|
||||
Jeffrey~Dean and
|
||||
Matthieu~Devin and
|
||||
Sanjay~Ghemawat and
|
||||
Ian~Goodfellow and
|
||||
Andrew~Harp and
|
||||
Geoffrey~Irving and
|
||||
Michael~Isard and
|
||||
Yangqing Jia and
|
||||
Rafal~Jozefowicz and
|
||||
Lukasz~Kaiser and
|
||||
Manjunath~Kudlur and
|
||||
Josh~Levenberg and
|
||||
Dandelion~Man\'{e} and
|
||||
Rajat~Monga and
|
||||
Sherry~Moore and
|
||||
Derek~Murray and
|
||||
Chris~Olah and
|
||||
Mike~Schuster and
|
||||
Jonathon~Shlens and
|
||||
Benoit~Steiner and
|
||||
Ilya~Sutskever and
|
||||
Kunal~Talwar and
|
||||
Paul~Tucker and
|
||||
Vincent~Vanhoucke and
|
||||
Vijay~Vasudevan and
|
||||
Fernanda~Vi\'{e}gas and
|
||||
Oriol~Vinyals and
|
||||
Pete~Warden and
|
||||
Martin~Wattenberg and
|
||||
Martin~Wicke and
|
||||
Yuan~Yu and
|
||||
Xiaoqiang~Zheng},
|
||||
year={2015},
|
||||
}
|
||||
|
||||
@incollection{pytorch,
|
||||
title = {PyTorch: An Imperative Style, High-Performance Deep Learning Library},
|
||||
author = {Paszke, Adam and Gross, Sam and Massa, Francisco and Lerer, Adam and Bradbury, James and Chanan, Gregory and Killeen, Trevor and Lin, Zeming and Gimelshein, Natalia and Antiga, Luca and Desmaison, Alban and Kopf, Andreas and Yang, Edward and DeVito, Zachary and Raison, Martin and Tejani, Alykhan and Chilamkurthy, Sasank and Steiner, Benoit and Fang, Lu and Bai, Junjie and Chintala, Soumith},
|
||||
booktitle = {Advances in Neural Information Processing Systems 32},
|
||||
editor = {H. Wallach and H. Larochelle and A. Beygelzimer and F. d\textquotesingle Alch\'{e}-Buc and E. Fox and R. Garnett},
|
||||
pages = {8024--8035},
|
||||
year = {2019},
|
||||
publisher = {Curran Associates, Inc.},
|
||||
url = {http://papers.neurips.cc/paper/9015-pytorch-an-imperative-style-high-performance-deep-learning-library.pdf}
|
||||
}
|
||||
|
||||
@ARTICLE{scipy,
|
||||
author = {Virtanen, Pauli and Gommers, Ralf and Oliphant, Travis E. and
|
||||
Haberland, Matt and Reddy, Tyler and Cournapeau, David and
|
||||
Burovski, Evgeni and Peterson, Pearu and Weckesser, Warren and
|
||||
Bright, Jonathan and {van der Walt}, St{\'e}fan J. and
|
||||
Brett, Matthew and Wilson, Joshua and Millman, K. Jarrod and
|
||||
Mayorov, Nikolay and Nelson, Andrew R. J. and Jones, Eric and
|
||||
Kern, Robert and Larson, Eric and Carey, C J and
|
||||
Polat, {\.I}lhan and Feng, Yu and Moore, Eric W. and
|
||||
{VanderPlas}, Jake and Laxalde, Denis and Perktold, Josef and
|
||||
Cimrman, Robert and Henriksen, Ian and Quintero, E. A. and
|
||||
Harris, Charles R. and Archibald, Anne M. and
|
||||
Ribeiro, Ant{\^o}nio H. and Pedregosa, Fabian and
|
||||
{van Mulbregt}, Paul and {SciPy 1.0 Contributors}},
|
||||
title = {{{SciPy} 1.0: Fundamental Algorithms for Scientific
|
||||
Computing in Python}},
|
||||
journal = {Nature Methods},
|
||||
year = {2020},
|
||||
volume = {17},
|
||||
pages = {261--272},
|
||||
adsurl = {https://rdcu.be/b08Wh},
|
||||
doi = {10.1038/s41592-019-0686-2},
|
||||
}
|
||||
|
||||
@Article{numpy,
|
||||
title = {Array programming with {NumPy}},
|
||||
author = {Charles R. Harris and K. Jarrod Millman and St{\'{e}}fan J.
|
||||
van der Walt and Ralf Gommers and Pauli Virtanen and David
|
||||
Cournapeau and Eric Wieser and Julian Taylor and Sebastian
|
||||
Berg and Nathaniel J. Smith and Robert Kern and Matti Picus
|
||||
and Stephan Hoyer and Marten H. van Kerkwijk and Matthew
|
||||
Brett and Allan Haldane and Jaime Fern{\'{a}}ndez del
|
||||
R{\'{i}}o and Mark Wiebe and Pearu Peterson and Pierre
|
||||
G{\'{e}}rard-Marchant and Kevin Sheppard and Tyler Reddy and
|
||||
Warren Weckesser and Hameer Abbasi and Christoph Gohlke and
|
||||
Travis E. Oliphant},
|
||||
year = {2020},
|
||||
month = sep,
|
||||
journal = {Nature},
|
||||
volume = {585},
|
||||
number = {7825},
|
||||
pages = {357--362},
|
||||
doi = {10.1038/s41586-020-2649-2},
|
||||
publisher = {Springer Science and Business Media {LLC}},
|
||||
url = {https://doi.org/10.1038/s41586-020-2649-2}
|
||||
}
|
||||
|
||||
@inproceedings{pandas,
|
||||
title={Data structures for statistical computing in python},
|
||||
author={McKinney, Wes and others},
|
||||
booktitle={Proceedings of the 9th Python in Science Conference},
|
||||
volume={445},
|
||||
pages={51--56},
|
||||
year={2010},
|
||||
organization={Austin, TX},
|
||||
doi={10.25080/Majora-92bf1922-00a}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@online{finrl,
|
||||
title = {AI4Finance-Foundation},
|
||||
year = 2022,
|
||||
url = {https://github.com/AI4Finance-Foundation/FinRL},
|
||||
urldate = {2022-09-30}
|
||||
}
|
||||
|
||||
|
||||
@online{tensortrade,
|
||||
title = {tensortrade},
|
||||
year = 2022,
|
||||
url = {https://tensortradex.readthedocs.io/en/latest/L},
|
||||
urldate = {2022-09-30}
|
||||
}
|
941
docs/JOSS_paper/paper.jats
Normal file
941
docs/JOSS_paper/paper.jats
Normal file
@@ -0,0 +1,941 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.2 20190208//EN"
|
||||
"JATS-publishing1.dtd">
|
||||
<article xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink" dtd-version="1.2" article-type="other">
|
||||
<front>
|
||||
<journal-meta>
|
||||
<journal-id></journal-id>
|
||||
<journal-title-group>
|
||||
<journal-title>Journal of Open Source Software</journal-title>
|
||||
<abbrev-journal-title>JOSS</abbrev-journal-title>
|
||||
</journal-title-group>
|
||||
<issn publication-format="electronic">2475-9066</issn>
|
||||
<publisher>
|
||||
<publisher-name>Open Journals</publisher-name>
|
||||
</publisher>
|
||||
</journal-meta>
|
||||
<article-meta>
|
||||
<article-id pub-id-type="publisher-id">0</article-id>
|
||||
<article-id pub-id-type="doi">N/A</article-id>
|
||||
<title-group>
|
||||
<article-title><monospace>FreqAI</monospace>: generalizing adaptive
|
||||
modeling for chaotic time-series market forecasts</article-title>
|
||||
</title-group>
|
||||
<contrib-group>
|
||||
<contrib contrib-type="author">
|
||||
<contrib-id contrib-id-type="orcid">0000-0001-5618-8629</contrib-id>
|
||||
<name>
|
||||
<surname>Ph.D</surname>
|
||||
<given-names>Robert A. Caulk</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-1"/>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<contrib-id contrib-id-type="orcid">0000-0003-3289-8604</contrib-id>
|
||||
<name>
|
||||
<surname>Ph.D</surname>
|
||||
<given-names>Elin Törnquist</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-1"/>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>Voppichler</surname>
|
||||
<given-names>Matthias</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>Lawless</surname>
|
||||
<given-names>Andrew R.</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>McMullan</surname>
|
||||
<given-names>Ryan</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>Santos</surname>
|
||||
<given-names>Wagner Costa</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-1"/>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>Pogue</surname>
|
||||
<given-names>Timothy C.</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-1"/>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>van der Vlugt</surname>
|
||||
<given-names>Johan</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>Gehring</surname>
|
||||
<given-names>Stefan P.</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<contrib contrib-type="author">
|
||||
<name>
|
||||
<surname>Schmidt</surname>
|
||||
<given-names>Pascal</given-names>
|
||||
</name>
|
||||
<xref ref-type="aff" rid="aff-2"/>
|
||||
</contrib>
|
||||
<aff id="aff-1">
|
||||
<institution-wrap>
|
||||
<institution>Emergent Methods LLC, Arvada Colorado, 80005,
|
||||
USA</institution>
|
||||
</institution-wrap>
|
||||
</aff>
|
||||
<aff id="aff-2">
|
||||
<institution-wrap>
|
||||
<institution>Freqtrade open source project</institution>
|
||||
</institution-wrap>
|
||||
</aff>
|
||||
</contrib-group>
|
||||
<volume>¿VOL?</volume>
|
||||
<issue>¿ISSUE?</issue>
|
||||
<fpage>¿PAGE?</fpage>
|
||||
<permissions>
|
||||
<copyright-statement>Authors of papers retain copyright and release the
|
||||
work under a Creative Commons Attribution 4.0 International License (CC
|
||||
BY 4.0)</copyright-statement>
|
||||
<copyright-year>2022</copyright-year>
|
||||
<copyright-holder>The article authors</copyright-holder>
|
||||
<license license-type="open-access" xlink:href="https://creativecommons.org/licenses/by/4.0/">
|
||||
<license-p>Authors of papers retain copyright and release the work under
|
||||
a Creative Commons Attribution 4.0 International License (CC BY
|
||||
4.0)</license-p>
|
||||
</license>
|
||||
</permissions>
|
||||
<kwd-group kwd-group-type="author">
|
||||
<kwd>Python</kwd>
|
||||
<kwd>Machine Learning</kwd>
|
||||
<kwd>adaptive modeling</kwd>
|
||||
<kwd>chaotic systems</kwd>
|
||||
<kwd>time-series forecasting</kwd>
|
||||
</kwd-group>
|
||||
</article-meta>
|
||||
</front>
|
||||
<body>
|
||||
<sec id="statement-of-need">
|
||||
<title>Statement of need</title>
|
||||
<p>Forecasting chaotic time-series based systems, such as
|
||||
equity/cryptocurrency markets, requires a broad set of tools geared
|
||||
toward testing a wide range of hypotheses. Fortunately, a recent
|
||||
maturation of robust machine learning libraries
|
||||
(e.g. <monospace>scikit-learn</monospace>), has opened up a wide range
|
||||
of research possibilities. Scientists from a diverse range of fields
|
||||
can now easily prototype their studies on an abundance of established
|
||||
machine learning algorithms. Similarly, these user-friendly libraries
|
||||
enable “citzen scientists” to use their basic Python skills for
|
||||
data-exploration. However, leveraging these machine learning libraries
|
||||
on historical and live chaotic data sources can be logistically
|
||||
difficult and expensive. Additionally, robust data-collection,
|
||||
storage, and handling presents a disparate challenge.
|
||||
<ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/freqai/"><monospace>FreqAI</monospace></ext-link>
|
||||
aims to provide a generalized and extensible open-sourced framework
|
||||
geared toward live deployments of adaptive modeling for market
|
||||
forecasting. The <monospace>FreqAI</monospace> framework is
|
||||
effectively a sandbox for the rich world of open-source machine
|
||||
learning libraries. Inside the <monospace>FreqAI</monospace> sandbox,
|
||||
users find they can combine a wide variety of third-party libraries to
|
||||
test creative hypotheses on a free live 24/7 chaotic data source -
|
||||
cryptocurrency exchange data.</p>
|
||||
</sec>
|
||||
<sec id="summary">
|
||||
<title>Summary</title>
|
||||
<p><ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/freqai/"><monospace>FreqAI</monospace></ext-link>
|
||||
evolved from a desire to test and compare a range of adaptive
|
||||
time-series forecasting methods on chaotic data. Cryptocurrency
|
||||
markets provide a unique data source since they are operational 24/7
|
||||
and the data is freely available. Luckily, an existing open-source
|
||||
software,
|
||||
<ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/stable/"><monospace>Freqtrade</monospace></ext-link>,
|
||||
had already matured under a range of talented developers to support
|
||||
robust data collection/storage, as well as robust live environmental
|
||||
interactions for standard algorithmic trading.
|
||||
<monospace>Freqtrade</monospace> also provides a set of data
|
||||
analysis/visualization tools for the evaluation of historical
|
||||
performance as well as live environmental feedback.
|
||||
<monospace>FreqAI</monospace> builds on top of
|
||||
<monospace>Freqtrade</monospace> to include a user-friendly well
|
||||
tested interface for integrating external machine learning libraries
|
||||
for adaptive time-series forecasting. Beyond enabling the integration
|
||||
of existing libraries, <monospace>FreqAI</monospace> hosts a range of
|
||||
custom algorithms and methodologies aimed at improving computational
|
||||
and predictive performances. Thus, <monospace>FreqAI</monospace>
|
||||
contains a range of unique features which can be easily tested in
|
||||
combination with all the existing Python-accessible machine learning
|
||||
libraries to generate novel research on live and historical data.</p>
|
||||
<p>The high-level overview of the software is depicted in Figure
|
||||
1.</p>
|
||||
<p><named-content content-type="image">freqai-algo</named-content>
|
||||
<italic>Abstracted overview of FreqAI algorithm</italic></p>
|
||||
<sec id="connecting-machine-learning-libraries">
|
||||
<title>Connecting machine learning libraries</title>
|
||||
<p>Although the <monospace>FreqAI</monospace> framework is designed
|
||||
to accommodate any Python library in the “Model training” and
|
||||
“Feature set engineering” portions of the software (Figure 1), it
|
||||
already boasts a wide range of well documented examples based on
|
||||
various combinations of:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>scikit-learn
|
||||
(<xref alt="Pedregosa et al., 2011" rid="ref-scikit-learn" ref-type="bibr">Pedregosa
|
||||
et al., 2011</xref>), Catboost
|
||||
(<xref alt="Prokhorenkova et al., 2018" rid="ref-catboost" ref-type="bibr">Prokhorenkova
|
||||
et al., 2018</xref>), LightGBM
|
||||
(<xref alt="Ke et al., 2017" rid="ref-lightgbm" ref-type="bibr">Ke
|
||||
et al., 2017</xref>), XGBoost
|
||||
(<xref alt="Chen & Guestrin, 2016" rid="ref-xgboost" ref-type="bibr">Chen
|
||||
& Guestrin, 2016</xref>), stable_baselines3
|
||||
(<xref alt="Raffin et al., 2021" rid="ref-stable-baselines3" ref-type="bibr">Raffin
|
||||
et al., 2021</xref>), openai gym
|
||||
(<xref alt="Brockman et al., 2016" rid="ref-openai" ref-type="bibr">Brockman
|
||||
et al., 2016</xref>), tensorflow
|
||||
(<xref alt="Abadi et al., 2015" rid="ref-tensorflow" ref-type="bibr">Abadi
|
||||
et al., 2015</xref>), pytorch
|
||||
(<xref alt="Paszke et al., 2019" rid="ref-pytorch" ref-type="bibr">Paszke
|
||||
et al., 2019</xref>), Scipy
|
||||
(<xref alt="Virtanen et al., 2020" rid="ref-scipy" ref-type="bibr">Virtanen
|
||||
et al., 2020</xref>), Numpy
|
||||
(<xref alt="Harris et al., 2020" rid="ref-numpy" ref-type="bibr">Harris
|
||||
et al., 2020</xref>), and pandas
|
||||
(<xref alt="McKinney & others, 2010" rid="ref-pandas" ref-type="bibr">McKinney
|
||||
& others, 2010</xref>).</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>These mature projects contain a wide range of peer-reviewed and
|
||||
industry standard methods, including:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>Regression, Classification, Neural Networks, Reinforcement
|
||||
Learning, Support Vector Machines, Principal Component Analysis,
|
||||
point clustering, and much more.</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>which are all leveraged in <monospace>FreqAI</monospace> for
|
||||
users to use as templates or extend with their own methods.</p>
|
||||
</sec>
|
||||
<sec id="furnishing-novel-methods-and-features">
|
||||
<title>Furnishing novel methods and features</title>
|
||||
<p>Beyond the industry standard methods available through external
|
||||
libraries - <monospace>FreqAI</monospace> includes novel methods
|
||||
which are not available anywhere else in the open-source (or
|
||||
scientific) world. For example, <monospace>FreqAI</monospace>
|
||||
provides :</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>a custom algorithm/methodology for adaptive modeling</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>rapid and self-monitored feature engineering tools</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>unique model features/indicators</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>optimized data collection algorithms</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>safely integrated outlier detection methods</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>websocket communicated forecasts</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>Of particular interest for researchers,
|
||||
<monospace>FreqAI</monospace> provides the option of large scale
|
||||
experimentation via an optimized websocket communications
|
||||
interface.</p>
|
||||
</sec>
|
||||
<sec id="optimizing-the-back-end">
|
||||
<title>Optimizing the back-end</title>
|
||||
<p><monospace>FreqAI</monospace> aims to make it simple for users to
|
||||
combine all the above tools to run studies based in two distinct
|
||||
modules:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>backtesting studies</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>live-deployments</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>Both of these modules and their respective data management
|
||||
systems are built on top of
|
||||
<ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/"><monospace>Freqtrade</monospace></ext-link>,
|
||||
a mature and actively developed cryptocurrency trading software.
|
||||
This means that <monospace>FreqAI</monospace> benefits from a wide
|
||||
range of tangential/disparate feature developments such as:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>FreqUI, a graphical interface for backtesting and live
|
||||
monitoring</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>telegram control</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>robust database handling</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>futures/leverage trading</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>dollar cost averaging</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>trading strategy handling</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>a variety of free data sources via CCXT (FTX, Binance, Kucoin
|
||||
etc.)</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>These features derive from a strong external developer community
|
||||
that shares in the benefit and stability of a communal CI
|
||||
(Continuous Integration) system. Beyond the developer community,
|
||||
<monospace>FreqAI</monospace> benefits strongly from the userbase of
|
||||
<monospace>Freqtrade</monospace>, where most
|
||||
<monospace>FreqAI</monospace> beta-testers/developers originated.
|
||||
This symbiotic relationship between <monospace>Freqtrade</monospace>
|
||||
and <monospace>FreqAI</monospace> ignited a thoroughly tested
|
||||
<ext-link ext-link-type="uri" xlink:href="https://github.com/freqtrade/freqtrade/pull/6832"><monospace>beta</monospace></ext-link>,
|
||||
which demanded a four month beta and
|
||||
<ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/freqai/">comprehensive
|
||||
documentation</ext-link> containing:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>numerous example scripts</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>a full parameter table</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>methodological descriptions</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>high-resolution diagrams/figures</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>detailed parameter setting recommendations</p>
|
||||
</list-item>
|
||||
</list>
|
||||
</sec>
|
||||
<sec id="providing-a-reproducible-foundation-for-researchers">
|
||||
<title>Providing a reproducible foundation for researchers</title>
|
||||
<p><monospace>FreqAI</monospace> provides an extensible, robust,
|
||||
framework for researchers and citizen data scientists. The
|
||||
<monospace>FreqAI</monospace> sandbox enables rapid conception and
|
||||
testing of exotic hypotheses. From a research perspective,
|
||||
<monospace>FreqAI</monospace> handles the multitude of logistics
|
||||
associated with live deployments, historical backtesting, and
|
||||
feature engineering. With <monospace>FreqAI</monospace>, researchers
|
||||
can focus on their primary interests of feature engineering and
|
||||
hypothesis testing rather than figuring out how to collect and
|
||||
handle data. Further - the well maintained and easily installed
|
||||
open-source framework of <monospace>FreqAI</monospace> enables
|
||||
reproducible scientific studies. This reproducibility component is
|
||||
essential to general scientific advancement in time-series
|
||||
forecasting for chaotic systems.</p>
|
||||
</sec>
|
||||
</sec>
|
||||
<sec id="technical-details">
|
||||
<title>Technical details</title>
|
||||
<p>Typical users configure <monospace>FreqAI</monospace> via two
|
||||
files:</p>
|
||||
<list list-type="order">
|
||||
<list-item>
|
||||
<p>A <monospace>configuration</monospace> file
|
||||
(<monospace>--config</monospace>) which provides access to the
|
||||
full parameter list available
|
||||
<ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/freqai/">here</ext-link>:</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>control high-level feature engineering</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>customize adaptive modeling techniques</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>set any model training parameters available in third-party
|
||||
libraries</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>manage adaptive modeling parameters (retrain frequency,
|
||||
training window size, continual learning, etc.)</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<list list-type="order">
|
||||
<list-item>
|
||||
<label>2.</label>
|
||||
<p>A strategy file (<monospace>--strategy</monospace>) where
|
||||
users:</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>list of the base training features</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>set standard technical-analysis strategies</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>control trade entry/exit criteria</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>With these two files, most users can exploit a wide range of
|
||||
pre-existing integrations in <monospace>Catboost</monospace> and 7
|
||||
other libraries with a simple command:</p>
|
||||
<preformat>freqtrade trade --config config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel CatboostRegressor</preformat>
|
||||
<p>Advanced users will edit one of the existing
|
||||
<monospace>--freqaimodel</monospace> files, which are simply an
|
||||
children of the <monospace>IFreqaiModel</monospace> (details below).
|
||||
Within these files, advanced users can customize training procedures,
|
||||
prediction procedures, outlier detection methods, data preparation,
|
||||
data saving methods, etc. This is all configured in a way where they
|
||||
can customize as little or as much as they want. This flexible
|
||||
customization is owed to the foundational architecture in
|
||||
<monospace>FreqAI</monospace>, which is comprised of three distinct
|
||||
Python objects:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p><monospace>IFreqaiModel</monospace></p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>A singular long-lived object containing all the necessary
|
||||
logic to collect data, store data, process data, engineer
|
||||
features, run training, and inference models.</p>
|
||||
</list-item>
|
||||
</list>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p><monospace>FreqaiDataKitchen</monospace></p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>A short-lived object which is uniquely created for each
|
||||
asset/model. Beyond metadata, it also contains a variety of
|
||||
data processing tools.</p>
|
||||
</list-item>
|
||||
</list>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p><monospace>FreqaiDataDrawer</monospace></p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>Singular long-lived object containing all the historical
|
||||
predictions, models, and save/load methods.</p>
|
||||
</list-item>
|
||||
</list>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>These objects interact with one another with one goal in mind - to
|
||||
provide a clean data set to machine learning experts/enthusiasts at
|
||||
the user endpoint. These power-users interact with an inherited
|
||||
<monospace>IFreqaiModel</monospace> that allows them to dig as deep or
|
||||
as shallow as they wish into the inheritence tree. Typical power-users
|
||||
focus their efforts on customizing training procedures and testing
|
||||
exotic functionalities available in third-party libraries. Thus,
|
||||
power-users are freed from the algorithmic weight associated with data
|
||||
management, and can instead focus their energy on testing creative
|
||||
hypotheses. Meanwhile, some users choose to override deeper
|
||||
functionalities within <monospace>IFreqaiModel</monospace> to help
|
||||
them craft unique data structures and training procedures.</p>
|
||||
<p>The class structure and algorithmic details are depicted in the
|
||||
following diagram:</p>
|
||||
<p><named-content content-type="image">image</named-content>
|
||||
<italic>Class diagram summarizing object interactions in
|
||||
FreqAI</italic></p>
|
||||
</sec>
|
||||
<sec id="online-documentation">
|
||||
<title>Online documentation</title>
|
||||
<p>The documentation for
|
||||
<ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/freqai/"><monospace>FreqAI</monospace></ext-link>
|
||||
is available online at
|
||||
<ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/freqai/">https://www.freqtrade.io/en/latest/freqai/</ext-link>
|
||||
and covers a wide range of materials:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>Quick-start with a single command and example files -
|
||||
(beginners)</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Introduction to the feature engineering interface and basic
|
||||
configurations - (intermediate users)</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Parameter table with indepth descriptions and default parameter
|
||||
setting recommendations - (intermediate users)</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Data analysis and post-processing - (advanced users)</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Methodological considerations complemented by high resolution
|
||||
figures - (advanced users)</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Instructions for integrating third party machine learning
|
||||
libraries into custom prediction models - (advanced users)</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Software architectural description with class diagram -
|
||||
(developers)</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>File structure descriptions - (developers)</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>The docs direct users to a variety of pre-made examples which
|
||||
integrate <monospace>Catboost</monospace>,
|
||||
<monospace>LightGBM</monospace>, <monospace>XGBoost</monospace>,
|
||||
<monospace>Sklearn</monospace>,
|
||||
<monospace>stable_baselines3</monospace>,
|
||||
<monospace>torch</monospace>, <monospace>tensorflow</monospace>.
|
||||
Meanwhile, developers will also find thorough docstrings and type
|
||||
hinting throughout the source code to aid in code readability and
|
||||
customization.</p>
|
||||
<p><monospace>FreqAI</monospace> also benefits from a strong support
|
||||
network of users and developers on the
|
||||
<ext-link ext-link-type="uri" xlink:href="https://discord.gg/w6nDM6cM4y"><monospace>Freqtrade</monospace>
|
||||
discord</ext-link> as well as on the
|
||||
<ext-link ext-link-type="uri" xlink:href="https://discord.gg/xE4RMg4QYw"><monospace>FreqAI</monospace>
|
||||
discord</ext-link>. Within the <monospace>FreqAI</monospace> discord,
|
||||
users will find a deep and easily searched knowledge base containing
|
||||
common errors. But more importantly, users in the
|
||||
<monospace>FreqAI</monospace> discord share anectdotal and
|
||||
quantitative observations which compare performance between various
|
||||
third-party libraries and methods.</p>
|
||||
</sec>
|
||||
<sec id="state-of-the-field">
|
||||
<title>State of the field</title>
|
||||
<p>There are two other open-source tools which are geared toward
|
||||
helping users build models for time-series forecasts on market based
|
||||
data. However, each of these tools suffer from a non-generalized
|
||||
frameworks that do not permit comparison of methods and libraries.
|
||||
Additionally, they do not permit easy live-deployments or
|
||||
adaptive-modeling methods. For example, two open-sourced projects
|
||||
called
|
||||
<ext-link ext-link-type="uri" xlink:href="https://tensortradex.readthedocs.io/en/latest/"><monospace>tensortrade</monospace></ext-link>
|
||||
(<xref alt="Tensortrade, 2022" rid="ref-tensortrade" ref-type="bibr"><italic>Tensortrade</italic>,
|
||||
2022</xref>) and
|
||||
<ext-link ext-link-type="uri" xlink:href="https://github.com/AI4Finance-Foundation/FinRL"><monospace>FinRL</monospace></ext-link>
|
||||
(<xref alt="AI4Finance-Foundation, 2022" rid="ref-finrl" ref-type="bibr"><italic>AI4Finance-Foundation</italic>,
|
||||
2022</xref>) limit users to the exploration of reinforcement learning
|
||||
on historical data. These softwares also do not provide robust live
|
||||
deployments, they do not furnish novel feature engineering algorithms,
|
||||
and they do not provide custom data analysis tools.
|
||||
<monospace>FreqAI</monospace> fills the gap.</p>
|
||||
</sec>
|
||||
<sec id="on-going-research">
|
||||
<title>On-going research</title>
|
||||
<p>Emergent Methods, based in Arvada CO, is actively using
|
||||
<monospace>FreqAI</monospace> to perform large scale experiments aimed
|
||||
at comparing machine learning libraries in live and historical
|
||||
environments. Past projects include backtesting parametric sweeps,
|
||||
while active projects include a 3 week live deployment comparison
|
||||
between <monospace>CatboosRegressor</monospace>,
|
||||
<monospace>LightGBMRegressor</monospace>, and
|
||||
<monospace>XGBoostRegressor</monospace>. Results from these studies
|
||||
are on track for publication in scientific journals as well as more
|
||||
general data science blogs (e.g. Medium).</p>
|
||||
</sec>
|
||||
<sec id="installing-and-running-freqai">
|
||||
<title>Installing and running <monospace>FreqAI</monospace></title>
|
||||
<p><monospace>FreqAI</monospace> is automatically installed with
|
||||
<monospace>Freqtrade</monospace> using the following commands on linux
|
||||
systems:</p>
|
||||
<preformat>git clone git@github.com:freqtrade/freqtrade.git
|
||||
cd freqtrade
|
||||
./setup.sh -i</preformat>
|
||||
<p>However, <monospace>FreqAI</monospace> also benefits from
|
||||
<monospace>Freqtrade</monospace> docker distributions, and can be run
|
||||
with docker by pulling the stable or develop images from
|
||||
<monospace>Freqtrade</monospace> distributions.</p>
|
||||
</sec>
|
||||
<sec id="funding-sources">
|
||||
<title>Funding sources</title>
|
||||
<p><ext-link ext-link-type="uri" xlink:href="https://www.freqtrade.io/en/latest/freqai/"><monospace>FreqAI</monospace></ext-link>
|
||||
has had no official sponsors, and is entirely grass roots. All
|
||||
donations into the project (e.g. the GitHub sponsor system) are kept
|
||||
inside the project to help support development of open-sourced and
|
||||
communally beneficial features.</p>
|
||||
</sec>
|
||||
<sec id="acknowledgements">
|
||||
<title>Acknowledgements</title>
|
||||
<p>We would like to acknowledge various beta testers of
|
||||
<monospace>FreqAI</monospace>:</p>
|
||||
<list list-type="bullet">
|
||||
<list-item>
|
||||
<p>Richárd Józsa</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Juha Nykänen</p>
|
||||
</list-item>
|
||||
<list-item>
|
||||
<p>Salah Lamkadem</p>
|
||||
</list-item>
|
||||
</list>
|
||||
<p>As well as various <monospace>Freqtrade</monospace>
|
||||
<ext-link ext-link-type="uri" xlink:href="https://github.com/freqtrade/freqtrade/graphs/contributors">developers</ext-link>
|
||||
maintaining tangential, yet essential, modules.</p>
|
||||
</sec>
|
||||
</body>
|
||||
<back>
|
||||
<ref-list>
|
||||
<ref id="ref-scikit-learn">
|
||||
<element-citation publication-type="article-journal">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Pedregosa</surname><given-names>F.</given-names></name>
|
||||
<name><surname>Varoquaux</surname><given-names>G.</given-names></name>
|
||||
<name><surname>Gramfort</surname><given-names>A.</given-names></name>
|
||||
<name><surname>Michel</surname><given-names>V.</given-names></name>
|
||||
<name><surname>Thirion</surname><given-names>B.</given-names></name>
|
||||
<name><surname>Grisel</surname><given-names>O.</given-names></name>
|
||||
<name><surname>Blondel</surname><given-names>M.</given-names></name>
|
||||
<name><surname>Prettenhofer</surname><given-names>P.</given-names></name>
|
||||
<name><surname>Weiss</surname><given-names>R.</given-names></name>
|
||||
<name><surname>Dubourg</surname><given-names>V.</given-names></name>
|
||||
<name><surname>Vanderplas</surname><given-names>J.</given-names></name>
|
||||
<name><surname>Passos</surname><given-names>A.</given-names></name>
|
||||
<name><surname>Cournapeau</surname><given-names>D.</given-names></name>
|
||||
<name><surname>Brucher</surname><given-names>M.</given-names></name>
|
||||
<name><surname>Perrot</surname><given-names>M.</given-names></name>
|
||||
<name><surname>Duchesnay</surname><given-names>E.</given-names></name>
|
||||
</person-group>
|
||||
<article-title>Scikit-learn: Machine learning in Python</article-title>
|
||||
<source>Journal of Machine Learning Research</source>
|
||||
<year iso-8601-date="2011">2011</year>
|
||||
<volume>12</volume>
|
||||
<fpage>2825</fpage>
|
||||
<lpage>2830</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-catboost">
|
||||
<element-citation publication-type="paper-conference">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Prokhorenkova</surname><given-names>Liudmila</given-names></name>
|
||||
<name><surname>Gusev</surname><given-names>Gleb</given-names></name>
|
||||
<name><surname>Vorobev</surname><given-names>Aleksandr</given-names></name>
|
||||
<name><surname>Dorogush</surname><given-names>Anna Veronika</given-names></name>
|
||||
<name><surname>Gulin</surname><given-names>Andrey</given-names></name>
|
||||
</person-group>
|
||||
<article-title>CatBoost: Unbiased boosting with categorical features</article-title>
|
||||
<source>Proceedings of the 32nd international conference on neural information processing systems</source>
|
||||
<publisher-name>Curran Associates Inc.</publisher-name>
|
||||
<publisher-loc>Red Hook, NY, USA</publisher-loc>
|
||||
<year iso-8601-date="2018">2018</year>
|
||||
<fpage>6639</fpage>
|
||||
<lpage>6649</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-lightgbm">
|
||||
<element-citation publication-type="article-journal">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Ke</surname><given-names>Guolin</given-names></name>
|
||||
<name><surname>Meng</surname><given-names>Qi</given-names></name>
|
||||
<name><surname>Finley</surname><given-names>Thomas</given-names></name>
|
||||
<name><surname>Wang</surname><given-names>Taifeng</given-names></name>
|
||||
<name><surname>Chen</surname><given-names>Wei</given-names></name>
|
||||
<name><surname>Ma</surname><given-names>Weidong</given-names></name>
|
||||
<name><surname>Ye</surname><given-names>Qiwei</given-names></name>
|
||||
<name><surname>Liu</surname><given-names>Tie-Yan</given-names></name>
|
||||
</person-group>
|
||||
<article-title>Lightgbm: A highly efficient gradient boosting decision tree</article-title>
|
||||
<source>Advances in neural information processing systems</source>
|
||||
<year iso-8601-date="2017">2017</year>
|
||||
<volume>30</volume>
|
||||
<fpage>3146</fpage>
|
||||
<lpage>3154</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-xgboost">
|
||||
<element-citation publication-type="paper-conference">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Chen</surname><given-names>Tianqi</given-names></name>
|
||||
<name><surname>Guestrin</surname><given-names>Carlos</given-names></name>
|
||||
</person-group>
|
||||
<article-title>XGBoost: A scalable tree boosting system</article-title>
|
||||
<source>Proceedings of the 22nd ACM SIGKDD international conference on knowledge discovery and data mining</source>
|
||||
<publisher-name>ACM</publisher-name>
|
||||
<publisher-loc>New York, NY, USA</publisher-loc>
|
||||
<year iso-8601-date="2016">2016</year>
|
||||
<isbn>978-1-4503-4232-2</isbn>
|
||||
<uri>http://doi.acm.org/10.1145/2939672.2939785</uri>
|
||||
<pub-id pub-id-type="doi">10.1145/2939672.2939785</pub-id>
|
||||
<fpage>785</fpage>
|
||||
<lpage>794</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-stable-baselines3">
|
||||
<element-citation publication-type="article-journal">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Raffin</surname><given-names>Antonin</given-names></name>
|
||||
<name><surname>Hill</surname><given-names>Ashley</given-names></name>
|
||||
<name><surname>Gleave</surname><given-names>Adam</given-names></name>
|
||||
<name><surname>Kanervisto</surname><given-names>Anssi</given-names></name>
|
||||
<name><surname>Ernestus</surname><given-names>Maximilian</given-names></name>
|
||||
<name><surname>Dormann</surname><given-names>Noah</given-names></name>
|
||||
</person-group>
|
||||
<article-title>Stable-Baselines3: Reliable reinforcement learning implementations</article-title>
|
||||
<source>Journal of Machine Learning Research</source>
|
||||
<year iso-8601-date="2021">2021</year>
|
||||
<volume>22</volume>
|
||||
<issue>268</issue>
|
||||
<uri>http://jmlr.org/papers/v22/20-1364.html</uri>
|
||||
<fpage>1</fpage>
|
||||
<lpage>8</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-openai">
|
||||
<element-citation>
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Brockman</surname><given-names>Greg</given-names></name>
|
||||
<name><surname>Cheung</surname><given-names>Vicki</given-names></name>
|
||||
<name><surname>Pettersson</surname><given-names>Ludwig</given-names></name>
|
||||
<name><surname>Schneider</surname><given-names>Jonas</given-names></name>
|
||||
<name><surname>Schulman</surname><given-names>John</given-names></name>
|
||||
<name><surname>Tang</surname><given-names>Jie</given-names></name>
|
||||
<name><surname>Zaremba</surname><given-names>Wojciech</given-names></name>
|
||||
</person-group>
|
||||
<article-title>OpenAI gym</article-title>
|
||||
<year iso-8601-date="2016">2016</year>
|
||||
<uri>https://arxiv.org/abs/1606.01540</uri>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-tensorflow">
|
||||
<element-citation>
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Abadi</surname><given-names>Martín</given-names></name>
|
||||
<name><surname>Agarwal</surname><given-names>Ashish</given-names></name>
|
||||
<name><surname>Barham</surname><given-names>Paul</given-names></name>
|
||||
<name><surname>Brevdo</surname><given-names>Eugene</given-names></name>
|
||||
<name><surname>Chen</surname><given-names>Zhifeng</given-names></name>
|
||||
<name><surname>Citro</surname><given-names>Craig</given-names></name>
|
||||
<name><surname>Corrado</surname><given-names>Greg S.</given-names></name>
|
||||
<name><surname>Davis</surname><given-names>Andy</given-names></name>
|
||||
<name><surname>Dean</surname><given-names>Jeffrey</given-names></name>
|
||||
<name><surname>Devin</surname><given-names>Matthieu</given-names></name>
|
||||
<name><surname>Ghemawat</surname><given-names>Sanjay</given-names></name>
|
||||
<name><surname>Goodfellow</surname><given-names>Ian</given-names></name>
|
||||
<name><surname>Harp</surname><given-names>Andrew</given-names></name>
|
||||
<name><surname>Irving</surname><given-names>Geoffrey</given-names></name>
|
||||
<name><surname>Isard</surname><given-names>Michael</given-names></name>
|
||||
<name><surname>Jia</surname><given-names>Yangqing</given-names></name>
|
||||
<name><surname>Jozefowicz</surname><given-names>Rafal</given-names></name>
|
||||
<name><surname>Kaiser</surname><given-names>Lukasz</given-names></name>
|
||||
<name><surname>Kudlur</surname><given-names>Manjunath</given-names></name>
|
||||
<name><surname>Levenberg</surname><given-names>Josh</given-names></name>
|
||||
<name><surname>Mané</surname><given-names>Dandelion</given-names></name>
|
||||
<name><surname>Monga</surname><given-names>Rajat</given-names></name>
|
||||
<name><surname>Moore</surname><given-names>Sherry</given-names></name>
|
||||
<name><surname>Murray</surname><given-names>Derek</given-names></name>
|
||||
<name><surname>Olah</surname><given-names>Chris</given-names></name>
|
||||
<name><surname>Schuster</surname><given-names>Mike</given-names></name>
|
||||
<name><surname>Shlens</surname><given-names>Jonathon</given-names></name>
|
||||
<name><surname>Steiner</surname><given-names>Benoit</given-names></name>
|
||||
<name><surname>Sutskever</surname><given-names>Ilya</given-names></name>
|
||||
<name><surname>Talwar</surname><given-names>Kunal</given-names></name>
|
||||
<name><surname>Tucker</surname><given-names>Paul</given-names></name>
|
||||
<name><surname>Vanhoucke</surname><given-names>Vincent</given-names></name>
|
||||
<name><surname>Vasudevan</surname><given-names>Vijay</given-names></name>
|
||||
<name><surname>Viégas</surname><given-names>Fernanda</given-names></name>
|
||||
<name><surname>Vinyals</surname><given-names>Oriol</given-names></name>
|
||||
<name><surname>Warden</surname><given-names>Pete</given-names></name>
|
||||
<name><surname>Wattenberg</surname><given-names>Martin</given-names></name>
|
||||
<name><surname>Wicke</surname><given-names>Martin</given-names></name>
|
||||
<name><surname>Yu</surname><given-names>Yuan</given-names></name>
|
||||
<name><surname>Zheng</surname><given-names>Xiaoqiang</given-names></name>
|
||||
</person-group>
|
||||
<article-title>TensorFlow: Large-scale machine learning on heterogeneous systems</article-title>
|
||||
<year iso-8601-date="2015">2015</year>
|
||||
<uri>https://www.tensorflow.org/</uri>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-pytorch">
|
||||
<element-citation publication-type="chapter">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Paszke</surname><given-names>Adam</given-names></name>
|
||||
<name><surname>Gross</surname><given-names>Sam</given-names></name>
|
||||
<name><surname>Massa</surname><given-names>Francisco</given-names></name>
|
||||
<name><surname>Lerer</surname><given-names>Adam</given-names></name>
|
||||
<name><surname>Bradbury</surname><given-names>James</given-names></name>
|
||||
<name><surname>Chanan</surname><given-names>Gregory</given-names></name>
|
||||
<name><surname>Killeen</surname><given-names>Trevor</given-names></name>
|
||||
<name><surname>Lin</surname><given-names>Zeming</given-names></name>
|
||||
<name><surname>Gimelshein</surname><given-names>Natalia</given-names></name>
|
||||
<name><surname>Antiga</surname><given-names>Luca</given-names></name>
|
||||
<name><surname>Desmaison</surname><given-names>Alban</given-names></name>
|
||||
<name><surname>Kopf</surname><given-names>Andreas</given-names></name>
|
||||
<name><surname>Yang</surname><given-names>Edward</given-names></name>
|
||||
<name><surname>DeVito</surname><given-names>Zachary</given-names></name>
|
||||
<name><surname>Raison</surname><given-names>Martin</given-names></name>
|
||||
<name><surname>Tejani</surname><given-names>Alykhan</given-names></name>
|
||||
<name><surname>Chilamkurthy</surname><given-names>Sasank</given-names></name>
|
||||
<name><surname>Steiner</surname><given-names>Benoit</given-names></name>
|
||||
<name><surname>Fang</surname><given-names>Lu</given-names></name>
|
||||
<name><surname>Bai</surname><given-names>Junjie</given-names></name>
|
||||
<name><surname>Chintala</surname><given-names>Soumith</given-names></name>
|
||||
</person-group>
|
||||
<article-title>PyTorch: An imperative style, high-performance deep learning library</article-title>
|
||||
<source>Advances in neural information processing systems 32</source>
|
||||
<person-group person-group-type="editor">
|
||||
<name><surname>Wallach</surname><given-names>H.</given-names></name>
|
||||
<name><surname>Larochelle</surname><given-names>H.</given-names></name>
|
||||
<name><surname>Beygelzimer</surname><given-names>A.</given-names></name>
|
||||
<name><surname>dAlché-Buc</surname><given-names>F.</given-names></name>
|
||||
<name><surname>Fox</surname><given-names>E.</given-names></name>
|
||||
<name><surname>Garnett</surname><given-names>R.</given-names></name>
|
||||
</person-group>
|
||||
<publisher-name>Curran Associates, Inc.</publisher-name>
|
||||
<year iso-8601-date="2019">2019</year>
|
||||
<uri>http://papers.neurips.cc/paper/9015-pytorch-an-imperative-style-high-performance-deep-learning-library.pdf</uri>
|
||||
<fpage>8024</fpage>
|
||||
<lpage>8035</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-scipy">
|
||||
<element-citation publication-type="article-journal">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Virtanen</surname><given-names>Pauli</given-names></name>
|
||||
<name><surname>Gommers</surname><given-names>Ralf</given-names></name>
|
||||
<name><surname>Oliphant</surname><given-names>Travis E.</given-names></name>
|
||||
<name><surname>Haberland</surname><given-names>Matt</given-names></name>
|
||||
<name><surname>Reddy</surname><given-names>Tyler</given-names></name>
|
||||
<name><surname>Cournapeau</surname><given-names>David</given-names></name>
|
||||
<name><surname>Burovski</surname><given-names>Evgeni</given-names></name>
|
||||
<name><surname>Peterson</surname><given-names>Pearu</given-names></name>
|
||||
<name><surname>Weckesser</surname><given-names>Warren</given-names></name>
|
||||
<name><surname>Bright</surname><given-names>Jonathan</given-names></name>
|
||||
<name><surname>van der Walt</surname><given-names>Stéfan J.</given-names></name>
|
||||
<name><surname>Brett</surname><given-names>Matthew</given-names></name>
|
||||
<name><surname>Wilson</surname><given-names>Joshua</given-names></name>
|
||||
<name><surname>Millman</surname><given-names>K. Jarrod</given-names></name>
|
||||
<name><surname>Mayorov</surname><given-names>Nikolay</given-names></name>
|
||||
<name><surname>Nelson</surname><given-names>Andrew R. J.</given-names></name>
|
||||
<name><surname>Jones</surname><given-names>Eric</given-names></name>
|
||||
<name><surname>Kern</surname><given-names>Robert</given-names></name>
|
||||
<name><surname>Larson</surname><given-names>Eric</given-names></name>
|
||||
<name><surname>Carey</surname><given-names>C J</given-names></name>
|
||||
<name><surname>Polat</surname><given-names>İlhan</given-names></name>
|
||||
<name><surname>Feng</surname><given-names>Yu</given-names></name>
|
||||
<name><surname>Moore</surname><given-names>Eric W.</given-names></name>
|
||||
<name><surname>VanderPlas</surname><given-names>Jake</given-names></name>
|
||||
<name><surname>Laxalde</surname><given-names>Denis</given-names></name>
|
||||
<name><surname>Perktold</surname><given-names>Josef</given-names></name>
|
||||
<name><surname>Cimrman</surname><given-names>Robert</given-names></name>
|
||||
<name><surname>Henriksen</surname><given-names>Ian</given-names></name>
|
||||
<name><surname>Quintero</surname><given-names>E. A.</given-names></name>
|
||||
<name><surname>Harris</surname><given-names>Charles R.</given-names></name>
|
||||
<name><surname>Archibald</surname><given-names>Anne M.</given-names></name>
|
||||
<name><surname>Ribeiro</surname><given-names>Antônio H.</given-names></name>
|
||||
<name><surname>Pedregosa</surname><given-names>Fabian</given-names></name>
|
||||
<name><surname>van Mulbregt</surname><given-names>Paul</given-names></name>
|
||||
<string-name>SciPy 1.0 Contributors</string-name>
|
||||
</person-group>
|
||||
<article-title>SciPy 1.0: Fundamental Algorithms for Scientific Computing in Python</article-title>
|
||||
<source>Nature Methods</source>
|
||||
<year iso-8601-date="2020">2020</year>
|
||||
<volume>17</volume>
|
||||
<pub-id pub-id-type="doi">10.1038/s41592-019-0686-2</pub-id>
|
||||
<fpage>261</fpage>
|
||||
<lpage>272</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-numpy">
|
||||
<element-citation publication-type="article-journal">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>Harris</surname><given-names>Charles R.</given-names></name>
|
||||
<name><surname>Millman</surname><given-names>K. Jarrod</given-names></name>
|
||||
<name><surname>Walt</surname><given-names>Stéfan J. van der</given-names></name>
|
||||
<name><surname>Gommers</surname><given-names>Ralf</given-names></name>
|
||||
<name><surname>Virtanen</surname><given-names>Pauli</given-names></name>
|
||||
<name><surname>Cournapeau</surname><given-names>David</given-names></name>
|
||||
<name><surname>Wieser</surname><given-names>Eric</given-names></name>
|
||||
<name><surname>Taylor</surname><given-names>Julian</given-names></name>
|
||||
<name><surname>Berg</surname><given-names>Sebastian</given-names></name>
|
||||
<name><surname>Smith</surname><given-names>Nathaniel J.</given-names></name>
|
||||
<name><surname>Kern</surname><given-names>Robert</given-names></name>
|
||||
<name><surname>Picus</surname><given-names>Matti</given-names></name>
|
||||
<name><surname>Hoyer</surname><given-names>Stephan</given-names></name>
|
||||
<name><surname>Kerkwijk</surname><given-names>Marten H. van</given-names></name>
|
||||
<name><surname>Brett</surname><given-names>Matthew</given-names></name>
|
||||
<name><surname>Haldane</surname><given-names>Allan</given-names></name>
|
||||
<name><surname>Río</surname><given-names>Jaime Fernández del</given-names></name>
|
||||
<name><surname>Wiebe</surname><given-names>Mark</given-names></name>
|
||||
<name><surname>Peterson</surname><given-names>Pearu</given-names></name>
|
||||
<name><surname>Gérard-Marchant</surname><given-names>Pierre</given-names></name>
|
||||
<name><surname>Sheppard</surname><given-names>Kevin</given-names></name>
|
||||
<name><surname>Reddy</surname><given-names>Tyler</given-names></name>
|
||||
<name><surname>Weckesser</surname><given-names>Warren</given-names></name>
|
||||
<name><surname>Abbasi</surname><given-names>Hameer</given-names></name>
|
||||
<name><surname>Gohlke</surname><given-names>Christoph</given-names></name>
|
||||
<name><surname>Oliphant</surname><given-names>Travis E.</given-names></name>
|
||||
</person-group>
|
||||
<article-title>Array programming with NumPy</article-title>
|
||||
<source>Nature</source>
|
||||
<publisher-name>Springer Science; Business Media LLC</publisher-name>
|
||||
<year iso-8601-date="2020-09">2020</year><month>09</month>
|
||||
<volume>585</volume>
|
||||
<issue>7825</issue>
|
||||
<uri>https://doi.org/10.1038/s41586-020-2649-2</uri>
|
||||
<pub-id pub-id-type="doi">10.1038/s41586-020-2649-2</pub-id>
|
||||
<fpage>357</fpage>
|
||||
<lpage>362</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-pandas">
|
||||
<element-citation publication-type="paper-conference">
|
||||
<person-group person-group-type="author">
|
||||
<name><surname>McKinney</surname><given-names>Wes</given-names></name>
|
||||
<name><surname>others</surname></name>
|
||||
</person-group>
|
||||
<article-title>Data structures for statistical computing in python</article-title>
|
||||
<source>Proceedings of the 9th python in science conference</source>
|
||||
<publisher-name>Austin, TX</publisher-name>
|
||||
<year iso-8601-date="2010">2010</year>
|
||||
<volume>445</volume>
|
||||
<fpage>51</fpage>
|
||||
<lpage>56</lpage>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-finrl">
|
||||
<element-citation publication-type="webpage">
|
||||
<article-title>AI4Finance-foundation</article-title>
|
||||
<year iso-8601-date="2022">2022</year>
|
||||
<date-in-citation content-type="access-date"><year iso-8601-date="2022-09-30">2022</year><month>09</month><day>30</day></date-in-citation>
|
||||
<uri>https://github.com/AI4Finance-Foundation/FinRL</uri>
|
||||
</element-citation>
|
||||
</ref>
|
||||
<ref id="ref-tensortrade">
|
||||
<element-citation publication-type="webpage">
|
||||
<article-title>Tensortrade</article-title>
|
||||
<year iso-8601-date="2022">2022</year>
|
||||
<date-in-citation content-type="access-date"><year iso-8601-date="2022-09-30">2022</year><month>09</month><day>30</day></date-in-citation>
|
||||
<uri>https://tensortradex.readthedocs.io/en/latest/L</uri>
|
||||
</element-citation>
|
||||
</ref>
|
||||
</ref-list>
|
||||
</back>
|
||||
</article>
|
212
docs/JOSS_paper/paper.md
Normal file
212
docs/JOSS_paper/paper.md
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: '`FreqAI`: generalizing adaptive modeling for chaotic time-series market forecasts'
|
||||
tags:
|
||||
- Python
|
||||
- Machine Learning
|
||||
- adaptive modeling
|
||||
- chaotic systems
|
||||
- time-series forecasting
|
||||
authors:
|
||||
- name: Robert A. Caulk
|
||||
orcid: 0000-0001-5618-8629
|
||||
affiliation: 1, 2
|
||||
- name: Elin Törnquist
|
||||
orcid: 0000-0003-3289-8604
|
||||
affiliation: 1, 2
|
||||
- name: Matthias Voppichler
|
||||
orcid:
|
||||
affiliation: 2
|
||||
- name: Andrew R. Lawless
|
||||
orcid:
|
||||
affiliation: 2
|
||||
- name: Ryan McMullan
|
||||
orcid:
|
||||
affiliation: 2
|
||||
- name: Wagner Costa Santos
|
||||
orcid:
|
||||
affiliation: 1, 2
|
||||
- name: Timothy C. Pogue
|
||||
orcid:
|
||||
affiliation: 1, 2
|
||||
- name: Johan van der Vlugt
|
||||
orcid:
|
||||
affiliation: 2
|
||||
- name: Stefan P. Gehring
|
||||
orcid:
|
||||
affiliation: 2
|
||||
- name: Pascal Schmidt
|
||||
orcid: 0000-0001-9328-4345
|
||||
affiliation: 2
|
||||
|
||||
<!-- affiliation: "1, 2" # (Multiple affiliations must be quoted) -->
|
||||
affiliations:
|
||||
- name: Emergent Methods LLC, Arvada Colorado, 80005, USA
|
||||
index: 1
|
||||
- name: Freqtrade open source project
|
||||
index: 2
|
||||
date: October 2022
|
||||
bibliography: paper.bib
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
# Statement of need
|
||||
|
||||
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`), has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citizen scientists" to use their basic Python skills for data-exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data-collection, storage, and handling presents a disparate challenge. [`FreqAI`](https://www.freqtrade.io/en/latest/freqai/) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
[`FreqAI`](https://www.freqtrade.io/en/latest/freqai/) evolved from a desire to test and compare a range of adaptive time-series forecasting methods on chaotic data. Cryptocurrency markets provide a unique data source since they are operational 24/7 and the data is freely available via a variety of open-sourced [exchange APIs](https://docs.ccxt.com/en/latest/manual.html#exchange-structure). Luckily, an existing open-source software, [`Freqtrade`](https://www.freqtrade.io/en/stable/), had already matured under a range of talented developers to support robust data collection/storage, as well as robust live environmental interactions for standard algorithmic trading. `Freqtrade` also provides a set of data analysis/visualization tools for the evaluation of historical performance as well as live environmental feedback. `FreqAI` builds on top of `Freqtrade` to include a user-friendly well tested interface for integrating external machine learning libraries for adaptive time-series forecasting. Beyond enabling the integration of existing libraries, `FreqAI` hosts a range of custom algorithms and methodologies aimed at improving computational and predictive performances. Thus, `FreqAI` contains a range of unique features which can be easily tested in combination with all the existing Python-accessible machine learning libraries to generate novel research on live and historical data.
|
||||
|
||||
The high-level overview of the software is depicted in Figure 1.
|
||||
|
||||

|
||||
*Abstracted overview of FreqAI algorithm*
|
||||
|
||||
## Connecting machine learning libraries
|
||||
|
||||
Although the `FreqAI` framework is designed to accommodate any Python library in the "Model training" and "Feature set engineering" portions of the software (Figure 1), it already boasts a wide range of well documented examples based on various combinations of:
|
||||
|
||||
* scikit-learn [@scikit-learn], Catboost [@catboost], LightGBM [@lightgbm], XGBoost [@xgboost], stable_baselines3 [@stable-baselines3], openai gym [@openai], tensorflow [@tensorflow], pytorch [@pytorch], Scipy [@scipy], Numpy [@numpy], and pandas [@pandas].
|
||||
|
||||
These mature projects contain a wide range of peer-reviewed and industry standard methods, including:
|
||||
|
||||
* Regression, Classification, Neural Networks, Reinforcement Learning, Support Vector Machines, Principal Component Analysis, point clustering, and much more.
|
||||
|
||||
which are all leveraged in `FreqAI` for users to use as templates or extend with their own methods.
|
||||
|
||||
## Furnishing novel methods and features
|
||||
|
||||
Beyond the industry standard methods available through external libraries - `FreqAI` includes novel methods which are not available anywhere else in the open-source (or scientific) world. For example, `FreqAI` provides :
|
||||
|
||||
* a custom algorithm/methodology for adaptive modeling details [here](https://www.freqtrade.io/en/stable/freqai/#general-approach) and [here](https://www.freqtrade.io/en/stable/freqai-developers/#project-architecture)
|
||||
* rapid and self-monitored feature engineering tools, details [here](https://www.freqtrade.io/en/stable/freqai-feature-engineering/#feature-engineering)
|
||||
* unique model features/indicators, such as the [inlier metric](https://www.freqtrade.io/en/stable/freqai-feature-engineering/#inlier-metric)
|
||||
* optimized data collection/storage algorithms, all code shown [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/freqai/data_drawer.py)
|
||||
* safely integrated outlier detection methods, details [here](https://www.freqtrade.io/en/stable/freqai-feature-engineering/#outlier-detection)
|
||||
* websocket communicated forecasts, details [here](https://www.freqtrade.io/en/stable/producer-consumer/)
|
||||
|
||||
Of particular interest for researchers, `FreqAI` provides the option of large scale experimentation via an optimized [websocket communications interface](https://www.freqtrade.io/en/stable/producer-consumer/).
|
||||
|
||||
## Optimizing the back-end
|
||||
|
||||
`FreqAI` aims to make it simple for users to combine all the above tools to run studies based in two distinct modules:
|
||||
|
||||
* backtesting studies
|
||||
* live-deployments
|
||||
|
||||
Both of these modules and their respective data management systems are built on top of [`Freqtrade`](https://www.freqtrade.io/en/latest/), a mature and actively developed cryptocurrency trading software. This means that `FreqAI` benefits from a wide range of tangential/disparate feature developments such as:
|
||||
|
||||
* FreqUI, a graphical interface for backtesting and live monitoring
|
||||
* telegram control
|
||||
* robust database handling
|
||||
* futures/leverage trading
|
||||
* dollar cost averaging
|
||||
* trading strategy handling
|
||||
* a variety of free data sources via [CCXT](https://docs.ccxt.com/en/latest/manual.html#exchange-structure) (FTX, Binance, Kucoin etc.)
|
||||
|
||||
These features derive from a strong external developer community that shares in the benefit and stability of a communal CI (Continuous Integration) system. Beyond the developer community, `FreqAI` benefits strongly from the userbase of `Freqtrade`, where most `FreqAI` beta-testers/developers originated. This symbiotic relationship between `Freqtrade` and `FreqAI` ignited a thoroughly tested [`beta`](https://github.com/freqtrade/freqtrade/pull/6832), which demanded a four month beta and [comprehensive documentation](https://www.freqtrade.io/en/latest/freqai/) containing:
|
||||
|
||||
* numerous example scripts
|
||||
* a full parameter table
|
||||
* methodological descriptions
|
||||
* high-resolution diagrams/figures
|
||||
* detailed parameter setting recommendations
|
||||
|
||||
## Providing a reproducible foundation for researchers
|
||||
|
||||
`FreqAI` provides an extensible, robust, framework for researchers and citizen data scientists. The `FreqAI` sandbox enables rapid conception and testing of exotic hypotheses. From a research perspective, `FreqAI` handles the multitude of logistics associated with live deployments, historical backtesting, and feature engineering. With `FreqAI`, researchers can focus on their primary interests of feature engineering and hypothesis testing rather than figuring out how to collect and handle data. Further - the well maintained and easily installed open-source framework of `FreqAI` enables reproducible scientific studies. This reproducibility component is essential to general scientific advancement in time-series forecasting for chaotic systems.
|
||||
|
||||
# Technical details
|
||||
|
||||
Typical users configure `FreqAI` via two files:
|
||||
|
||||
1. A `configuration` file (`--config`) which provides access to the full parameter list available [here](https://www.freqtrade.io/en/latest/freqai/):
|
||||
* control high-level feature engineering
|
||||
* customize adaptive modeling techniques
|
||||
* set any model training parameters available in third-party libraries
|
||||
* manage adaptive modeling parameters (retrain frequency, training window size, continual learning, etc.)
|
||||
|
||||
2. A strategy file (`--strategy`) where users:
|
||||
* list of the base training features
|
||||
* set standard technical-analysis strategies
|
||||
* control trade entry/exit criteria
|
||||
|
||||
With these two files, most users can exploit a wide range of pre-existing integrations in `Catboost` and 7 other libraries with a simple command:
|
||||
|
||||
```
|
||||
freqtrade trade --config config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel CatboostRegressor
|
||||
```
|
||||
|
||||
Advanced users will edit one of the existing `--freqaimodel` files, which are simply an children of the `IFreqaiModel` (details below). Within these files, advanced users can customize training procedures, prediction procedures, outlier detection methods, data preparation, data saving methods, etc. This is all configured in a way where they can customize as little or as much as they want. This flexible customization is owed to the foundational architecture in `FreqAI`, which is comprised of three distinct Python objects:
|
||||
|
||||
* `IFreqaiModel`
|
||||
* A singular long-lived object containing all the necessary logic to collect data, store data, process data, engineer features, run training, and inference models.
|
||||
* `FreqaiDataKitchen`
|
||||
* A short-lived object which is uniquely created for each asset/model. Beyond metadata, it also contains a variety of data processing tools.
|
||||
* `FreqaiDataDrawer`
|
||||
* Singular long-lived object containing all the historical predictions, models, and save/load methods.
|
||||
|
||||
These objects interact with one another with one goal in mind - to provide a clean data set to machine learning experts/enthusiasts at the user endpoint. These power-users interact with an inherited `IFreqaiModel` that allows them to dig as deep or as shallow as they wish into the inheritence tree. Typical power-users focus their efforts on customizing training procedures and testing exotic functionalities available in third-party libraries. Thus, power-users are freed from the algorithmic weight associated with data management, and can instead focus their energy on testing creative hypotheses. Meanwhile, some users choose to override deeper functionalities within `IFreqaiModel` to help them craft unique data structures and training procedures.
|
||||
|
||||
The class structure and algorithmic details are depicted in the following diagram:
|
||||
|
||||

|
||||
*Class diagram summarizing object interactions in FreqAI*
|
||||
|
||||
# Online documentation
|
||||
|
||||
The documentation for [`FreqAI`](https://www.freqtrade.io/en/latest/freqai/) is available online at [https://www.freqtrade.io/en/latest/freqai/](https://www.freqtrade.io/en/latest/freqai/) and covers a wide range of materials:
|
||||
|
||||
* Quick-start with a single command and example files - (beginners)
|
||||
* Introduction to the feature engineering interface and basic configurations - (intermediate users)
|
||||
* Parameter table with indepth descriptions and default parameter setting recommendations - (intermediate users)
|
||||
* Data analysis and post-processing - (advanced users)
|
||||
* Methodological considerations complemented by high resolution figures - (advanced users)
|
||||
* Instructions for integrating third party machine learning libraries into custom prediction models - (advanced users)
|
||||
* Software architectural description with class diagram - (developers)
|
||||
* File structure descriptions - (developers)
|
||||
|
||||
The docs direct users to a variety of pre-made examples which integrate `Catboost`, `LightGBM`, `XGBoost`, `Sklearn`, `stable_baselines3`, `torch`, `tensorflow`. Meanwhile, developers will also find thorough docstrings and type hinting throughout the source code to aid in code readability and customization.
|
||||
|
||||
`FreqAI` also benefits from a strong support network of users and developers on the [`Freqtrade` discord](https://discord.gg/w6nDM6cM4y) as well as on the [`FreqAI` discord](https://discord.gg/xE4RMg4QYw). Within the `FreqAI` discord, users will find a deep and easily searched knowledge base containing common errors. But more importantly, users in the `FreqAI` discord share anectdotal and quantitative observations which compare performance between various third-party libraries and methods.
|
||||
|
||||
# State of the field
|
||||
|
||||
There are two other open-source tools which are geared toward helping users build models for time-series forecasts on market based data. However, each of these tools suffer from a non-generalized frameworks that do not permit comparison of methods and libraries. Additionally, they do not permit easy live-deployments or adaptive-modeling methods. For example, two open-sourced projects called [`tensortrade`](https://tensortradex.readthedocs.io/en/latest/) [@tensortrade] and [`FinRL`](https://github.com/AI4Finance-Foundation/FinRL) [@finrl] limit users to the exploration of reinforcement learning on historical data. These softwares also do not provide robust live deployments, they do not furnish novel feature engineering algorithms, and they do not provide custom data analysis tools. `FreqAI` fills the gap.
|
||||
|
||||
# On-going research
|
||||
|
||||
Emergent Methods, based in Arvada CO, is actively using `FreqAI` to perform large scale experiments aimed at comparing machine learning libraries in live and historical environments. Past projects include backtesting parametric sweeps, while active projects include a 3 week live deployment comparison between `CatboostRegressor`, `LightGBMRegressor`, and `XGBoostRegressor`. Results from these studies are planned for submission to scientific journals as well as more general data science blogs (e.g. Medium).
|
||||
|
||||
# Installing and running `FreqAI`
|
||||
|
||||
`FreqAI` is automatically installed with `Freqtrade` using the following commands on linux systems:
|
||||
|
||||
```
|
||||
git clone git@github.com:freqtrade/freqtrade.git
|
||||
cd freqtrade
|
||||
./setup.sh -i
|
||||
```
|
||||
|
||||
However, `FreqAI` also benefits from `Freqtrade` docker distributions, and can be run with docker by pulling the stable or develop images from `Freqtrade` distributions.
|
||||
|
||||
# Funding sources
|
||||
|
||||
[`FreqAI`](https://www.freqtrade.io/en/latest/freqai/) has had no official sponsors, and is entirely grass roots. All donations into the project (e.g. the GitHub sponsor system) are kept inside the project to help support development of open-sourced and communally beneficial features.
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
We would like to acknowledge various beta testers of `FreqAI`:
|
||||
|
||||
- Longlong Yu (lolongcovas)
|
||||
- Richárd Józsa (richardjozsa)
|
||||
- Juha Nykänen (suikula)
|
||||
- Emre Suzen (aemr3)
|
||||
- Salah Lamkadem (ikonx)
|
||||
|
||||
As well as various `Freqtrade` [developers](https://github.com/freqtrade/freqtrade/graphs/contributors) maintaining tangential, yet essential, modules.
|
||||
|
||||
# References
|
BIN
docs/JOSS_paper/paper.pdf
Normal file
BIN
docs/JOSS_paper/paper.pdf
Normal file
Binary file not shown.
BIN
docs/assets/tensorboard.jpg
Normal file
BIN
docs/assets/tensorboard.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 362 KiB |
@@ -46,6 +46,16 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `n_estimators` | The number of boosted trees to fit in the training of the model. <br> **Datatype:** Integer.
|
||||
| `learning_rate` | Boosting learning rate during training of the model. <br> **Datatype:** Float.
|
||||
| `n_jobs`, `thread_count`, `task_type` | Set the number of threads for parallel processing and the `task_type` (`gpu` or `cpu`). Different model libraries use different parameter names. <br> **Datatype:** Float.
|
||||
| | *Reinforcement Learning Parameters**
|
||||
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
||||
| `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer.
|
||||
| `cpu_count` | Number of processors to dedicate to the Reinforcement Learning training process. <br> **Datatype:** int.
|
||||
| `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the user customizable `calculate_reward()` <br> **Datatype:** int.
|
||||
| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string.
|
||||
| `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string.
|
||||
| `max_training_drawdown_pct` | The maximum drawdown that the agent is allowed to experience during training. <br> **Datatype:** float. <br> Default: 0.8
|
||||
| `cpu_count` | Number of threads/cpus to dedicate to the Reinforcement Learning training process (depending on if `ReinforcementLearning_multiproc` is selected or not). <br> **Datatype:** int.
|
||||
| `model_reward_parameters` | Parameters used inside the user customizable `calculate_reward()` function in `ReinforcementLearner.py` <br> **Datatype:** int.
|
||||
| | **Extraneous parameters**
|
||||
| `keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||
| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
202
docs/freqai-reinforcement-learning.md
Normal file
202
docs/freqai-reinforcement-learning.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# Reinforcement Learning
|
||||
|
||||
!!! Note
|
||||
Reinforcement learning dependencies include large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]?" Users who prefer docker should ensure they use the docker image appended with `_freqaiRL`.
|
||||
|
||||
Setting up and running a Reinforcement Learning model is the same as running a Regressor or Classifier. The same two flags, `--freqaimodel` and `--strategy`, must be defined on the command line:
|
||||
|
||||
```bash
|
||||
freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json
|
||||
```
|
||||
|
||||
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner`. The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `populate_any_indicators` as a typical Regressor:
|
||||
|
||||
```python
|
||||
def populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
|
||||
|
||||
# The following features are necessary for RL models
|
||||
informative[f"%-{coin}raw_close"] = informative["close"]
|
||||
informative[f"%-{coin}raw_open"] = informative["open"]
|
||||
informative[f"%-{coin}raw_high"] = informative["high"]
|
||||
informative[f"%-{coin}raw_low"] = informative["low"]
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
|
||||
# Add generalized indicators here (because in live, it will call this
|
||||
# function to populate indicators during training). Notice how we ensure not to
|
||||
# add them multiple times
|
||||
if set_generalized_indicators:
|
||||
|
||||
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||
# until the agent sends an action.
|
||||
df["&-action"] = 0
|
||||
|
||||
return df
|
||||
```
|
||||
|
||||
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environent:
|
||||
|
||||
```python
|
||||
# The following features are necessary for RL models
|
||||
informative[f"%-{coin}raw_close"] = informative["close"]
|
||||
informative[f"%-{coin}raw_open"] = informative["open"]
|
||||
informative[f"%-{coin}raw_high"] = informative["high"]
|
||||
informative[f"%-{coin}raw_low"] = informative["low"]
|
||||
```
|
||||
|
||||
Finally, there is no explicit "label" to make - instead the you need to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the user set the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
||||
|
||||
After users realize there are no labels to set, they will soon understand that the agent is making its "own" entry and exit decisions. This makes strategy construction rather simple. The entry and exit signals come from the agent in the form of an integer - which are used directly to decide entries and exits in the strategy:
|
||||
|
||||
```python
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
enter_long_conditions = [df["do_predict"] == 1, df["&-action"] == 1]
|
||||
|
||||
if enter_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
|
||||
] = (1, "long")
|
||||
|
||||
enter_short_conditions = [df["do_predict"] == 1, df["&-action"] == 3]
|
||||
|
||||
if enter_short_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]
|
||||
] = (1, "short")
|
||||
|
||||
return df
|
||||
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
exit_long_conditions = [df["do_predict"] == 1, df["&-action"] == 2]
|
||||
if exit_long_conditions:
|
||||
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
|
||||
|
||||
exit_short_conditions = [df["do_predict"] == 1, df["&-action"] == 4]
|
||||
if exit_short_conditions:
|
||||
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
|
||||
|
||||
return df
|
||||
```
|
||||
|
||||
It is important to consider that `&-action` depends on which environment they choose to use. The example above shows 5 actions, where 0 is neutral, 1 is enter long, 2 is exit long, 3 is enter short and 4 is exit short.
|
||||
|
||||
## Configuring the Reinforcement Learner
|
||||
|
||||
In order to configure the `Reinforcement Learner` the following dictionary to their `freqai` config:
|
||||
|
||||
```json
|
||||
"rl_config": {
|
||||
"train_cycles": 25,
|
||||
"max_trade_duration_candles": 300,
|
||||
"max_training_drawdown_pct": 0.02,
|
||||
"cpu_count": 8,
|
||||
"model_type": "PPO",
|
||||
"policy_type": "MlpPolicy",
|
||||
"model_reward_parameters": {
|
||||
"rr": 1,
|
||||
"profit_aim": 0.025
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Parameter details can be found [here](freqai-parameter-table.md), but in general the `train_cycles` decides how many times the agent should cycle through the candle data in its artificial environemtn to train weights in the model. `model_type` is a string which selects one of the available models in [stable_baselines](https://stable-baselines3.readthedocs.io/en/master/)(external link).
|
||||
|
||||
## Creating the reward
|
||||
|
||||
As users begin to modify the strategy and the prediction model, they will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, the user sets a `calculate_reward()` function inside their custom `ReinforcementLearner.py` file. A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to give users the necessary building blocks to start their own models. It is inside the `calculate_reward()` where users express their creative theories about the market. For example, the user wants to reward their agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, the user wishes to reward the agnet for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated:
|
||||
|
||||
```python
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||
Users can override any functions from those parent classes. Here is an example
|
||||
of a user customized `calculate_reward()` function.
|
||||
"""
|
||||
def calculate_reward(self, action):
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
return -2
|
||||
pnl = self.get_unrealized_profit()
|
||||
|
||||
factor = 100
|
||||
# reward agent for entering trades
|
||||
if action in (Actions.Long_enter.value, Actions.Short_enter.value) \
|
||||
and self._position == Positions.Neutral:
|
||||
return 25
|
||||
# discourage agent from not entering trades
|
||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||
return -1
|
||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||
trade_duration = self._current_tick - self._last_trade_tick
|
||||
if trade_duration <= max_trade_duration:
|
||||
factor *= 1.5
|
||||
elif trade_duration > max_trade_duration:
|
||||
factor *= 0.5
|
||||
# discourage sitting in position
|
||||
if self._position in (Positions.Short, Positions.Long) and \
|
||||
action == Actions.Neutral.value:
|
||||
return -1 * trade_duration / max_trade_duration
|
||||
# close long
|
||||
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
# close short
|
||||
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
return 0.
|
||||
```
|
||||
|
||||
### Creating a custom agent
|
||||
|
||||
Users can inherit from `stable_baselines3` and customize anything they wish about their agent. Doing this is for advanced users only, an example is presented in `freqai/RL/ReinforcementLearnerCustomAgent.py`
|
||||
|
||||
### Using Tensorboard
|
||||
|
||||
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. To start, the user should ensure Tensorboard is installed on their computer:
|
||||
|
||||
```bash
|
||||
pip3 install tensorboard
|
||||
```
|
||||
|
||||
Next, the user can activate Tensorboard with the following command:
|
||||
|
||||
```bash
|
||||
cd freqtrade
|
||||
tensorboard --logdir user_data/models/unique-id
|
||||
```
|
||||
|
||||
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if the user wishes to view the output in their browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
||||
|
||||

|
@@ -1,71 +0,0 @@
|
||||
# Using the `spice_rack`
|
||||
|
||||
!!! Note:
|
||||
`spice_rack` indicators should not be used exclusively for entries and exits, the following example is just a demonstration of syntax. `spice_rack` indicators should **always** be used to support existing strategies.
|
||||
|
||||
The `spice_rack` is aimed at users who do not wish to deal with setting up `FreqAI` confgs, but instead prefer to interact with `FreqAI` similar to a `talib` indicator. In this case, the user can instead simply add two keys to their config:
|
||||
|
||||
```json
|
||||
"freqai_spice_rack": true,
|
||||
"freqai_identifier": "spicey-id",
|
||||
```
|
||||
|
||||
Which tells `FreqAI` to set up a pre-set `FreqAI` instance automatically under the hood with preset parameters. Now the user can access a suite of custom `FreqAI` supercharged indicators inside their strategy by placing the following code into `populate_indicators`:
|
||||
|
||||
```python
|
||||
dataframe['dissimilarity_index'] = self.freqai.spice_rack(
|
||||
'DI_values', dataframe, metadata, self)
|
||||
dataframe['extrema'] = self.freqai.spice_rack(
|
||||
'&s-extrema', dataframe, metadata, self)
|
||||
self.freqai.close_spice_rack() # user must close the spicerack
|
||||
```
|
||||
|
||||
Users can then use these columns in concert with all their own additional indicators added to `populate_indicators` in their entry/exit criteria and strategy callback methods the same way as any typical indicator. For example:
|
||||
|
||||
```python
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
df.loc[
|
||||
(
|
||||
(df['dissimilarity_index'] < 1) &
|
||||
(df['extrema'] < -0.1)
|
||||
),
|
||||
'enter_long'] = 1
|
||||
|
||||
df.loc[
|
||||
(
|
||||
(df['dissimilarity_index'] < 1) &
|
||||
(df['extrema'] > 0.1)
|
||||
),
|
||||
'enter_short'] = 1
|
||||
|
||||
return df
|
||||
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
df.loc[
|
||||
(
|
||||
(df['dissimilarity_index'] < 1) &
|
||||
(df['extrema'] > 0.1)
|
||||
),
|
||||
|
||||
'exit_long'] = 1
|
||||
|
||||
df.loc[
|
||||
(
|
||||
|
||||
(df['dissimilarity_index'] < 1) &
|
||||
(df['extrema'] < -0.1)
|
||||
),
|
||||
'exit_short'] = 1
|
||||
|
||||
return df
|
||||
```
|
||||
|
||||
|
||||
## Available indicators
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| `DI_values` | **Required.** <br> The dissimilarity index of the current candle to the recent candles. More information available [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) <br> **Datatype:** Floats.
|
||||
| `extrema` | **Required.** <br> A continuous prediction from FreqAI which aims to help predict if the current candle is a maxima or a minma. FreqAI aims for 1 to be a maxima and -1 to be a minima - but the values should typically hover between -0.2 and 0.2. <br> **Datatype:** Floats.
|
@@ -11,8 +11,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
|
||||
refresh_backtest_trades_data)
|
||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange, market_is_active, timeframe_to_minutes
|
||||
from freqtrade.freqai.utils import setup_freqai_spice_rack
|
||||
from freqtrade.exchange import market_is_active, timeframe_to_minutes
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
|
||||
@@ -49,10 +48,6 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
|
||||
if config.get('freqai_spice_rack', False):
|
||||
config = setup_freqai_spice_rack(config, exchange)
|
||||
|
||||
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
||||
or config.get('include_inactive')]
|
||||
|
||||
@@ -68,19 +63,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
exchange.validate_timeframes(timeframe)
|
||||
|
||||
try:
|
||||
pairs_not_available = download_trades(exchange, expanded_pairs, config, timerange)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("SIGINT received, aborting ...")
|
||||
|
||||
finally:
|
||||
if pairs_not_available:
|
||||
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||
f"on exchange {exchange.name}.")
|
||||
|
||||
|
||||
def download_trades(exchange: Exchange, expanded_pairs: list,
|
||||
config: Dict[str, Any], timerange: TimeRange) -> list:
|
||||
if config.get('download_trades'):
|
||||
if config.get('trading_mode') == 'futures':
|
||||
raise OperationalException("Trade download not supported for futures.")
|
||||
@@ -112,7 +95,13 @@ def download_trades(exchange: Exchange, expanded_pairs: list,
|
||||
prepend=config.get('prepend_data', False)
|
||||
)
|
||||
|
||||
return pairs_not_available
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("SIGINT received, aborting ...")
|
||||
|
||||
finally:
|
||||
if pairs_not_available:
|
||||
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||
f"on exchange {exchange.name}.")
|
||||
|
||||
|
||||
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
|
@@ -571,10 +571,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"n_estimators": {"type": "integer", "default": 1000}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
|
@@ -18,12 +18,12 @@ import ccxt.async_support as ccxt_async
|
||||
from cachetools import TTLCache
|
||||
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
||||
from dateutil import parser
|
||||
from pandas import DataFrame, concat
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
|
||||
Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
|
||||
PairWithTimeframe)
|
||||
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException, PricingError,
|
||||
@@ -184,9 +184,8 @@ class Exchange:
|
||||
# Initial markets load
|
||||
self._load_markets()
|
||||
self.validate_config(config)
|
||||
self._startup_candle_count: int = config.get('startup_candle_count', 0)
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
self._startup_candle_count, config.get('timeframe', ''))
|
||||
config.get('startup_candle_count', 0), config.get('timeframe', ''))
|
||||
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = exchange_config.get(
|
||||
@@ -1851,22 +1850,10 @@ class Exchange:
|
||||
return pair, timeframe, candle_type, data
|
||||
|
||||
def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType,
|
||||
since_ms: Optional[int], cache: bool) -> Coroutine:
|
||||
not_all_data = self.required_candle_call_count > 1
|
||||
if cache and (pair, timeframe, candle_type) in self._klines:
|
||||
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
|
||||
min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp()
|
||||
# Check if 1 call can get us updated candles without hole in the data.
|
||||
if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
|
||||
# Cache can be used - do one-off call.
|
||||
not_all_data = False
|
||||
else:
|
||||
# Time jump detected, evict cache
|
||||
logger.info(
|
||||
f"Time jump detected. Evicting cache for {pair}, {timeframe}, {candle_type}")
|
||||
del self._klines[(pair, timeframe, candle_type)]
|
||||
since_ms: Optional[int]) -> Coroutine:
|
||||
|
||||
if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)):
|
||||
if (not since_ms
|
||||
and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)):
|
||||
# Multiple calls for one pair - to get more history
|
||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
|
||||
timeframe, candle_type, since_ms)
|
||||
@@ -1891,8 +1878,10 @@ class Exchange:
|
||||
input_coroutines = []
|
||||
cached_pairs = []
|
||||
for pair, timeframe, candle_type in set(pair_list):
|
||||
if (timeframe not in self.timeframes
|
||||
and candle_type in (CandleType.SPOT, CandleType.FUTURES)):
|
||||
if (
|
||||
timeframe not in self.timeframes
|
||||
and candle_type in (CandleType.SPOT, CandleType.FUTURES)
|
||||
):
|
||||
logger.warning(
|
||||
f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
|
||||
f"not available on {self.name}. Available timeframes are "
|
||||
@@ -1901,9 +1890,8 @@ class Exchange:
|
||||
|
||||
if ((pair, timeframe, candle_type) not in self._klines or not cache
|
||||
or self._now_is_time_to_refresh(pair, timeframe, candle_type)):
|
||||
|
||||
input_coroutines.append(
|
||||
self._build_coroutine(pair, timeframe, candle_type, since_ms, cache))
|
||||
input_coroutines.append(self._build_coroutine(
|
||||
pair, timeframe, candle_type=candle_type, since_ms=since_ms))
|
||||
|
||||
else:
|
||||
logger.debug(
|
||||
@@ -1913,28 +1901,6 @@ class Exchange:
|
||||
|
||||
return input_coroutines, cached_pairs
|
||||
|
||||
def _process_ohlcv_df(self, pair: str, timeframe: str, c_type: CandleType, ticks: List[List],
|
||||
cache: bool, drop_incomplete: bool) -> DataFrame:
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks and cache:
|
||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=drop_incomplete)
|
||||
if cache:
|
||||
if (pair, timeframe, c_type) in self._klines:
|
||||
old = self._klines[(pair, timeframe, c_type)]
|
||||
# Reassign so we return the updated, combined df
|
||||
ohlcv_df = clean_ohlcv_dataframe(concat([old, ohlcv_df], axis=0), timeframe, pair,
|
||||
fill_missing=True, drop_incomplete=False)
|
||||
candle_limit = self.ohlcv_candle_limit(timeframe, self._config['candle_type_def'])
|
||||
# Age out old candles
|
||||
ohlcv_df = ohlcv_df.tail(candle_limit + self._startup_candle_count)
|
||||
self._klines[(pair, timeframe, c_type)] = ohlcv_df
|
||||
else:
|
||||
self._klines[(pair, timeframe, c_type)] = ohlcv_df
|
||||
return ohlcv_df
|
||||
|
||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||
since_ms: Optional[int] = None, cache: bool = True,
|
||||
drop_incomplete: Optional[bool] = None
|
||||
@@ -1971,11 +1937,16 @@ class Exchange:
|
||||
continue
|
||||
# Deconstruct tuple (has 4 elements)
|
||||
pair, timeframe, c_type, ticks = res
|
||||
ohlcv_df = self._process_ohlcv_df(
|
||||
pair, timeframe, c_type, ticks, cache, drop_incomplete)
|
||||
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks:
|
||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=drop_incomplete)
|
||||
results_df[(pair, timeframe, c_type)] = ohlcv_df
|
||||
|
||||
if cache:
|
||||
self._klines[(pair, timeframe, c_type)] = ohlcv_df
|
||||
# Return cached klines
|
||||
for pair, timeframe, c_type in cached_pairs:
|
||||
results_df[(pair, timeframe, c_type)] = self.klines(
|
||||
|
134
freqtrade/freqai/RL/Base4ActionRLEnv.py
Normal file
134
freqtrade/freqai/RL/Base4ActionRLEnv.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from gym import spaces
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Actions(Enum):
|
||||
Neutral = 0
|
||||
Exit = 1
|
||||
Long_enter = 2
|
||||
Short_enter = 3
|
||||
|
||||
|
||||
class Base4ActionRLEnv(BaseEnvironment):
|
||||
"""
|
||||
Base class for a 4 action environment
|
||||
"""
|
||||
|
||||
def set_action_space(self):
|
||||
self.action_space = spaces.Discrete(len(Actions))
|
||||
|
||||
def step(self, action: int):
|
||||
"""
|
||||
Logic for a single step (incrementing one candle in time)
|
||||
by the agent
|
||||
:param: action: int = the action type that the agent plans
|
||||
to take for the current step.
|
||||
:returns:
|
||||
observation = current state of environment
|
||||
step_reward = the reward from `calculate_reward()`
|
||||
_done = if the agent "died" or if the candles finished
|
||||
info = dict passed back to openai gym lib
|
||||
"""
|
||||
self._done = False
|
||||
self._current_tick += 1
|
||||
|
||||
if self._current_tick == self._end_tick:
|
||||
self._done = True
|
||||
|
||||
self._update_unrealized_total_profit()
|
||||
|
||||
step_reward = self.calculate_reward(action)
|
||||
self.total_reward += step_reward
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
"""
|
||||
Action: Neutral, position: Long -> Close Long
|
||||
Action: Neutral, position: Short -> Close Short
|
||||
|
||||
Action: Long, position: Neutral -> Open Long
|
||||
Action: Long, position: Short -> Close Short and Open Long
|
||||
|
||||
Action: Short, position: Neutral -> Open Short
|
||||
Action: Short, position: Long -> Close Long and Open Short
|
||||
"""
|
||||
|
||||
if action == Actions.Neutral.value:
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Long_enter.value:
|
||||
self._position = Positions.Long
|
||||
trade_type = "long"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Short_enter.value:
|
||||
self._position = Positions.Short
|
||||
trade_type = "short"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Exit.value:
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type})
|
||||
|
||||
if self._total_profit < 1 - self.rl_config.get('max_training_drawdown_pct', 0.8):
|
||||
self._done = True
|
||||
|
||||
self._position_history.append(self._position)
|
||||
|
||||
info = dict(
|
||||
tick=self._current_tick,
|
||||
total_reward=self.total_reward,
|
||||
total_profit=self._total_profit,
|
||||
position=self._position.value
|
||||
)
|
||||
|
||||
observation = self._get_observation()
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, info
|
||||
|
||||
def is_tradesignal(self, action: int):
|
||||
"""
|
||||
Determine if the signal is a trade signal
|
||||
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||
"""
|
||||
return not ((action == Actions.Neutral.value and self._position == Positions.Neutral) or
|
||||
(action == Actions.Neutral.value and self._position == Positions.Short) or
|
||||
(action == Actions.Neutral.value and self._position == Positions.Long) or
|
||||
(action == Actions.Short_enter.value and self._position == Positions.Short) or
|
||||
(action == Actions.Short_enter.value and self._position == Positions.Long) or
|
||||
(action == Actions.Exit.value and self._position == Positions.Neutral) or
|
||||
(action == Actions.Long_enter.value and self._position == Positions.Long) or
|
||||
(action == Actions.Long_enter.value and self._position == Positions.Short))
|
||||
|
||||
def _is_valid(self, action: int):
|
||||
"""
|
||||
Determine if the signal is valid.
|
||||
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||
"""
|
||||
# Agent should only try to exit if it is in position
|
||||
if action == Actions.Exit.value:
|
||||
if self._position not in (Positions.Short, Positions.Long):
|
||||
return False
|
||||
|
||||
# Agent should only try to enter if it is not in position
|
||||
if action in (Actions.Short_enter.value, Actions.Long_enter.value):
|
||||
if self._position != Positions.Neutral:
|
||||
return False
|
||||
|
||||
return True
|
201
freqtrade/freqai/RL/Base5ActionRLEnv.py
Normal file
201
freqtrade/freqai/RL/Base5ActionRLEnv.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from gym import spaces
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Actions(Enum):
|
||||
Neutral = 0
|
||||
Long_enter = 1
|
||||
Long_exit = 2
|
||||
Short_enter = 3
|
||||
Short_exit = 4
|
||||
|
||||
|
||||
def mean_over_std(x):
|
||||
std = np.std(x, ddof=1)
|
||||
mean = np.mean(x)
|
||||
return mean / std if std > 0 else 0
|
||||
|
||||
|
||||
class Base5ActionRLEnv(BaseEnvironment):
|
||||
"""
|
||||
Base class for a 5 action environment
|
||||
"""
|
||||
|
||||
def set_action_space(self):
|
||||
self.action_space = spaces.Discrete(len(Actions))
|
||||
|
||||
def reset(self):
|
||||
|
||||
self._done = False
|
||||
|
||||
if self.starting_point is True:
|
||||
self._position_history = (self._start_tick * [None]) + [self._position]
|
||||
else:
|
||||
self._position_history = (self.window_size * [None]) + [self._position]
|
||||
|
||||
self._current_tick = self._start_tick
|
||||
self._last_trade_tick = None
|
||||
self._position = Positions.Neutral
|
||||
|
||||
self.total_reward = 0.
|
||||
self._total_profit = 1. # unit
|
||||
self.history = {}
|
||||
self.trade_history = []
|
||||
self.portfolio_log_returns = np.zeros(len(self.prices))
|
||||
|
||||
self._profits = [(self._start_tick, 1)]
|
||||
self.close_trade_profit = []
|
||||
self._total_unrealized_profit = 1
|
||||
|
||||
return self._get_observation()
|
||||
|
||||
def step(self, action: int):
|
||||
"""
|
||||
Logic for a single step (incrementing one candle in time)
|
||||
by the agent
|
||||
:param: action: int = the action type that the agent plans
|
||||
to take for the current step.
|
||||
:returns:
|
||||
observation = current state of environment
|
||||
step_reward = the reward from `calculate_reward()`
|
||||
_done = if the agent "died" or if the candles finished
|
||||
info = dict passed back to openai gym lib
|
||||
"""
|
||||
self._done = False
|
||||
self._current_tick += 1
|
||||
|
||||
if self._current_tick == self._end_tick:
|
||||
self._done = True
|
||||
|
||||
self.update_portfolio_log_returns(action)
|
||||
|
||||
self._update_unrealized_total_profit()
|
||||
step_reward = self.calculate_reward(action)
|
||||
self.total_reward += step_reward
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
"""
|
||||
Action: Neutral, position: Long -> Close Long
|
||||
Action: Neutral, position: Short -> Close Short
|
||||
|
||||
Action: Long, position: Neutral -> Open Long
|
||||
Action: Long, position: Short -> Close Short and Open Long
|
||||
|
||||
Action: Short, position: Neutral -> Open Short
|
||||
Action: Short, position: Long -> Close Long and Open Short
|
||||
"""
|
||||
|
||||
if action == Actions.Neutral.value:
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Long_enter.value:
|
||||
self._position = Positions.Long
|
||||
trade_type = "long"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Short_enter.value:
|
||||
self._position = Positions.Short
|
||||
trade_type = "short"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Long_exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Short_exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type})
|
||||
|
||||
if (self._total_profit < self.max_drawdown or
|
||||
self._total_unrealized_profit < self.max_drawdown):
|
||||
self._done = True
|
||||
|
||||
self._position_history.append(self._position)
|
||||
|
||||
info = dict(
|
||||
tick=self._current_tick,
|
||||
total_reward=self.total_reward,
|
||||
total_profit=self._total_profit,
|
||||
position=self._position.value
|
||||
)
|
||||
|
||||
observation = self._get_observation()
|
||||
|
||||
self._update_history(info)
|
||||
|
||||
return observation, step_reward, self._done, info
|
||||
|
||||
def _get_observation(self):
|
||||
features_window = self.signal_features[(
|
||||
self._current_tick - self.window_size):self._current_tick]
|
||||
features_and_state = DataFrame(np.zeros((len(features_window), 3)),
|
||||
columns=['current_profit_pct', 'position', 'trade_duration'],
|
||||
index=features_window.index)
|
||||
|
||||
features_and_state['current_profit_pct'] = self.get_unrealized_profit()
|
||||
features_and_state['position'] = self._position.value
|
||||
features_and_state['trade_duration'] = self.get_trade_duration()
|
||||
features_and_state = pd.concat([features_window, features_and_state], axis=1)
|
||||
return features_and_state
|
||||
|
||||
def get_trade_duration(self):
|
||||
if self._last_trade_tick is None:
|
||||
return 0
|
||||
else:
|
||||
return self._current_tick - self._last_trade_tick
|
||||
|
||||
def is_tradesignal(self, action: int):
|
||||
# trade signal
|
||||
"""
|
||||
Determine if the signal is a trade signal
|
||||
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||
"""
|
||||
return not ((action == Actions.Neutral.value and self._position == Positions.Neutral) or
|
||||
(action == Actions.Neutral.value and self._position == Positions.Short) or
|
||||
(action == Actions.Neutral.value and self._position == Positions.Long) or
|
||||
(action == Actions.Short_enter.value and self._position == Positions.Short) or
|
||||
(action == Actions.Short_enter.value and self._position == Positions.Long) or
|
||||
(action == Actions.Short_exit.value and self._position == Positions.Long) or
|
||||
(action == Actions.Short_exit.value and self._position == Positions.Neutral) or
|
||||
(action == Actions.Long_enter.value and self._position == Positions.Long) or
|
||||
(action == Actions.Long_enter.value and self._position == Positions.Short) or
|
||||
(action == Actions.Long_exit.value and self._position == Positions.Short) or
|
||||
(action == Actions.Long_exit.value and self._position == Positions.Neutral))
|
||||
|
||||
def _is_valid(self, action: int):
|
||||
# trade signal
|
||||
"""
|
||||
Determine if the signal is valid.
|
||||
e.g.: agent wants a Actions.Long_exit while it is in a Positions.short
|
||||
"""
|
||||
# Agent should only try to exit if it is in position
|
||||
if action in (Actions.Short_exit.value, Actions.Long_exit.value):
|
||||
if self._position not in (Positions.Short, Positions.Long):
|
||||
return False
|
||||
|
||||
# Agent should only try to enter if it is not in position
|
||||
if action in (Actions.Short_enter.value, Actions.Long_enter.value):
|
||||
if self._position != Positions.Neutral:
|
||||
return False
|
||||
|
||||
return True
|
267
freqtrade/freqai/RL/BaseEnvironment.py
Normal file
267
freqtrade/freqai/RL/BaseEnvironment.py
Normal file
@@ -0,0 +1,267 @@
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
import gym
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from gym import spaces
|
||||
from gym.utils import seeding
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Positions(Enum):
|
||||
Short = 0
|
||||
Long = 1
|
||||
Neutral = 0.5
|
||||
|
||||
def opposite(self):
|
||||
return Positions.Short if self == Positions.Long else Positions.Long
|
||||
|
||||
|
||||
class BaseEnvironment(gym.Env):
|
||||
"""
|
||||
Base class for environments. This class is agnostic to action count.
|
||||
Inherited classes customize this to include varying action counts/types,
|
||||
See RL/Base5ActionRLEnv.py and RL/Base4ActionRLEnv.py
|
||||
"""
|
||||
|
||||
def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(),
|
||||
reward_kwargs: dict = {}, window_size=10, starting_point=True,
|
||||
id: str = 'baseenv-1', seed: int = 1, config: dict = {}):
|
||||
|
||||
self.rl_config = config['freqai']['rl_config']
|
||||
self.id = id
|
||||
self.seed(seed)
|
||||
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
|
||||
self.max_drawdown = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8)
|
||||
self.compound_trades = config['stake_amount'] == 'unlimited'
|
||||
|
||||
def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int,
|
||||
reward_kwargs: dict, starting_point=True):
|
||||
"""
|
||||
Resets the environment when the agent fails (in our case, if the drawdown
|
||||
exceeds the user set max_training_drawdown_pct)
|
||||
"""
|
||||
self.df = df
|
||||
self.signal_features = self.df
|
||||
self.prices = prices
|
||||
self.window_size = window_size
|
||||
self.starting_point = starting_point
|
||||
self.rr = reward_kwargs["rr"]
|
||||
self.profit_aim = reward_kwargs["profit_aim"]
|
||||
|
||||
self.fee = 0.0015
|
||||
|
||||
# # spaces
|
||||
self.shape = (window_size, self.signal_features.shape[1] + 3)
|
||||
self.set_action_space()
|
||||
self.observation_space = spaces.Box(
|
||||
low=-1, high=1, shape=self.shape, dtype=np.float32)
|
||||
|
||||
# episode
|
||||
self._start_tick: int = self.window_size
|
||||
self._end_tick: int = len(self.prices) - 1
|
||||
self._done: bool = False
|
||||
self._current_tick: int = self._start_tick
|
||||
self._last_trade_tick: Optional[int] = None
|
||||
self._position = Positions.Neutral
|
||||
self._position_history: list = [None]
|
||||
self.total_reward: float = 0
|
||||
self._total_profit: float = 1
|
||||
self._total_unrealized_profit: float = 1
|
||||
self.history: dict = {}
|
||||
self.trade_history: list = []
|
||||
|
||||
@abstractmethod
|
||||
def set_action_space(self):
|
||||
"""
|
||||
Unique to the environment action count. Must be inherited.
|
||||
"""
|
||||
|
||||
def seed(self, seed: int = 1):
|
||||
self.np_random, seed = seeding.np_random(seed)
|
||||
return [seed]
|
||||
|
||||
def reset(self):
|
||||
|
||||
self._done = False
|
||||
|
||||
if self.starting_point is True:
|
||||
self._position_history = (self._start_tick * [None]) + [self._position]
|
||||
else:
|
||||
self._position_history = (self.window_size * [None]) + [self._position]
|
||||
|
||||
self._current_tick = self._start_tick
|
||||
self._last_trade_tick = None
|
||||
self._position = Positions.Neutral
|
||||
|
||||
self.total_reward = 0.
|
||||
self._total_profit = 1. # unit
|
||||
self.history = {}
|
||||
self.trade_history = []
|
||||
self.portfolio_log_returns = np.zeros(len(self.prices))
|
||||
|
||||
self._profits = [(self._start_tick, 1)]
|
||||
self.close_trade_profit = []
|
||||
self._total_unrealized_profit = 1
|
||||
|
||||
return self._get_observation()
|
||||
|
||||
@abstractmethod
|
||||
def step(self, action: int):
|
||||
"""
|
||||
Step depeneds on action types, this must be inherited.
|
||||
"""
|
||||
return
|
||||
|
||||
def _get_observation(self):
|
||||
"""
|
||||
This may or may not be independent of action types, user can inherit
|
||||
this in their custom "MyRLEnv"
|
||||
"""
|
||||
features_window = self.signal_features[(
|
||||
self._current_tick - self.window_size):self._current_tick]
|
||||
features_and_state = DataFrame(np.zeros((len(features_window), 3)),
|
||||
columns=['current_profit_pct', 'position', 'trade_duration'],
|
||||
index=features_window.index)
|
||||
|
||||
features_and_state['current_profit_pct'] = self.get_unrealized_profit()
|
||||
features_and_state['position'] = self._position.value
|
||||
features_and_state['trade_duration'] = self.get_trade_duration()
|
||||
features_and_state = pd.concat([features_window, features_and_state], axis=1)
|
||||
return features_and_state
|
||||
|
||||
def get_trade_duration(self):
|
||||
"""
|
||||
Get the trade duration if the agent is in a trade
|
||||
"""
|
||||
if self._last_trade_tick is None:
|
||||
return 0
|
||||
else:
|
||||
return self._current_tick - self._last_trade_tick
|
||||
|
||||
def get_unrealized_profit(self):
|
||||
"""
|
||||
Get the unrealized profit if the agent is in a trade
|
||||
"""
|
||||
if self._last_trade_tick is None:
|
||||
return 0.
|
||||
|
||||
if self._position == Positions.Neutral:
|
||||
return 0.
|
||||
elif self._position == Positions.Short:
|
||||
current_price = self.add_entry_fee(self.prices.iloc[self._current_tick].open)
|
||||
last_trade_price = self.add_exit_fee(self.prices.iloc[self._last_trade_tick].open)
|
||||
return (last_trade_price - current_price) / last_trade_price
|
||||
elif self._position == Positions.Long:
|
||||
current_price = self.add_exit_fee(self.prices.iloc[self._current_tick].open)
|
||||
last_trade_price = self.add_entry_fee(self.prices.iloc[self._last_trade_tick].open)
|
||||
return (current_price - last_trade_price) / last_trade_price
|
||||
else:
|
||||
return 0.
|
||||
|
||||
@abstractmethod
|
||||
def is_tradesignal(self, action: int):
|
||||
"""
|
||||
Determine if the signal is a trade signal. This is
|
||||
unique to the actions in the environment, and therefore must be
|
||||
inherited.
|
||||
"""
|
||||
return
|
||||
|
||||
def _is_valid(self, action: int):
|
||||
"""
|
||||
Determine if the signal is valid.This is
|
||||
unique to the actions in the environment, and therefore must be
|
||||
inherited.
|
||||
"""
|
||||
return
|
||||
|
||||
def add_entry_fee(self, price):
|
||||
return price * (1 + self.fee)
|
||||
|
||||
def add_exit_fee(self, price):
|
||||
return price / (1 + self.fee)
|
||||
|
||||
def _update_history(self, info):
|
||||
if not self.history:
|
||||
self.history = {key: [] for key in info.keys()}
|
||||
|
||||
for key, value in info.items():
|
||||
self.history[key].append(value)
|
||||
|
||||
@abstractmethod
|
||||
def calculate_reward(self, action):
|
||||
"""
|
||||
An example reward function. This is the one function that users will likely
|
||||
wish to inject their own creativity into.
|
||||
:params:
|
||||
action: int = The action made by the agent for the current candle.
|
||||
:returns:
|
||||
float = the reward to give to the agent for current step (used for optimization
|
||||
of weights in NN)
|
||||
"""
|
||||
|
||||
def _update_unrealized_total_profit(self):
|
||||
"""
|
||||
Update the unrealized total profit incase of episode end.
|
||||
"""
|
||||
if self._position in (Positions.Long, Positions.Short):
|
||||
pnl = self.get_unrealized_profit()
|
||||
if self.compound_trades:
|
||||
# assumes unit stake and compounding
|
||||
unrl_profit = self._total_profit * (1 + pnl)
|
||||
else:
|
||||
# assumes unit stake and no compounding
|
||||
unrl_profit = self._total_profit + pnl
|
||||
self._total_unrealized_profit = unrl_profit
|
||||
|
||||
def _update_total_profit(self):
|
||||
pnl = self.get_unrealized_profit()
|
||||
if self.compound_trades:
|
||||
# assumes unite stake and compounding
|
||||
self._total_profit = self._total_profit * (1 + pnl)
|
||||
else:
|
||||
# assumes unit stake and no compounding
|
||||
self._total_profit += pnl
|
||||
|
||||
def most_recent_return(self, action: int):
|
||||
"""
|
||||
Calculate the tick to tick return if in a trade.
|
||||
Return is generated from rising prices in Long
|
||||
and falling prices in Short positions.
|
||||
The actions Sell/Buy or Hold during a Long position trigger the sell/buy-fee.
|
||||
"""
|
||||
# Long positions
|
||||
if self._position == Positions.Long:
|
||||
current_price = self.prices.iloc[self._current_tick].open
|
||||
previous_price = self.prices.iloc[self._current_tick - 1].open
|
||||
|
||||
if (self._position_history[self._current_tick - 1] == Positions.Short
|
||||
or self._position_history[self._current_tick - 1] == Positions.Neutral):
|
||||
previous_price = self.add_entry_fee(previous_price)
|
||||
|
||||
return np.log(current_price) - np.log(previous_price)
|
||||
|
||||
# Short positions
|
||||
if self._position == Positions.Short:
|
||||
current_price = self.prices.iloc[self._current_tick].open
|
||||
previous_price = self.prices.iloc[self._current_tick - 1].open
|
||||
if (self._position_history[self._current_tick - 1] == Positions.Long
|
||||
or self._position_history[self._current_tick - 1] == Positions.Neutral):
|
||||
previous_price = self.add_exit_fee(previous_price)
|
||||
|
||||
return np.log(previous_price) - np.log(current_price)
|
||||
|
||||
return 0
|
||||
|
||||
def update_portfolio_log_returns(self, action):
|
||||
self.portfolio_log_returns[self._current_tick] = self.most_recent_return(action)
|
||||
|
||||
def current_price(self) -> float:
|
||||
return self.prices.iloc[self._current_tick].open
|
376
freqtrade/freqai/RL/BaseReinforcementLearningModel.py
Normal file
376
freqtrade/freqai/RL/BaseReinforcementLearningModel.py
Normal file
@@ -0,0 +1,376 @@
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Tuple, Type, Union
|
||||
|
||||
import gym
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import pandas as pd
|
||||
import torch as th
|
||||
import torch.multiprocessing
|
||||
from pandas import DataFrame
|
||||
from stable_baselines3.common.callbacks import EvalCallback
|
||||
from stable_baselines3.common.monitor import Monitor
|
||||
from stable_baselines3.common.utils import set_random_seed
|
||||
from stable_baselines3.common.vec_env import SubprocVecEnv
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv
|
||||
from freqtrade.freqai.RL.BaseEnvironment import Positions
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
torch.multiprocessing.set_sharing_strategy('file_system')
|
||||
|
||||
SB3_MODELS = ['PPO', 'A2C', 'DQN']
|
||||
SB3_CONTRIB_MODELS = ['TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO']
|
||||
|
||||
|
||||
class BaseReinforcementLearningModel(IFreqaiModel):
|
||||
"""
|
||||
User created Reinforcement Learning Model prediction class
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(config=kwargs['config'])
|
||||
self.max_threads = min(self.freqai_info['rl_config'].get(
|
||||
'cpu_count', 1), max(int(self.max_system_threads / 2), 1))
|
||||
th.set_num_threads(self.max_threads)
|
||||
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
|
||||
self.train_env: Union[SubprocVecEnv, gym.Env] = None
|
||||
self.eval_env: Union[SubprocVecEnv, gym.Env] = None
|
||||
self.eval_callback: EvalCallback = None
|
||||
self.model_type = self.freqai_info['rl_config']['model_type']
|
||||
self.rl_config = self.freqai_info['rl_config']
|
||||
self.continual_learning = self.freqai_info.get('continual_learning', False)
|
||||
if self.model_type in SB3_MODELS:
|
||||
import_str = 'stable_baselines3'
|
||||
elif self.model_type in SB3_CONTRIB_MODELS:
|
||||
import_str = 'sb3_contrib'
|
||||
else:
|
||||
raise OperationalException(f'{self.model_type} not available in stable_baselines3 or '
|
||||
f'sb3_contrib. please choose one of {SB3_MODELS} or '
|
||||
f'{SB3_CONTRIB_MODELS}')
|
||||
|
||||
mod = __import__(import_str, fromlist=[
|
||||
self.model_type])
|
||||
self.MODELCLASS = getattr(mod, self.model_type)
|
||||
self.policy_type = self.freqai_info['rl_config']['policy_type']
|
||||
self.unset_outlier_removal()
|
||||
|
||||
def unset_outlier_removal(self):
|
||||
"""
|
||||
If user has activated any function that may remove training points, this
|
||||
function will set them to false and warn them
|
||||
"""
|
||||
if self.ft_params.get('use_SVM_to_remove_outliers', False):
|
||||
self.ft_params.update({'use_SVM_to_remove_outliers': False})
|
||||
logger.warning('User tried to use SVM with RL. Deactivating SVM.')
|
||||
if self.ft_params.get('use_DBSCAN_to_remove_outliers', False):
|
||||
self.ft_params.update({'use_SVM_to_remove_outliers': False})
|
||||
logger.warning('User tried to use DBSCAN with RL. Deactivating DBSCAN.')
|
||||
if self.freqai_info['data_split_parameters'].get('shuffle', False):
|
||||
self.freqai_info['data_split_parameters'].update('shuffle', False)
|
||||
logger.warning('User tried to shuffle training data. Setting shuffle to False')
|
||||
|
||||
def train(
|
||||
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
|
||||
for storing, saving, loading, and analyzing the data.
|
||||
:param unfiltered_df: Full dataframe for the current training period
|
||||
:param metadata: pair metadata from strategy.
|
||||
:returns:
|
||||
:model: Trained model which can be used to inference (self.predict)
|
||||
"""
|
||||
|
||||
logger.info("--------------------Starting training " f"{pair} --------------------")
|
||||
|
||||
features_filtered, labels_filtered = dk.filter_features(
|
||||
unfiltered_df,
|
||||
dk.training_features_list,
|
||||
dk.label_list,
|
||||
training_filter=True,
|
||||
)
|
||||
|
||||
data_dictionary: Dict[str, Any] = dk.make_train_test_datasets(
|
||||
features_filtered, labels_filtered)
|
||||
dk.fit_labels() # FIXME useless for now, but just satiating append methods
|
||||
|
||||
# normalize all data based on train_dataset only
|
||||
prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk)
|
||||
data_dictionary = dk.normalize_data(data_dictionary)
|
||||
|
||||
# data cleaning/analysis
|
||||
self.data_cleaning_train(dk)
|
||||
|
||||
logger.info(
|
||||
f'Training model on {len(dk.data_dictionary["train_features"].columns)}'
|
||||
f' features and {len(data_dictionary["train_features"])} data points'
|
||||
)
|
||||
|
||||
self.set_train_and_eval_environments(data_dictionary, prices_train, prices_test, dk)
|
||||
|
||||
model = self.fit(data_dictionary, dk)
|
||||
|
||||
logger.info(f"--------------------done training {pair}--------------------")
|
||||
|
||||
return model
|
||||
|
||||
def set_train_and_eval_environments(self, data_dictionary: Dict[str, DataFrame],
|
||||
prices_train: DataFrame, prices_test: DataFrame,
|
||||
dk: FreqaiDataKitchen):
|
||||
"""
|
||||
User can override this if they are using a custom MyRLEnv
|
||||
:params:
|
||||
data_dictionary: dict = common data dictionary containing train and test
|
||||
features/labels/weights.
|
||||
prices_train/test: DataFrame = dataframe comprised of the prices to be used in the
|
||||
environment during training
|
||||
or testing
|
||||
dk: FreqaiDataKitchen = the datakitchen for the current pair
|
||||
"""
|
||||
train_df = data_dictionary["train_features"]
|
||||
test_df = data_dictionary["test_features"]
|
||||
|
||||
self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, window_size=self.CONV_WIDTH,
|
||||
reward_kwargs=self.reward_params, config=self.config)
|
||||
self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test,
|
||||
window_size=self.CONV_WIDTH,
|
||||
reward_kwargs=self.reward_params, config=self.config))
|
||||
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
||||
render=False, eval_freq=len(train_df),
|
||||
best_model_save_path=str(dk.data_path))
|
||||
|
||||
@abstractmethod
|
||||
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
||||
"""
|
||||
Agent customizations and abstract Reinforcement Learning customizations
|
||||
go in here. Abstract method, so this function must be overridden by
|
||||
user class.
|
||||
"""
|
||||
return
|
||||
|
||||
def get_state_info(self, pair: str) -> Tuple[float, float, int]:
|
||||
"""
|
||||
State info during dry/live/backtesting which is fed back
|
||||
into the model.
|
||||
:param:
|
||||
pair: str = COIN/STAKE to get the environment information for
|
||||
:returns:
|
||||
market_side: float = representing short, long, or neutral for
|
||||
pair
|
||||
trade_duration: int = the number of candles that the trade has
|
||||
been open for
|
||||
"""
|
||||
open_trades = Trade.get_trades_proxy(is_open=True)
|
||||
market_side = 0.5
|
||||
current_profit: float = 0
|
||||
trade_duration = 0
|
||||
for trade in open_trades:
|
||||
if trade.pair == pair:
|
||||
if self.strategy.dp._exchange is None: # type: ignore
|
||||
logger.error('No exchange available.')
|
||||
else:
|
||||
current_value = self.strategy.dp._exchange.get_rate( # type: ignore
|
||||
pair, refresh=False, side="exit", is_short=trade.is_short)
|
||||
openrate = trade.open_rate
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
trade_duration = int((now - trade.open_date.timestamp()) / self.base_tf_seconds)
|
||||
if 'long' in str(trade.enter_tag):
|
||||
market_side = 1
|
||||
current_profit = (current_value - openrate) / openrate
|
||||
else:
|
||||
market_side = 0
|
||||
current_profit = (openrate - current_value) / openrate
|
||||
|
||||
return market_side, current_profit, int(trade_duration)
|
||||
|
||||
def predict(
|
||||
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||
"""
|
||||
Filter the prediction features data and predict with it.
|
||||
:param: unfiltered_dataframe: Full dataframe for the current backtest period.
|
||||
:return:
|
||||
:pred_df: dataframe containing the predictions
|
||||
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||
data (NaNs) or felt uncertain about data (PCA and DI index)
|
||||
"""
|
||||
|
||||
dk.find_features(unfiltered_df)
|
||||
filtered_dataframe, _ = dk.filter_features(
|
||||
unfiltered_df, dk.training_features_list, training_filter=False
|
||||
)
|
||||
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
|
||||
dk.data_dictionary["prediction_features"] = filtered_dataframe
|
||||
|
||||
# optional additional data cleaning/analysis
|
||||
self.data_cleaning_predict(dk)
|
||||
|
||||
pred_df = self.rl_model_predict(
|
||||
dk.data_dictionary["prediction_features"], dk, self.model)
|
||||
pred_df.fillna(0, inplace=True)
|
||||
|
||||
return (pred_df, dk.do_predict)
|
||||
|
||||
def rl_model_predict(self, dataframe: DataFrame,
|
||||
dk: FreqaiDataKitchen, model: Any) -> DataFrame:
|
||||
"""
|
||||
A helper function to make predictions in the Reinforcement learning module.
|
||||
:params:
|
||||
dataframe: DataFrame = the dataframe of features to make the predictions on
|
||||
dk: FreqaiDatakitchen = data kitchen for the current pair
|
||||
model: Any = the trained model used to inference the features.
|
||||
"""
|
||||
output = pd.DataFrame(np.zeros(len(dataframe)), columns=dk.label_list)
|
||||
|
||||
def _predict(window):
|
||||
market_side, current_profit, trade_duration = self.get_state_info(dk.pair)
|
||||
observations = dataframe.iloc[window.index]
|
||||
observations['current_profit_pct'] = current_profit
|
||||
observations['position'] = market_side
|
||||
observations['trade_duration'] = trade_duration
|
||||
res, _ = model.predict(observations, deterministic=True)
|
||||
return res
|
||||
|
||||
output = output.rolling(window=self.CONV_WIDTH).apply(_predict)
|
||||
|
||||
return output
|
||||
|
||||
def build_ohlc_price_dataframes(self, data_dictionary: dict,
|
||||
pair: str, dk: FreqaiDataKitchen) -> Tuple[DataFrame,
|
||||
DataFrame]:
|
||||
"""
|
||||
Builds the train prices and test prices for the environment.
|
||||
"""
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
train_df = data_dictionary["train_features"]
|
||||
test_df = data_dictionary["test_features"]
|
||||
|
||||
# price data for model training and evaluation
|
||||
tf = self.config['timeframe']
|
||||
ohlc_list = [f'%-{coin}raw_open_{tf}', f'%-{coin}raw_low_{tf}',
|
||||
f'%-{coin}raw_high_{tf}', f'%-{coin}raw_close_{tf}']
|
||||
rename_dict = {f'%-{coin}raw_open_{tf}': 'open', f'%-{coin}raw_low_{tf}': 'low',
|
||||
f'%-{coin}raw_high_{tf}': ' high', f'%-{coin}raw_close_{tf}': 'close'}
|
||||
|
||||
prices_train = train_df.filter(ohlc_list, axis=1)
|
||||
prices_train.rename(columns=rename_dict, inplace=True)
|
||||
prices_train.reset_index(drop=True)
|
||||
|
||||
prices_test = test_df.filter(ohlc_list, axis=1)
|
||||
prices_test.rename(columns=rename_dict, inplace=True)
|
||||
prices_test.reset_index(drop=True)
|
||||
|
||||
return prices_train, prices_test
|
||||
|
||||
def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any:
|
||||
"""
|
||||
Can be used by user if they are trying to limit_ram_usage *and*
|
||||
perform continual learning.
|
||||
For now, this is unused.
|
||||
"""
|
||||
exists = Path(dk.data_path / f"{dk.model_filename}_model").is_file()
|
||||
if exists:
|
||||
model = self.MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model")
|
||||
else:
|
||||
logger.info('No model file on disk to continue learning from.')
|
||||
|
||||
return model
|
||||
|
||||
# Nested class which can be overridden by user to customize further
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||
sets a custom reward based on profit and trade duration.
|
||||
"""
|
||||
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
"""
|
||||
An example reward function. This is the one function that users will likely
|
||||
wish to inject their own creativity into.
|
||||
:params:
|
||||
action: int = The action made by the agent for the current candle.
|
||||
:returns:
|
||||
float = the reward to give to the agent for current step (used for optimization
|
||||
of weights in NN)
|
||||
"""
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
return -2
|
||||
|
||||
pnl = self.get_unrealized_profit()
|
||||
rew = np.sign(pnl) * (pnl + 1)
|
||||
factor = 100.
|
||||
|
||||
# reward agent for entering trades
|
||||
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||
and self._position == Positions.Neutral):
|
||||
return 25
|
||||
# discourage agent from not entering trades
|
||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||
return -1
|
||||
|
||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||
if self._last_trade_tick:
|
||||
trade_duration = self._current_tick - self._last_trade_tick
|
||||
else:
|
||||
trade_duration = 0
|
||||
|
||||
if trade_duration <= max_trade_duration:
|
||||
factor *= 1.5
|
||||
elif trade_duration > max_trade_duration:
|
||||
factor *= 0.5
|
||||
|
||||
# discourage sitting in position
|
||||
if (self._position in (Positions.Short, Positions.Long) and
|
||||
action == Actions.Neutral.value):
|
||||
return -1 * trade_duration / max_trade_duration
|
||||
|
||||
# close long
|
||||
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(rew * factor)
|
||||
|
||||
# close short
|
||||
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(rew * factor)
|
||||
|
||||
return 0.
|
||||
|
||||
|
||||
def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
|
||||
seed: int, train_df: DataFrame, price: DataFrame,
|
||||
reward_params: Dict[str, int], window_size: int, monitor: bool = False,
|
||||
config: Dict[str, Any] = {}) -> Callable:
|
||||
"""
|
||||
Utility function for multiprocessed env.
|
||||
|
||||
:param env_id: (str) the environment ID
|
||||
:param num_env: (int) the number of environment you wish to have in subprocesses
|
||||
:param seed: (int) the inital seed for RNG
|
||||
:param rank: (int) index of the subprocess
|
||||
:return: (Callable)
|
||||
"""
|
||||
|
||||
def _init() -> gym.Env:
|
||||
|
||||
env = MyRLEnv(df=train_df, prices=price, window_size=window_size,
|
||||
reward_kwargs=reward_params, id=env_id, seed=seed + rank, config=config)
|
||||
if monitor:
|
||||
env = Monitor(env)
|
||||
return env
|
||||
set_random_seed(seed)
|
||||
return _init
|
0
freqtrade/freqai/RL/__init__.py
Normal file
0
freqtrade/freqai/RL/__init__.py
Normal file
@@ -3,10 +3,10 @@ from time import time
|
||||
from typing import Any
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
import numpy as np
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
||||
|
||||
import tensorflow as tf
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,6 +17,13 @@ class BaseTensorFlowModel(IFreqaiModel):
|
||||
User *must* inherit from this class and set fit() and predict().
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(config=kwargs['config'])
|
||||
self.keras = True
|
||||
if self.ft_params.get("DI_threshold", 0):
|
||||
self.ft_params["DI_threshold"] = 0
|
||||
logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
|
||||
|
||||
def train(
|
||||
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Any:
|
||||
@@ -68,3 +75,76 @@ class BaseTensorFlowModel(IFreqaiModel):
|
||||
f"({end_time - start_time:.2f} secs) --------------------")
|
||||
|
||||
return model
|
||||
|
||||
|
||||
class WindowGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
input_width,
|
||||
label_width,
|
||||
shift,
|
||||
train_df=None,
|
||||
val_df=None,
|
||||
test_df=None,
|
||||
train_labels=None,
|
||||
val_labels=None,
|
||||
test_labels=None,
|
||||
batch_size=None,
|
||||
):
|
||||
# Store the raw data.
|
||||
self.train_df = train_df
|
||||
self.val_df = val_df
|
||||
self.test_df = test_df
|
||||
self.train_labels = train_labels
|
||||
self.val_labels = val_labels
|
||||
self.test_labels = test_labels
|
||||
self.batch_size = batch_size
|
||||
self.input_width = input_width
|
||||
self.label_width = label_width
|
||||
self.shift = shift
|
||||
self.total_window_size = input_width + shift
|
||||
self.input_slice = slice(0, input_width)
|
||||
self.input_indices = np.arange(self.total_window_size)[self.input_slice]
|
||||
|
||||
def make_dataset(self, data, labels=None):
|
||||
data = np.array(data, dtype=np.float32)
|
||||
if labels is not None:
|
||||
labels = np.array(labels, dtype=np.float32)
|
||||
ds = tf.keras.preprocessing.timeseries_dataset_from_array(
|
||||
data=data,
|
||||
targets=labels,
|
||||
sequence_length=self.total_window_size,
|
||||
sequence_stride=1,
|
||||
sampling_rate=1,
|
||||
shuffle=False,
|
||||
batch_size=self.batch_size,
|
||||
)
|
||||
|
||||
return ds
|
||||
|
||||
@property
|
||||
def train(self):
|
||||
return self.make_dataset(self.train_df, self.train_labels)
|
||||
|
||||
@property
|
||||
def val(self):
|
||||
return self.make_dataset(self.val_df, self.val_labels)
|
||||
|
||||
@property
|
||||
def test(self):
|
||||
return self.make_dataset(self.test_df, self.test_labels)
|
||||
|
||||
@property
|
||||
def inference(self):
|
||||
return self.make_dataset(self.test_df)
|
||||
|
||||
@property
|
||||
def example(self):
|
||||
"""Get and cache an example batch of `inputs, labels` for plotting."""
|
||||
result = getattr(self, "_example", None)
|
||||
if result is None:
|
||||
# No example batch was found, so get one from the `.train` dataset
|
||||
result = next(iter(self.train))
|
||||
# And cache it for next time
|
||||
self._example = result
|
||||
return result
|
||||
|
@@ -91,6 +91,13 @@ class FreqaiDataDrawer:
|
||||
self.empty_pair_dict: pair_info = {
|
||||
"model_filename": "", "trained_timestamp": 0,
|
||||
"data_path": "", "extras": {}}
|
||||
self.limit_ram_use = self.freqai_info.get('limit_ram_usage', False)
|
||||
if 'rl_config' in self.freqai_info:
|
||||
self.model_type = 'stable_baselines'
|
||||
logger.warning('User indicated rl_config, FreqAI will now use stable_baselines3'
|
||||
' to save models.')
|
||||
else:
|
||||
self.model_type = self.freqai_info.get('model_save_type', 'joblib')
|
||||
|
||||
def load_drawer_from_disk(self):
|
||||
"""
|
||||
@@ -423,10 +430,12 @@ class FreqaiDataDrawer:
|
||||
save_path = Path(dk.data_path)
|
||||
|
||||
# Save the trained model
|
||||
if not dk.keras:
|
||||
if self.model_type == 'joblib':
|
||||
dump(model, save_path / f"{dk.model_filename}_model.joblib")
|
||||
else:
|
||||
elif self.model_type == 'keras':
|
||||
model.save(save_path / f"{dk.model_filename}_model.h5")
|
||||
elif 'stable_baselines' in self.model_type:
|
||||
model.save(save_path / f"{dk.model_filename}_model.zip")
|
||||
|
||||
if dk.svm_model is not None:
|
||||
dump(dk.svm_model, save_path / f"{dk.model_filename}_svm_model.joblib")
|
||||
@@ -453,7 +462,7 @@ class FreqaiDataDrawer:
|
||||
dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb")
|
||||
)
|
||||
|
||||
# if self.live:
|
||||
if not self.limit_ram_use:
|
||||
self.model_dictionary[coin] = model
|
||||
self.pair_dict[coin]["model_filename"] = dk.model_filename
|
||||
self.pair_dict[coin]["data_path"] = str(dk.data_path)
|
||||
@@ -503,14 +512,18 @@ class FreqaiDataDrawer:
|
||||
)
|
||||
|
||||
# try to access model in memory instead of loading object from disk to save time
|
||||
if dk.live and coin in self.model_dictionary:
|
||||
if dk.live and coin in self.model_dictionary and not self.limit_ram_use:
|
||||
model = self.model_dictionary[coin]
|
||||
elif not dk.keras:
|
||||
elif self.model_type == 'joblib':
|
||||
model = load(dk.data_path / f"{dk.model_filename}_model.joblib")
|
||||
else:
|
||||
elif self.model_type == 'keras':
|
||||
from tensorflow import keras
|
||||
|
||||
model = keras.models.load_model(dk.data_path / f"{dk.model_filename}_model.h5")
|
||||
elif self.model_type == 'stable_baselines':
|
||||
mod = __import__('stable_baselines3', fromlist=[
|
||||
self.freqai_info['rl_config']['model_type']])
|
||||
MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type'])
|
||||
model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model")
|
||||
|
||||
if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file():
|
||||
dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib")
|
||||
@@ -520,7 +533,11 @@ class FreqaiDataDrawer:
|
||||
f"Unable to load model, ensure model exists at " f"{dk.data_path} "
|
||||
)
|
||||
|
||||
if self.config["freqai"]["feature_parameters"].get("principal_component_analysis", False):
|
||||
# load it into ram if it was loaded from disk
|
||||
if coin not in self.model_dictionary and not self.limit_ram_use:
|
||||
self.model_dictionary[coin] = model
|
||||
|
||||
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
|
||||
dk.pca = cloudpickle.load(
|
||||
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
|
||||
)
|
||||
@@ -616,9 +633,9 @@ class FreqaiDataDrawer:
|
||||
pairs = self.freqai_info["feature_parameters"].get(
|
||||
"include_corr_pairlist", []
|
||||
)
|
||||
|
||||
for tf in self.freqai_info["feature_parameters"].get("include_timeframes"):
|
||||
base_dataframes[tf] = dk.slice_dataframe(
|
||||
timerange, historic_data[pair][tf]).reset_index(drop=True)
|
||||
base_dataframes[tf] = dk.slice_dataframe(timerange, historic_data[pair][tf])
|
||||
if pairs:
|
||||
for p in pairs:
|
||||
if pair in p:
|
||||
@@ -627,25 +644,6 @@ class FreqaiDataDrawer:
|
||||
corr_dataframes[p] = {}
|
||||
corr_dataframes[p][tf] = dk.slice_dataframe(
|
||||
timerange, historic_data[p][tf]
|
||||
).reset_index(drop=True)
|
||||
)
|
||||
|
||||
return corr_dataframes, base_dataframes
|
||||
|
||||
# to be used if we want to send predictions directly to the follower instead of forcing
|
||||
# follower to load models and inference
|
||||
# def save_model_return_values_to_disk(self) -> None:
|
||||
# with open(self.full_path / str('model_return_values.json'), "w") as fp:
|
||||
# json.dump(self.model_return_values, fp, default=self.np_encoder)
|
||||
|
||||
# def load_model_return_values_from_disk(self, dk: FreqaiDataKitchen) -> FreqaiDataKitchen:
|
||||
# exists = Path(self.full_path / str('model_return_values.json')).resolve().exists()
|
||||
# if exists:
|
||||
# with open(self.full_path / str('model_return_values.json'), "r") as fp:
|
||||
# self.model_return_values = json.load(fp)
|
||||
# elif not self.follow_mode:
|
||||
# logger.info("Could not find existing datadrawer, starting from scratch")
|
||||
# else:
|
||||
# logger.warning(f'Follower could not find pair_dictionary at {self.full_path} '
|
||||
# 'sending null values back to strategy')
|
||||
|
||||
# return exists, dk
|
||||
|
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Tuple
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import pandas as pd
|
||||
import psutil
|
||||
from pandas import DataFrame
|
||||
from scipy import stats
|
||||
from sklearn import linear_model
|
||||
@@ -76,9 +77,10 @@ class FreqaiDataKitchen:
|
||||
self.backtest_predictions_folder: str = "backtesting_predictions"
|
||||
self.live = live
|
||||
self.pair = pair
|
||||
self.model_save_type = self.freqai_config.get('model_save_type', 'joblib')
|
||||
|
||||
self.svm_model: linear_model.SGDOneClassSVM = None
|
||||
self.keras: bool = self.freqai_config.get("keras", False)
|
||||
# self.model_save_type: bool = self.freqai_config.get("keras", False)
|
||||
self.set_all_pairs()
|
||||
if not self.live:
|
||||
if not self.config["timerange"]:
|
||||
@@ -95,11 +97,13 @@ class FreqaiDataKitchen:
|
||||
)
|
||||
|
||||
self.data['extra_returns_per_train'] = self.freqai_config.get('extra_returns_per_train', {})
|
||||
self.thread_count = self.freqai_config.get("data_kitchen_thread_count", -1)
|
||||
if not self.freqai_config.get("data_kitchen_thread_count", 0):
|
||||
self.thread_count = max(int(psutil.cpu_count() * 2 - 2), 1)
|
||||
else:
|
||||
self.thread_count = self.freqai_config["data_kitchen_thread_count"]
|
||||
self.train_dates: DataFrame = pd.DataFrame()
|
||||
self.unique_classes: Dict[str, list] = {}
|
||||
self.unique_class_list: list = []
|
||||
self.spice_dataframe: DataFrame = None
|
||||
|
||||
def set_paths(
|
||||
self,
|
||||
@@ -566,7 +570,7 @@ class FreqaiDataKitchen:
|
||||
predict: bool = If true, inference an existing SVM model, else construct one
|
||||
"""
|
||||
|
||||
if self.keras:
|
||||
if self.model_save_type == 'keras':
|
||||
logger.warning(
|
||||
"SVM outlier removal not currently supported for Keras based models. "
|
||||
"Skipping user requested function."
|
||||
@@ -1260,11 +1264,3 @@ class FreqaiDataKitchen:
|
||||
f"Could not find backtesting prediction file at {path_to_predictionfile}"
|
||||
)
|
||||
return file_exists
|
||||
|
||||
def spice_extractor(self, indicator: str, dataframe: DataFrame) -> npt.NDArray:
|
||||
if indicator in dataframe.columns:
|
||||
return np.array(dataframe[indicator])
|
||||
else:
|
||||
logger.warning(f'User asked spice_rack for {indicator}, '
|
||||
f'but it is not available. Returning 0s')
|
||||
return np.zeros(len(dataframe.index))
|
||||
|
@@ -7,10 +7,11 @@ from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import psutil
|
||||
from numpy.typing import NDArray
|
||||
from pandas import DataFrame
|
||||
|
||||
@@ -72,10 +73,10 @@ class IFreqaiModel(ABC):
|
||||
self.identifier: str = self.freqai_info.get("identifier", "no_id_provided")
|
||||
self.scanning = False
|
||||
self.ft_params = self.freqai_info["feature_parameters"]
|
||||
self.keras: bool = self.freqai_info.get("keras", False)
|
||||
if self.keras and self.ft_params.get("DI_threshold", 0):
|
||||
self.ft_params["DI_threshold"] = 0
|
||||
logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
|
||||
# self.keras: bool = self.freqai_info.get("keras", False)
|
||||
# if self.keras and self.ft_params.get("DI_threshold", 0):
|
||||
# self.ft_params["DI_threshold"] = 0
|
||||
# logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
|
||||
self.CONV_WIDTH = self.freqai_info.get("conv_width", 2)
|
||||
if self.ft_params.get("inlier_metric_window", 0):
|
||||
self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2
|
||||
@@ -93,15 +94,18 @@ class IFreqaiModel(ABC):
|
||||
self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe'])
|
||||
self.continual_learning = self.freqai_info.get('continual_learning', False)
|
||||
self.plot_features = self.ft_params.get("plot_feature_importances", 0)
|
||||
self.spice_rack_open: bool = False
|
||||
|
||||
self._threads: List[threading.Thread] = []
|
||||
self._stop_event = threading.Event()
|
||||
self.strategy: Optional[IStrategy] = None
|
||||
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
|
||||
|
||||
def __getstate__(self):
|
||||
"""
|
||||
Return an empty state to be pickled in hyperopt
|
||||
"""
|
||||
return ({})
|
||||
self.strategy: Optional[IStrategy] = None
|
||||
|
||||
def assert_config(self, config: Config) -> None:
|
||||
|
||||
@@ -122,6 +126,7 @@ class IFreqaiModel(ABC):
|
||||
|
||||
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
|
||||
self.dd.set_pair_dict_info(metadata)
|
||||
self.strategy = strategy
|
||||
|
||||
if self.live:
|
||||
self.inference_timer('start')
|
||||
@@ -142,7 +147,7 @@ class IFreqaiModel(ABC):
|
||||
dk = self.start_backtesting(dataframe, metadata, self.dk)
|
||||
|
||||
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
||||
# self.clean_up()
|
||||
self.clean_up()
|
||||
if self.live:
|
||||
self.inference_timer('stop')
|
||||
return dataframe
|
||||
@@ -156,6 +161,13 @@ class IFreqaiModel(ABC):
|
||||
self.model = None
|
||||
self.dk = None
|
||||
|
||||
def _on_stop(self):
|
||||
"""
|
||||
Callback for Subclasses to override to include logic for shutting down resources
|
||||
when SIGINT is sent.
|
||||
"""
|
||||
return
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Cleans up threads on Shutdown, set stop event. Join threads to wait
|
||||
@@ -164,6 +176,8 @@ class IFreqaiModel(ABC):
|
||||
logger.info("Stopping FreqAI")
|
||||
self._stop_event.set()
|
||||
|
||||
self._on_stop()
|
||||
|
||||
logger.info("Waiting on Training iteration")
|
||||
for _thread in self._threads:
|
||||
_thread.join()
|
||||
@@ -211,8 +225,7 @@ class IFreqaiModel(ABC):
|
||||
new_trained_timerange, pair, strategy, dk, data_load_timerange
|
||||
)
|
||||
except Exception as msg:
|
||||
logger.warning(f"Training {pair} raised exception {msg.__class__.__name__}. "
|
||||
f"Message: {msg}, skipping.")
|
||||
logger.warning(f'Training {pair} raised exception {msg}, skipping.')
|
||||
|
||||
self.train_timer('stop')
|
||||
|
||||
@@ -632,7 +645,8 @@ class IFreqaiModel(ABC):
|
||||
|
||||
# # for keras type models, the conv_window needs to be prepended so
|
||||
# # viewing is correct in frequi
|
||||
if self.freqai_info.get('keras', False) or self.ft_params.get('inlier_metric_window', 0):
|
||||
if (not self.freqai_info.get('model_save_type', 'joblib') or
|
||||
self.ft_params.get('inlier_metric_window', 0)):
|
||||
n_lost_points = self.freqai_info.get('conv_width', 2)
|
||||
zeros_df = DataFrame(np.zeros((n_lost_points, len(hist_preds_df.columns))),
|
||||
columns=hist_preds_df.columns)
|
||||
@@ -732,18 +746,6 @@ class IFreqaiModel(ABC):
|
||||
f'Best approximation queue: {best_queue}')
|
||||
return best_queue
|
||||
|
||||
def spice_rack(self, indicator: str, dataframe: DataFrame,
|
||||
metadata: dict, strategy: IStrategy) -> NDArray:
|
||||
if not self.spice_rack_open:
|
||||
dataframe = self.start(dataframe, metadata, strategy)
|
||||
self.dk.spice_dataframe = dataframe
|
||||
self.spice_rack_open = True
|
||||
return self.dk.spice_extractor(indicator, dataframe)
|
||||
else:
|
||||
return self.dk.spice_extractor(indicator, self.dk.spice_dataframe)
|
||||
|
||||
def close_spice_rack(self):
|
||||
self.spice_rack_open = False
|
||||
# Following methods which are overridden by user made prediction models.
|
||||
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
|
||||
|
||||
|
144
freqtrade/freqai/prediction_models/CNNPredictionModel.py
Normal file
144
freqtrade/freqai/prediction_models/CNNPredictionModel.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import logging
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
import tensorflow as tf
|
||||
from freqtrade.freqai.base_models.BaseTensorFlowModel import BaseTensorFlowModel, WindowGenerator
|
||||
from tensorflow.keras.layers import Input, Conv1D, Dense
|
||||
from tensorflow.keras.models import Model
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# tf.config.run_functions_eagerly(True)
|
||||
# tf.data.experimental.enable_debug_mode()
|
||||
|
||||
MAX_EPOCHS = 10
|
||||
|
||||
|
||||
class CNNPredictionModel(BaseTensorFlowModel):
|
||||
"""
|
||||
User created prediction model. The class needs to override three necessary
|
||||
functions, predict(), fit().
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:params:
|
||||
:data_dictionary: the dictionary constructed by DataHandler to hold
|
||||
all the training and test data/labels.
|
||||
"""
|
||||
train_df = data_dictionary["train_features"]
|
||||
train_labels = data_dictionary["train_labels"]
|
||||
test_df = data_dictionary["test_features"]
|
||||
test_labels = data_dictionary["test_labels"]
|
||||
n_labels = len(train_labels.columns)
|
||||
if n_labels > 1:
|
||||
raise OperationalException(
|
||||
"Neural Net not yet configured for multi-targets. Please "
|
||||
" reduce number of targets to 1 in strategy."
|
||||
)
|
||||
|
||||
n_features = len(data_dictionary["train_features"].columns)
|
||||
BATCH_SIZE = self.freqai_info.get("batch_size", 64)
|
||||
input_dims = [BATCH_SIZE, self.CONV_WIDTH, n_features]
|
||||
|
||||
w1 = WindowGenerator(
|
||||
input_width=self.CONV_WIDTH,
|
||||
label_width=1,
|
||||
shift=1,
|
||||
train_df=train_df,
|
||||
val_df=test_df,
|
||||
train_labels=train_labels,
|
||||
val_labels=test_labels,
|
||||
batch_size=BATCH_SIZE,
|
||||
)
|
||||
|
||||
model = self.create_model(input_dims, n_labels)
|
||||
|
||||
steps_per_epoch = np.ceil(len(test_df) / BATCH_SIZE)
|
||||
lr_schedule = tf.keras.optimizers.schedules.InverseTimeDecay(
|
||||
0.001, decay_steps=steps_per_epoch * 1000, decay_rate=1, staircase=False
|
||||
)
|
||||
|
||||
early_stopping = tf.keras.callbacks.EarlyStopping(
|
||||
monitor="loss", patience=3, mode="min", min_delta=0.0001
|
||||
)
|
||||
|
||||
model.compile(
|
||||
loss=tf.losses.MeanSquaredError(),
|
||||
optimizer=tf.optimizers.Adam(lr_schedule),
|
||||
metrics=[tf.metrics.MeanAbsoluteError()],
|
||||
)
|
||||
|
||||
model.fit(
|
||||
w1.train,
|
||||
epochs=MAX_EPOCHS,
|
||||
shuffle=False,
|
||||
validation_data=w1.val,
|
||||
callbacks=[early_stopping],
|
||||
verbose=1,
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
def predict(
|
||||
self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first=True
|
||||
) -> Tuple[DataFrame, DataFrame]:
|
||||
"""
|
||||
Filter the prediction features data and predict with it.
|
||||
:param: unfiltered_dataframe: Full dataframe for the current backtest period.
|
||||
:return:
|
||||
:predictions: np.array of predictions
|
||||
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||
data (NaNs) or felt uncertain about data (PCA and DI index)
|
||||
"""
|
||||
|
||||
dk.find_features(unfiltered_dataframe)
|
||||
filtered_dataframe, _ = dk.filter_features(
|
||||
unfiltered_dataframe, dk.training_features_list, training_filter=False
|
||||
)
|
||||
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
|
||||
dk.data_dictionary["prediction_features"] = filtered_dataframe
|
||||
|
||||
# optional additional data cleaning/analysis
|
||||
self.data_cleaning_predict(dk, filtered_dataframe)
|
||||
|
||||
if first:
|
||||
full_df = dk.data_dictionary["prediction_features"]
|
||||
|
||||
w1 = WindowGenerator(
|
||||
input_width=self.CONV_WIDTH,
|
||||
label_width=1,
|
||||
shift=1,
|
||||
test_df=full_df,
|
||||
batch_size=len(full_df),
|
||||
)
|
||||
|
||||
predictions = self.model.predict(w1.inference)
|
||||
len_diff = len(dk.do_predict) - len(predictions)
|
||||
if len_diff > 0:
|
||||
dk.do_predict = dk.do_predict[len_diff:]
|
||||
|
||||
else:
|
||||
data = dk.data_dictionary["prediction_features"]
|
||||
data = tf.expand_dims(data, axis=0)
|
||||
predictions = self.model(data, training=False)
|
||||
|
||||
predictions = predictions[:, 0, 0]
|
||||
pred_df = DataFrame(predictions, columns=dk.label_list)
|
||||
|
||||
pred_df = dk.denormalize_labels_from_metadata(pred_df)
|
||||
|
||||
return (pred_df, np.ones(len(pred_df)))
|
||||
|
||||
def create_model(self, input_dims, n_labels) -> Any:
|
||||
|
||||
input_layer = Input(shape=(input_dims[1], input_dims[2]))
|
||||
Layer_1 = Conv1D(filters=32, kernel_size=(self.CONV_WIDTH,), activation="relu")(input_layer)
|
||||
Layer_3 = Dense(units=32, activation="relu")(Layer_1)
|
||||
output_layer = Dense(units=n_labels)(Layer_3)
|
||||
return Model(inputs=input_layer, outputs=output_layer)
|
118
freqtrade/freqai/prediction_models/ReinforcementLearner.py
Normal file
118
freqtrade/freqai/prediction_models/ReinforcementLearner.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import torch as th
|
||||
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||
from freqtrade.freqai.RL.BaseReinforcementLearningModel import BaseReinforcementLearningModel
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReinforcementLearner(BaseReinforcementLearningModel):
|
||||
"""
|
||||
User created Reinforcement Learning Model prediction model.
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
||||
"""
|
||||
User customizable fit method
|
||||
:params:
|
||||
data_dictionary: dict = common data dictionary containing all train/test
|
||||
features/labels/weights.
|
||||
dk: FreqaiDatakitchen = data kitchen for current pair.
|
||||
:returns:
|
||||
model: Any = trained model to be used for inference in dry/live/backtesting
|
||||
"""
|
||||
train_df = data_dictionary["train_features"]
|
||||
total_timesteps = self.freqai_info["rl_config"]["train_cycles"] * len(train_df)
|
||||
|
||||
policy_kwargs = dict(activation_fn=th.nn.ReLU,
|
||||
net_arch=[128, 128])
|
||||
|
||||
if dk.pair not in self.dd.model_dictionary or not self.continual_learning:
|
||||
model = self.MODELCLASS(self.policy_type, self.train_env, policy_kwargs=policy_kwargs,
|
||||
tensorboard_log=Path(
|
||||
dk.full_path / "tensorboard" / dk.pair.split('/')[0]),
|
||||
**self.freqai_info['model_training_parameters']
|
||||
)
|
||||
else:
|
||||
logger.info('Continual training activated - starting training from previously '
|
||||
'trained agent.')
|
||||
model = self.dd.model_dictionary[dk.pair]
|
||||
model.set_env(self.train_env)
|
||||
|
||||
model.learn(
|
||||
total_timesteps=int(total_timesteps),
|
||||
callback=self.eval_callback
|
||||
)
|
||||
|
||||
if Path(dk.data_path / "best_model.zip").is_file():
|
||||
logger.info('Callback found a best model.')
|
||||
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
||||
return best_model
|
||||
|
||||
logger.info('Couldnt find best model, using final model instead.')
|
||||
|
||||
return model
|
||||
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||
sets a custom reward based on profit and trade duration.
|
||||
"""
|
||||
|
||||
def calculate_reward(self, action):
|
||||
"""
|
||||
An example reward function. This is the one function that users will likely
|
||||
wish to inject their own creativity into.
|
||||
:params:
|
||||
action: int = The action made by the agent for the current candle.
|
||||
:returns:
|
||||
float = the reward to give to the agent for current step (used for optimization
|
||||
of weights in NN)
|
||||
"""
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
return -2
|
||||
|
||||
pnl = self.get_unrealized_profit()
|
||||
factor = 100
|
||||
|
||||
# reward agent for entering trades
|
||||
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||
and self._position == Positions.Neutral):
|
||||
return 25
|
||||
# discourage agent from not entering trades
|
||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||
return -1
|
||||
|
||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||
trade_duration = self._current_tick - self._last_trade_tick
|
||||
|
||||
if trade_duration <= max_trade_duration:
|
||||
factor *= 1.5
|
||||
elif trade_duration > max_trade_duration:
|
||||
factor *= 0.5
|
||||
|
||||
# discourage sitting in position
|
||||
if (self._position in (Positions.Short, Positions.Long) and
|
||||
action == Actions.Neutral.value):
|
||||
return -1 * trade_duration / max_trade_duration
|
||||
|
||||
# close long
|
||||
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
|
||||
# close short
|
||||
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(pnl * factor)
|
||||
|
||||
return 0.
|
@@ -0,0 +1,100 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict # , Tuple
|
||||
|
||||
# import numpy.typing as npt
|
||||
import torch as th
|
||||
from pandas import DataFrame
|
||||
from stable_baselines3.common.callbacks import EvalCallback
|
||||
from stable_baselines3.common.vec_env import SubprocVecEnv
|
||||
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.RL.BaseReinforcementLearningModel import (BaseReinforcementLearningModel,
|
||||
make_env)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReinforcementLearner_multiproc(BaseReinforcementLearningModel):
|
||||
"""
|
||||
User created Reinforcement Learning Model prediction model.
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
||||
|
||||
train_df = data_dictionary["train_features"]
|
||||
total_timesteps = self.freqai_info["rl_config"]["train_cycles"] * len(train_df)
|
||||
|
||||
# model arch
|
||||
policy_kwargs = dict(activation_fn=th.nn.ReLU,
|
||||
net_arch=[128, 128])
|
||||
|
||||
if dk.pair not in self.dd.model_dictionary or not self.continual_learning:
|
||||
model = self.MODELCLASS(self.policy_type, self.train_env, policy_kwargs=policy_kwargs,
|
||||
tensorboard_log=Path(
|
||||
dk.full_path / "tensorboard" / dk.pair.split('/')[0]),
|
||||
**self.freqai_info['model_training_parameters']
|
||||
)
|
||||
else:
|
||||
logger.info('Continual learning activated - starting training from previously '
|
||||
'trained agent.')
|
||||
model = self.dd.model_dictionary[dk.pair]
|
||||
model.set_env(self.train_env)
|
||||
|
||||
model.learn(
|
||||
total_timesteps=int(total_timesteps),
|
||||
callback=self.eval_callback
|
||||
)
|
||||
|
||||
if Path(dk.data_path / "best_model.zip").is_file():
|
||||
logger.info('Callback found a best model.')
|
||||
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
||||
return best_model
|
||||
|
||||
logger.info('Couldnt find best model, using final model instead.')
|
||||
|
||||
return model
|
||||
|
||||
def set_train_and_eval_environments(self, data_dictionary: Dict[str, Any],
|
||||
prices_train: DataFrame, prices_test: DataFrame,
|
||||
dk: FreqaiDataKitchen):
|
||||
"""
|
||||
User can override this if they are using a custom MyRLEnv
|
||||
:params:
|
||||
data_dictionary: dict = common data dictionary containing train and test
|
||||
features/labels/weights.
|
||||
prices_train/test: DataFrame = dataframe comprised of the prices to be used in
|
||||
the environment during training
|
||||
or testing
|
||||
dk: FreqaiDataKitchen = the datakitchen for the current pair
|
||||
"""
|
||||
train_df = data_dictionary["train_features"]
|
||||
test_df = data_dictionary["test_features"]
|
||||
|
||||
env_id = "train_env"
|
||||
self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, train_df, prices_train,
|
||||
self.reward_params, self.CONV_WIDTH, monitor=True,
|
||||
config=self.config) for i
|
||||
in range(self.max_threads)])
|
||||
|
||||
eval_env_id = 'eval_env'
|
||||
self.eval_env = SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1,
|
||||
test_df, prices_test,
|
||||
self.reward_params, self.CONV_WIDTH, monitor=True,
|
||||
config=self.config) for i
|
||||
in range(self.max_threads)])
|
||||
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
||||
render=False, eval_freq=len(train_df),
|
||||
best_model_save_path=str(dk.data_path))
|
||||
|
||||
def _on_stop(self):
|
||||
"""
|
||||
Hook called on bot shutdown. Close SubprocVecEnv subprocesses for clean shutdown.
|
||||
"""
|
||||
|
||||
if self.train_env:
|
||||
self.train_env.close()
|
||||
|
||||
if self.eval_env:
|
||||
self.eval_env.close()
|
@@ -1,37 +0,0 @@
|
||||
{
|
||||
|
||||
"freqai": {
|
||||
"enabled": true,
|
||||
"purge_old_models": true,
|
||||
"train_period_days": 4,
|
||||
"backtest_period_days": 1,
|
||||
"identifier": "spicy-id",
|
||||
"feature_parameters": {
|
||||
"include_timeframes": [
|
||||
"30m",
|
||||
"1h",
|
||||
"4h"
|
||||
],
|
||||
"include_corr_pairlist": [
|
||||
"BTC/USD",
|
||||
"ETH/USD"
|
||||
],
|
||||
"label_period_candles": 20,
|
||||
"include_shifted_candles": 2,
|
||||
"DI_threshold": 0.9,
|
||||
"weight_factor": 0.9,
|
||||
"principal_component_analysis": true,
|
||||
"indicator_periods_candles": [
|
||||
10,
|
||||
20
|
||||
]
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0,
|
||||
"random_state": 1
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 800
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,24 +1,19 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
# for spice rack
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from scipy.signal import argrelextrema
|
||||
from technical import qtpylib
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.exchange.exchange import market_is_active
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.strategy import merge_informative_pair
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -94,136 +89,6 @@ def get_required_data_timerange(config: Config) -> TimeRange:
|
||||
return data_load_timerange
|
||||
|
||||
|
||||
def auto_populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
"""
|
||||
This is a premade `populate_any_indicators()` function which is set in
|
||||
the user strategy is they enable `freqai_spice_rack: true` in their
|
||||
configuration file.
|
||||
"""
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
|
||||
informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
|
||||
informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
|
||||
|
||||
bollinger = qtpylib.bollinger_bands(
|
||||
qtpylib.typical_price(informative), window=t, stds=2.2
|
||||
)
|
||||
informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"]
|
||||
informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"]
|
||||
informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"]
|
||||
|
||||
informative[f"%-{coin}bb_width-period_{t}"] = (
|
||||
informative[f"{coin}bb_upperband-period_{t}"]
|
||||
- informative[f"{coin}bb_lowerband-period_{t}"]
|
||||
) / informative[f"{coin}bb_middleband-period_{t}"]
|
||||
informative[f"%-{coin}close-bb_lower-period_{t}"] = (
|
||||
informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"]
|
||||
)
|
||||
|
||||
informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
|
||||
|
||||
informative[f"%-{coin}relative_volume-period_{t}"] = (
|
||||
informative["volume"] / informative["volume"].rolling(t).mean()
|
||||
)
|
||||
|
||||
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
|
||||
informative[f"%-{coin}raw_volume"] = informative["volume"]
|
||||
informative[f"%-{coin}raw_price"] = informative["close"]
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
if set_generalized_indicators:
|
||||
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||
df["&s-extrema"] = 0
|
||||
min_peaks = argrelextrema(df["close"].values, np.less, order=80)
|
||||
max_peaks = argrelextrema(df["close"].values, np.greater, order=80)
|
||||
for mp in min_peaks[0]:
|
||||
df.at[mp, "&s-extrema"] = -1
|
||||
for mp in max_peaks[0]:
|
||||
df.at[mp, "&s-extrema"] = 1
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def setup_freqai_spice_rack(config: dict, exchange: Optional[Exchange]) -> Dict[str, Any]:
|
||||
import difflib
|
||||
import json
|
||||
from pathlib import Path
|
||||
auto_config = config.get('freqai_config', 'lightgbm_config.json')
|
||||
with open(Path(__file__).parent / Path('spice_rack') / auto_config) as json_file:
|
||||
freqai_config = json.load(json_file)
|
||||
config['freqai'] = freqai_config['freqai']
|
||||
config['freqai']['identifier'] = config['freqai_identifier']
|
||||
corr_pairs = config['freqai']['feature_parameters']['include_corr_pairlist']
|
||||
timeframes = config['freqai']['feature_parameters']['include_timeframes']
|
||||
new_corr_pairs = []
|
||||
new_tfs = []
|
||||
|
||||
if not exchange:
|
||||
logger.warning('No dataprovider available.')
|
||||
config['freqai']['enabled'] = False
|
||||
return config
|
||||
# find the closest pairs to what the default config wants
|
||||
for pair in corr_pairs:
|
||||
closest_pair = difflib.get_close_matches(
|
||||
pair,
|
||||
exchange.markets
|
||||
)
|
||||
if not closest_pair:
|
||||
logger.warning(f'Could not find {pair} in markets, removing from '
|
||||
f'corr_pairlist.')
|
||||
else:
|
||||
closest_pair = closest_pair[0]
|
||||
|
||||
new_corr_pairs.append(closest_pair)
|
||||
logger.info(f'Spice rack will use {closest_pair} as informative in FreqAI model.')
|
||||
|
||||
# find the closest matching timeframes to what the default config wants
|
||||
if timeframe_to_seconds(config['timeframe']) > timeframe_to_seconds('15m'):
|
||||
logger.warning('Default spice rack is designed for lower base timeframes (e.g. > '
|
||||
f'15m). But user passed {config["timeframe"]}.')
|
||||
new_tfs.append(config['timeframe'])
|
||||
|
||||
list_tfs = [timeframe_to_seconds(tf) for tf
|
||||
in exchange.timeframes]
|
||||
for tf in timeframes:
|
||||
tf_secs = timeframe_to_seconds(tf)
|
||||
closest_index = min(range(len(list_tfs)), key=lambda i: abs(list_tfs[i] - tf_secs))
|
||||
closest_tf = exchange.timeframes[closest_index]
|
||||
logger.info(f'Spice rack will use {closest_tf} as informative tf in FreqAI model.')
|
||||
new_tfs.append(closest_tf)
|
||||
|
||||
config['freqai']['feature_parameters'].update({'include_timeframes': new_tfs})
|
||||
config['freqai']['feature_parameters'].update({'include_corr_pairlist': new_corr_pairs})
|
||||
config.update({"freqaimodel": 'LightGBMRegressor'})
|
||||
return config
|
||||
|
||||
# Keep below for when we wish to download heterogeneously lengthed data for FreqAI.
|
||||
# def download_all_data_for_training(dp: DataProvider, config: Config) -> None:
|
||||
# """
|
||||
|
@@ -89,10 +89,6 @@ class Backtesting:
|
||||
self._exchange_name, self.config, load_leverage_tiers=True)
|
||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||
|
||||
if config.get('freqai_spice_rack', False):
|
||||
from freqtrade.freqai.utils import setup_freqai_spice_rack
|
||||
self.config = setup_freqai_spice_rack(self.config, self.exchange)
|
||||
|
||||
if self.config.get('strategy_list'):
|
||||
if self.config.get('freqai', {}).get('enabled', False):
|
||||
logger.warning("Using --strategy-list with FreqAI REQUIRES all strategies "
|
||||
|
@@ -3,8 +3,8 @@ Module that define classes to convert Crypto-currency to FIAT
|
||||
e.g BTC to USD
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
from cachetools import TTLCache
|
||||
@@ -46,9 +46,7 @@ class CryptoToFiatConverter(LoggingMixin):
|
||||
if CryptoToFiatConverter.__instance is None:
|
||||
CryptoToFiatConverter.__instance = object.__new__(cls)
|
||||
try:
|
||||
# Limit retires to 1 (0 and 1)
|
||||
# otherwise we risk bot impact if coingecko is down.
|
||||
CryptoToFiatConverter._coingekko = CoinGeckoAPI(retries=1)
|
||||
CryptoToFiatConverter._coingekko = CoinGeckoAPI()
|
||||
except BaseException:
|
||||
CryptoToFiatConverter._coingekko = None
|
||||
return CryptoToFiatConverter.__instance
|
||||
@@ -69,7 +67,7 @@ class CryptoToFiatConverter(LoggingMixin):
|
||||
logger.warning(
|
||||
"Too many requests for CoinGecko API, backing off and trying again later.")
|
||||
# Set backoff timestamp to 60 seconds in the future
|
||||
self._backoff = datetime.now().timestamp() + 60
|
||||
self._backoff = datetime.datetime.now().timestamp() + 60
|
||||
return
|
||||
# If the request is not a 429 error we want to raise the normal error
|
||||
logger.error(
|
||||
@@ -83,7 +81,7 @@ class CryptoToFiatConverter(LoggingMixin):
|
||||
|
||||
def _get_gekko_id(self, crypto_symbol):
|
||||
if not self._coinlistings:
|
||||
if self._backoff <= datetime.now().timestamp():
|
||||
if self._backoff <= datetime.datetime.now().timestamp():
|
||||
self._load_cryptomap()
|
||||
# Still not loaded.
|
||||
if not self._coinlistings:
|
||||
|
@@ -146,27 +146,11 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
self._ft_informative.append((informative_data, cls_method))
|
||||
|
||||
def load_freqAI_model(self) -> None:
|
||||
spice_rack = self.config.get('freqai_spice_rack', False)
|
||||
if self.config.get('freqai', {}).get('enabled', False) or spice_rack:
|
||||
if spice_rack:
|
||||
from freqtrade.freqai.utils import setup_freqai_spice_rack
|
||||
self.config = setup_freqai_spice_rack(self.config, self.dp._exchange)
|
||||
if self.config.get('freqai', {}).get('enabled', False):
|
||||
# Import here to avoid importing this if freqAI is disabled
|
||||
from freqtrade.freqai.utils import download_all_data_for_training
|
||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||
self.freqai = FreqaiModelResolver.load_freqaimodel(self.config)
|
||||
if not self.process_only_new_candles:
|
||||
logger.warning('User set process_only_new_candles to false, '
|
||||
'FreqAI requires true. Changing to true.')
|
||||
self.process_only_new_candles = True
|
||||
|
||||
if spice_rack:
|
||||
import types
|
||||
|
||||
from freqtrade.freqai.utils import auto_populate_any_indicators
|
||||
self.populate_any_indicators = types.MethodType( # type: ignore
|
||||
auto_populate_any_indicators, self)
|
||||
|
||||
self.freqai_info = self.config["freqai"]
|
||||
|
||||
# download the desired data in dry/live
|
||||
@@ -177,7 +161,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"already on disk."
|
||||
)
|
||||
download_all_data_for_training(self.dp, self.config)
|
||||
|
||||
else:
|
||||
# Gracious failures if freqAI is disabled but "start" is called.
|
||||
class DummyClass():
|
||||
|
@@ -5,7 +5,6 @@
|
||||
import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame
|
||||
from typing import Optional, Union
|
||||
|
||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IStrategy, IntParameter)
|
||||
|
@@ -29,8 +29,9 @@ nav:
|
||||
- Parameter table: freqai-parameter-table.md
|
||||
- Feature engineering: freqai-feature-engineering.md
|
||||
- Running FreqAI: freqai-running.md
|
||||
- Spice Rack: freqai-spice-rack.md
|
||||
- Reinforcement Learning: freqai-reinforcement-learning.md
|
||||
- Developer guide: freqai-developers.md
|
||||
- JOSS paper: paper.md
|
||||
- Short / Leverage: leverage.md
|
||||
- Utility Sub-commands: utils.md
|
||||
- Plotting: plotting.md
|
||||
|
8
requirements-freqai-rl.txt
Normal file
8
requirements-freqai-rl.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements-freqai.txt
|
||||
|
||||
# Required for freqai-rl
|
||||
torch==1.12.1
|
||||
stable-baselines3==1.6.1
|
||||
gym==0.26.2
|
||||
sb3-contrib==1.6.1
|
@@ -7,3 +7,7 @@ joblib==1.2.0
|
||||
catboost==1.1; platform_machine != 'aarch64'
|
||||
lightgbm==3.3.2
|
||||
xgboost==1.6.2
|
||||
torch==1.12.1
|
||||
stable-baselines3==1.6.1
|
||||
gym==0.26.2
|
||||
sb3-contrib==1.6.1
|
||||
|
@@ -1,327 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple command line client for Testing/debugging
|
||||
a Freqtrade bot's message websocket
|
||||
|
||||
Should not import anything from freqtrade,
|
||||
so it can be used as a standalone script.
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import orjson
|
||||
import pandas
|
||||
import rapidjson
|
||||
import websockets
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
logger = logging.getLogger("WebSocketClient")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def setup_logging(filename: str):
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(filename),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'-c',
|
||||
'--config',
|
||||
help='Specify configuration file (default: %(default)s). ',
|
||||
dest='config',
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
default='config.json'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--logfile',
|
||||
help='The filename to log to.',
|
||||
dest='logfile',
|
||||
type=str,
|
||||
default='ws_client.log'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
return vars(args)
|
||||
|
||||
|
||||
def load_config(configfile):
|
||||
file = Path(configfile)
|
||||
if file.is_file():
|
||||
with file.open("r") as f:
|
||||
config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS |
|
||||
rapidjson.PM_TRAILING_COMMAS)
|
||||
return config
|
||||
else:
|
||||
logger.warning(f"Could not load config file {file}.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def readable_timedelta(delta):
|
||||
"""
|
||||
Convert a dateutil.relativedelta to a readable format
|
||||
|
||||
:param delta: A dateutil.relativedelta
|
||||
:returns: The readable time difference string
|
||||
"""
|
||||
attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds']
|
||||
return ", ".join([
|
||||
'%d %s' % (getattr(delta, attr), attr if getattr(delta, attr) > 1 else attr[:-1])
|
||||
for attr in attrs if getattr(delta, attr)
|
||||
])
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def json_serialize(message):
|
||||
"""
|
||||
Serialize a message to JSON using orjson
|
||||
:param message: The message to serialize
|
||||
"""
|
||||
return str(orjson.dumps(message), "utf-8")
|
||||
|
||||
|
||||
def json_deserialize(message):
|
||||
"""
|
||||
Deserialize JSON to a dict
|
||||
:param message: The message to deserialize
|
||||
"""
|
||||
def json_to_dataframe(data: str) -> pandas.DataFrame:
|
||||
dataframe = pandas.read_json(data, orient='split')
|
||||
if 'date' in dataframe.columns:
|
||||
dataframe['date'] = pandas.to_datetime(dataframe['date'], unit='ms', utc=True)
|
||||
|
||||
return dataframe
|
||||
|
||||
def _json_object_hook(z):
|
||||
if z.get('__type__') == 'dataframe':
|
||||
return json_to_dataframe(z.get('__value__'))
|
||||
return z
|
||||
|
||||
return rapidjson.loads(message, object_hook=_json_object_hook)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ClientProtocol:
|
||||
logger = logging.getLogger("WebSocketClient.Protocol")
|
||||
_MESSAGE_COUNT = 0
|
||||
_LAST_RECEIVED_AT = 0 # The epoch we received a message most recently
|
||||
|
||||
async def on_connect(self, websocket):
|
||||
# On connection we have to send our initial requests
|
||||
initial_requests = [
|
||||
{
|
||||
"type": "subscribe", # The subscribe request should always be first
|
||||
"data": ["analyzed_df", "whitelist"] # The message types we want
|
||||
},
|
||||
{
|
||||
"type": "whitelist",
|
||||
"data": None,
|
||||
},
|
||||
{
|
||||
"type": "analyzed_df",
|
||||
"data": {"limit": 1500}
|
||||
}
|
||||
]
|
||||
|
||||
for request in initial_requests:
|
||||
await websocket.send(json_serialize(request))
|
||||
|
||||
async def on_message(self, websocket, name, message):
|
||||
deserialized = json_deserialize(message)
|
||||
|
||||
message_size = sys.getsizeof(message)
|
||||
message_type = deserialized.get('type')
|
||||
message_data = deserialized.get('data')
|
||||
|
||||
self.logger.info(
|
||||
f"Received message of type {message_type} [{message_size} bytes] @ [{name}]"
|
||||
)
|
||||
|
||||
time_difference = self._calculate_time_difference()
|
||||
|
||||
if self._MESSAGE_COUNT > 0:
|
||||
self.logger.info(f"Time since last message: {time_difference}")
|
||||
|
||||
message_handler = getattr(self, f"_handle_{message_type}", None) or self._handle_default
|
||||
await message_handler(name, message_type, message_data)
|
||||
|
||||
self._MESSAGE_COUNT += 1
|
||||
self.logger.info(f"[{self._MESSAGE_COUNT}] total messages..")
|
||||
self.logger.info("-" * 80)
|
||||
|
||||
def _calculate_time_difference(self):
|
||||
old_last_received_at = self._LAST_RECEIVED_AT
|
||||
self._LAST_RECEIVED_AT = time.time() * 1000
|
||||
time_delta = relativedelta(microseconds=(self._LAST_RECEIVED_AT - old_last_received_at))
|
||||
|
||||
return readable_timedelta(time_delta)
|
||||
|
||||
async def _handle_whitelist(self, name, type, data):
|
||||
self.logger.info(data)
|
||||
|
||||
async def _handle_analyzed_df(self, name, type, data):
|
||||
key, la, df = data['key'], data['la'], data['df']
|
||||
|
||||
if not df.empty:
|
||||
columns = ", ".join([str(column) for column in df.columns])
|
||||
|
||||
self.logger.info(key)
|
||||
self.logger.info(f"Last analyzed datetime: {la}")
|
||||
self.logger.info(f"Latest candle datetime: {df.iloc[-1]['date']}")
|
||||
self.logger.info(f"DataFrame length: {len(df)}")
|
||||
self.logger.info(f"DataFrame columns: {columns}")
|
||||
else:
|
||||
self.logger.info("Empty DataFrame")
|
||||
|
||||
async def _handle_default(self, name, type, data):
|
||||
self.logger.info("Unkown message of type {type} received...")
|
||||
self.logger.info(data)
|
||||
|
||||
|
||||
async def create_client(
|
||||
host,
|
||||
port,
|
||||
token,
|
||||
name='default',
|
||||
protocol=ClientProtocol(),
|
||||
sleep_time=10,
|
||||
ping_timeout=10,
|
||||
wait_timeout=30,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Create a websocket client and listen for messages
|
||||
:param host: The host
|
||||
:param port: The port
|
||||
:param token: The websocket auth token
|
||||
:param name: The name of the producer
|
||||
:param **kwargs: Any extra kwargs passed to websockets.connect
|
||||
"""
|
||||
|
||||
while 1:
|
||||
try:
|
||||
websocket_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
|
||||
logger.info(f"Attempting to connect to {name} @ {host}:{port}")
|
||||
|
||||
async with websockets.connect(websocket_url, **kwargs) as ws:
|
||||
logger.info("Connection successful...")
|
||||
await protocol.on_connect(ws)
|
||||
|
||||
# Now listen for messages
|
||||
while 1:
|
||||
try:
|
||||
message = await asyncio.wait_for(
|
||||
ws.recv(),
|
||||
timeout=wait_timeout
|
||||
)
|
||||
|
||||
await protocol.on_message(ws, name, message)
|
||||
|
||||
except (
|
||||
asyncio.TimeoutError,
|
||||
websockets.exceptions.ConnectionClosed
|
||||
):
|
||||
# Try pinging
|
||||
try:
|
||||
pong = ws.ping()
|
||||
await asyncio.wait_for(
|
||||
pong,
|
||||
timeout=ping_timeout
|
||||
)
|
||||
logger.info("Connection still alive...")
|
||||
|
||||
continue
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Ping timed out, retrying in {sleep_time}s")
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
break
|
||||
|
||||
except (
|
||||
socket.gaierror,
|
||||
ConnectionRefusedError,
|
||||
websockets.exceptions.InvalidStatusCode,
|
||||
websockets.exceptions.InvalidMessage
|
||||
) as e:
|
||||
logger.error(f"Connection Refused - {e} retrying in {sleep_time}s")
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
continue
|
||||
|
||||
except (
|
||||
websockets.exceptions.ConnectionClosedError,
|
||||
websockets.exceptions.ConnectionClosedOK
|
||||
):
|
||||
# Just keep trying to connect again indefinitely
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
# An unforseen error has occurred, log and try reconnecting again
|
||||
logger.error("Unexpected error has occurred:")
|
||||
logger.exception(e)
|
||||
|
||||
await asyncio.sleep(sleep_time)
|
||||
continue
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _main(args):
|
||||
setup_logging(args['logfile'])
|
||||
config = load_config(args['config'])
|
||||
|
||||
emc_config = config.get('external_message_consumer', {})
|
||||
|
||||
producers = emc_config.get('producers', [])
|
||||
producer = producers[0]
|
||||
|
||||
wait_timeout = emc_config.get('wait_timeout', 300)
|
||||
ping_timeout = emc_config.get('ping_timeout', 10)
|
||||
sleep_time = emc_config.get('sleep_time', 10)
|
||||
message_size_limit = (emc_config.get('message_size_limit', 8) << 20)
|
||||
|
||||
await create_client(
|
||||
producer['host'],
|
||||
producer['port'],
|
||||
producer['ws_token'],
|
||||
producer['name'],
|
||||
sleep_time=sleep_time,
|
||||
ping_timeout=ping_timeout,
|
||||
wait_timeout=wait_timeout,
|
||||
max_size=message_size_limit
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
try:
|
||||
asyncio.run(_main(args))
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Exiting...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
9
setup.sh
9
setup.sh
@@ -78,14 +78,21 @@ function updateenv() {
|
||||
fi
|
||||
|
||||
REQUIREMENTS_FREQAI=""
|
||||
REQUIREMENTS_FREQAI_RL=""
|
||||
read -p "Do you want to install dependencies for freqai [y/N]? "
|
||||
dev=$REPLY
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||
then
|
||||
REQUIREMENTS_FREQAI="-r requirements-freqai.txt"
|
||||
read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? "
|
||||
dev=$REPLY
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||
then
|
||||
REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt"
|
||||
fi
|
||||
fi
|
||||
|
||||
${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI}
|
||||
${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} ${REQUIREMENTS_FREQAI_RL}
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed installing dependencies"
|
||||
exit 1
|
||||
|
@@ -10,7 +10,6 @@ from unittest.mock import MagicMock, Mock, PropertyMock
|
||||
|
||||
import arrow
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from telegram import Chat, Message, Update
|
||||
|
||||
@@ -20,7 +19,6 @@ from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import CandleType, MarginMode, RunMode, SignalDirection, TradingMode
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange import timeframe_to_minutes
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
@@ -84,33 +82,6 @@ def get_args(args):
|
||||
return Arguments(args).get_parsed_arg()
|
||||
|
||||
|
||||
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||
np.random.seed(42)
|
||||
tf_mins = timeframe_to_minutes(timeframe)
|
||||
|
||||
base = np.random.normal(20, 2, size=size)
|
||||
|
||||
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
|
||||
df = pd.DataFrame({
|
||||
'date': date,
|
||||
'open': base,
|
||||
'high': base + np.random.normal(2, 1, size=size),
|
||||
'low': base - np.random.normal(2, 1, size=size),
|
||||
'close': base + np.random.normal(0, 1, size=size),
|
||||
'volume': np.random.normal(200, size=size)
|
||||
}
|
||||
)
|
||||
df = df.dropna()
|
||||
return df
|
||||
|
||||
|
||||
def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||
""" Generates data in the ohlcv format used by ccxt """
|
||||
df = generate_test_data(timeframe, size, start)
|
||||
df['date'] = df.loc[:, 'date'].view(np.int64) // 1000 // 1000
|
||||
return list(list(x) for x in zip(*(df[x].values.tolist() for x in df.columns)))
|
||||
|
||||
|
||||
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||
# TODO: This should be replaced with AsyncMock once support for python 3.7 is dropped.
|
||||
def get_mock_coro(return_value=None, side_effect=None):
|
||||
|
@@ -22,8 +22,7 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_CO
|
||||
calculate_backoff, remove_credentials)
|
||||
from freqtrade.exchange.exchange import amount_to_contract_precision
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_exchange, log_has,
|
||||
log_has_re, num_log_has_re)
|
||||
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re
|
||||
|
||||
|
||||
# Make sure to always keep one exchange here which is NOT subclassed!!
|
||||
@@ -2084,7 +2083,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
|
||||
def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None:
|
||||
ohlcv = [
|
||||
[
|
||||
(arrow.utcnow().shift(minutes=-5).int_timestamp) * 1000, # unix timestamp ms
|
||||
(arrow.utcnow().int_timestamp - 1) * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
@@ -2141,22 +2140,10 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None
|
||||
assert len(res) == len(pairs)
|
||||
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||
exchange.required_candle_call_count = 1
|
||||
assert log_has(f"Using cached candle (OHLCV) data for {pairs[0][0]}, "
|
||||
f"{pairs[0][1]}, {candle_type} ...",
|
||||
caplog)
|
||||
caplog.clear()
|
||||
# Reset refresh times - must do 2 call per pair as cache is expired
|
||||
exchange._pairs_last_refresh_time = {}
|
||||
res = exchange.refresh_latest_ohlcv(
|
||||
[('IOTA/ETH', '5m', candle_type), ('XRP/ETH', '5m', candle_type)])
|
||||
assert len(res) == len(pairs)
|
||||
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 4
|
||||
|
||||
# cache - but disabled caching
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
exchange.required_candle_call_count = 1
|
||||
|
||||
pairlist = [
|
||||
('IOTA/ETH', '5m', candle_type),
|
||||
('XRP/ETH', '5m', candle_type),
|
||||
@@ -2172,7 +2159,6 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
caplog.clear()
|
||||
|
||||
# Call with invalid timeframe
|
||||
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '3m', candle_type)], cache=False)
|
||||
if candle_type != CandleType.MARK:
|
||||
@@ -2183,91 +2169,6 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None
|
||||
assert len(res) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('candle_type', [CandleType.FUTURES, CandleType.MARK, CandleType.SPOT])
|
||||
def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_machine) -> None:
|
||||
start = datetime(2021, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
|
||||
ohlcv = generate_test_data_raw('1h', 100, start.strftime('%Y-%m-%d'))
|
||||
time_machine.move_to(start + timedelta(hours=99, minutes=30))
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
pair1 = ('IOTA/ETH', '1h', candle_type)
|
||||
pair2 = ('XRP/ETH', '1h', candle_type)
|
||||
pairs = [pair1, pair2]
|
||||
|
||||
# No caching
|
||||
assert not exchange._klines
|
||||
res = exchange.refresh_latest_ohlcv(pairs, cache=False)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert not exchange._klines
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
|
||||
# With caching
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert exchange._klines
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
|
||||
# Returned from cache
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
|
||||
# Move time 1 candle further but result didn't change yet
|
||||
time_machine.move_to(start + timedelta(hours=101))
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
refresh_pior = exchange._pairs_last_refresh_time[pair1]
|
||||
|
||||
# New candle on exchange - only return 50 candles (but one candle further)
|
||||
new_startdate = (start + timedelta(hours=51)).strftime('%Y-%m-%d %H:%M')
|
||||
ohlcv = generate_test_data_raw('1h', 50, new_startdate)
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 100
|
||||
assert len(res[pair2]) == 100
|
||||
assert refresh_pior != exchange._pairs_last_refresh_time[pair1]
|
||||
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
assert exchange._pairs_last_refresh_time[pair2] == ohlcv[-1][0] // 1000
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
|
||||
# Retry same call - no action.
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 100
|
||||
assert len(res[pair2]) == 100
|
||||
|
||||
# Move to distant future (so a 1 call would cause a hole in the data)
|
||||
time_machine.move_to(start + timedelta(hours=2000))
|
||||
ohlcv = generate_test_data_raw('1h', 100, start + timedelta(hours=1900))
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
# Cache eviction - new data.
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||
|
@@ -158,28 +158,3 @@ def test_make_train_test_datasets(mocker, freqai_conf):
|
||||
assert data_dictionary
|
||||
assert len(data_dictionary) == 7
|
||||
assert len(data_dictionary['train_features'].index) == 1916
|
||||
|
||||
|
||||
@pytest.mark.parametrize('indicator', [
|
||||
'%-ADArsi-period_10_5m',
|
||||
'doesnt_exist',
|
||||
])
|
||||
def test_spice_extractor(mocker, freqai_conf, indicator, caplog):
|
||||
freqai, unfiltered_dataframe = make_unfiltered_dataframe(mocker, freqai_conf)
|
||||
freqai.dk.find_features(unfiltered_dataframe)
|
||||
|
||||
features_filtered, labels_filtered = freqai.dk.filter_features(
|
||||
unfiltered_dataframe,
|
||||
freqai.dk.training_features_list,
|
||||
freqai.dk.label_list,
|
||||
training_filter=True,
|
||||
)
|
||||
|
||||
vec = freqai.dk.spice_extractor(indicator, features_filtered)
|
||||
if 'doesnt_exist' in indicator:
|
||||
assert log_has_re(
|
||||
"User asked spice_rack for",
|
||||
caplog,
|
||||
)
|
||||
else:
|
||||
assert len(vec) == 2860
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import copy
|
||||
import platform
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
@@ -32,16 +31,47 @@ def is_mac() -> bool:
|
||||
'LightGBMRegressor',
|
||||
'XGBoostRegressor',
|
||||
'CatboostRegressor',
|
||||
'ReinforcementLearner',
|
||||
'ReinforcementLearner_multiproc',
|
||||
'ReinforcementLearner_test_4ac'
|
||||
])
|
||||
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model):
|
||||
if is_arm() and model == 'CatboostRegressor':
|
||||
pytest.skip("CatBoost is not supported on ARM")
|
||||
|
||||
if is_mac():
|
||||
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||
|
||||
model_save_ext = 'joblib'
|
||||
freqai_conf.update({"freqaimodel": model})
|
||||
freqai_conf.update({"timerange": "20180110-20180130"})
|
||||
freqai_conf.update({"strategy": "freqai_test_strat"})
|
||||
|
||||
if 'ReinforcementLearner' in model:
|
||||
model_save_ext = 'zip'
|
||||
freqai_conf.update({"strategy": "freqai_rl_test_strat"})
|
||||
freqai_conf["freqai"].update({"model_training_parameters": {
|
||||
"learning_rate": 0.00025,
|
||||
"gamma": 0.9,
|
||||
"verbose": 1
|
||||
}})
|
||||
freqai_conf["freqai"].update({"model_save_type": 'stable_baselines'})
|
||||
freqai_conf["freqai"]["rl_config"] = {
|
||||
"train_cycles": 1,
|
||||
"thread_count": 2,
|
||||
"max_trade_duration_candles": 300,
|
||||
"model_type": "PPO",
|
||||
"policy_type": "MlpPolicy",
|
||||
"max_training_drawdown_pct": 0.5,
|
||||
"model_reward_parameters": {
|
||||
"rr": 1,
|
||||
"profit_aim": 0.02,
|
||||
"win_reward_factor": 2
|
||||
}}
|
||||
|
||||
if 'test_4ac' in model:
|
||||
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
@@ -153,6 +183,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
("LightGBMRegressor", 6, "freqai_test_strat"),
|
||||
("XGBoostRegressor", 6, "freqai_test_strat"),
|
||||
("CatboostRegressor", 6, "freqai_test_strat"),
|
||||
("ReinforcementLearner", 7, "freqai_rl_test_strat"),
|
||||
("XGBoostClassifier", 6, "freqai_test_classifier"),
|
||||
("LightGBMClassifier", 6, "freqai_test_classifier"),
|
||||
("CatboostClassifier", 6, "freqai_test_classifier")
|
||||
@@ -165,10 +196,37 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat):
|
||||
if is_arm() and "Catboost" in model:
|
||||
pytest.skip("CatBoost is not supported on ARM")
|
||||
|
||||
if is_mac():
|
||||
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
|
||||
|
||||
freqai_conf.update({"freqaimodel": model})
|
||||
freqai_conf.update({"timerange": "20180120-20180130"})
|
||||
freqai_conf.update({"strategy": strat})
|
||||
|
||||
if 'ReinforcementLearner' in model:
|
||||
|
||||
freqai_conf["freqai"].update({"model_training_parameters": {
|
||||
"learning_rate": 0.00025,
|
||||
"gamma": 0.9,
|
||||
"verbose": 1
|
||||
}})
|
||||
freqai_conf["freqai"].update({"model_save_type": 'stable_baselines'})
|
||||
freqai_conf["freqai"]["rl_config"] = {
|
||||
"train_cycles": 1,
|
||||
"thread_count": 2,
|
||||
"max_trade_duration_candles": 300,
|
||||
"model_type": "PPO",
|
||||
"policy_type": "MlpPolicy",
|
||||
"max_training_drawdown_pct": 0.5,
|
||||
"model_reward_parameters": {
|
||||
"rr": 1,
|
||||
"profit_aim": 0.02,
|
||||
"win_reward_factor": 2
|
||||
}}
|
||||
|
||||
if 'test_4ac' in model:
|
||||
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
@@ -383,31 +441,6 @@ def test_plot_feature_importance(mocker, freqai_conf):
|
||||
shutil.rmtree(Path(freqai.dk.full_path))
|
||||
|
||||
|
||||
def test_spice_rack(mocker, default_conf, tmpdir, caplog):
|
||||
|
||||
strategy = get_patched_freqai_strategy(mocker, default_conf)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
strategy.dp = DataProvider(default_conf, exchange)
|
||||
|
||||
default_conf.update({"freqai_spice_rack": "true"})
|
||||
default_conf.update({"freqai_identifier": "spicy-id"})
|
||||
default_conf["config_files"] = [Path('config_examples', 'config_freqai.example.json')]
|
||||
default_conf["timerange"] = "20180110-20180115"
|
||||
default_conf["datadir"] = Path(default_conf["datadir"])
|
||||
default_conf['exchange'].update({'pair_whitelist':
|
||||
['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC']})
|
||||
default_conf["user_data_dir"] = Path(tmpdir)
|
||||
freqai_conf = copy.deepcopy(default_conf)
|
||||
|
||||
strategy.config = freqai_conf
|
||||
strategy.load_freqAI_model()
|
||||
|
||||
assert log_has_re("Spice rack will use LTC/USD", caplog)
|
||||
assert log_has_re("Spice rack will use 15m", caplog)
|
||||
assert 'freqai' in freqai_conf
|
||||
assert strategy.freqai
|
||||
|
||||
|
||||
@pytest.mark.parametrize('timeframes,corr_pairs', [
|
||||
(['5m'], ['ADA/BTC', 'DASH/BTC']),
|
||||
(['5m'], ['ADA/BTC', 'DASH/BTC', 'ETH/USDT']),
|
||||
|
104
tests/freqai/test_models/ReinforcementLearner_test_4ac.py
Normal file
104
tests/freqai/test_models/ReinforcementLearner_test_4ac.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import numpy as np
|
||||
import torch as th
|
||||
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from freqtrade.freqai.RL.Base4ActionRLEnv import Actions, Base4ActionRLEnv, Positions
|
||||
from freqtrade.freqai.RL.BaseReinforcementLearningModel import BaseReinforcementLearningModel
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReinforcementLearner_test_4ac(BaseReinforcementLearningModel):
|
||||
"""
|
||||
User created Reinforcement Learning Model prediction model.
|
||||
"""
|
||||
|
||||
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
||||
|
||||
train_df = data_dictionary["train_features"]
|
||||
total_timesteps = self.freqai_info["rl_config"]["train_cycles"] * len(train_df)
|
||||
|
||||
policy_kwargs = dict(activation_fn=th.nn.ReLU,
|
||||
net_arch=[128, 128])
|
||||
|
||||
if dk.pair not in self.dd.model_dictionary or not self.continual_learning:
|
||||
model = self.MODELCLASS(self.policy_type, self.train_env, policy_kwargs=policy_kwargs,
|
||||
tensorboard_log=Path(
|
||||
dk.full_path / "tensorboard" / dk.pair.split('/')[0]),
|
||||
**self.freqai_info['model_training_parameters']
|
||||
)
|
||||
else:
|
||||
logger.info('Continual training activated - starting training from previously '
|
||||
'trained agent.')
|
||||
model = self.dd.model_dictionary[dk.pair]
|
||||
model.set_env(self.train_env)
|
||||
|
||||
model.learn(
|
||||
total_timesteps=int(total_timesteps),
|
||||
callback=self.eval_callback
|
||||
)
|
||||
|
||||
if Path(dk.data_path / "best_model.zip").is_file():
|
||||
logger.info('Callback found a best model.')
|
||||
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
||||
return best_model
|
||||
|
||||
logger.info('Couldnt find best model, using final model instead.')
|
||||
|
||||
return model
|
||||
|
||||
class MyRLEnv(Base4ActionRLEnv):
|
||||
"""
|
||||
User can override any function in BaseRLEnv and gym.Env. Here the user
|
||||
sets a custom reward based on profit and trade duration.
|
||||
"""
|
||||
|
||||
def calculate_reward(self, action):
|
||||
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
return -2
|
||||
|
||||
pnl = self.get_unrealized_profit()
|
||||
rew = np.sign(pnl) * (pnl + 1)
|
||||
factor = 100
|
||||
|
||||
# reward agent for entering trades
|
||||
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||
and self._position == Positions.Neutral):
|
||||
return 25
|
||||
# discourage agent from not entering trades
|
||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||
return -1
|
||||
|
||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||
trade_duration = self._current_tick - self._last_trade_tick
|
||||
|
||||
if trade_duration <= max_trade_duration:
|
||||
factor *= 1.5
|
||||
elif trade_duration > max_trade_duration:
|
||||
factor *= 0.5
|
||||
|
||||
# discourage sitting in position
|
||||
if (self._position in (Positions.Short, Positions.Long) and
|
||||
action == Actions.Neutral.value):
|
||||
return -1 * trade_duration / max_trade_duration
|
||||
|
||||
# close long
|
||||
if action == Actions.Exit.value and self._position == Positions.Long:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(rew * factor)
|
||||
|
||||
# close short
|
||||
if action == Actions.Exit.value and self._position == Positions.Short:
|
||||
if pnl > self.profit_aim * self.rr:
|
||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||
return float(rew * factor)
|
||||
|
||||
return 0.
|
@@ -1457,6 +1457,7 @@ def test_api_strategies(botclient):
|
||||
'StrategyTestV2',
|
||||
'StrategyTestV3',
|
||||
'StrategyTestV3Futures',
|
||||
'freqai_rl_test_strat',
|
||||
'freqai_test_classifier',
|
||||
'freqai_test_multimodel_strat',
|
||||
'freqai_test_strat'
|
||||
|
139
tests/strategy/strats/freqai_rl_test_strat.py
Normal file
139
tests/strategy/strats/freqai_rl_test_strat.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import logging
|
||||
from functools import reduce
|
||||
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import IStrategy, merge_informative_pair
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class freqai_rl_test_strat(IStrategy):
|
||||
"""
|
||||
Test strategy - used for testing freqAI functionalities.
|
||||
DO not use in production.
|
||||
"""
|
||||
|
||||
minimal_roi = {"0": 0.1, "240": -1}
|
||||
|
||||
plot_config = {
|
||||
"main_plot": {},
|
||||
"subplots": {
|
||||
"prediction": {"prediction": {"color": "blue"}},
|
||||
"target_roi": {
|
||||
"target_roi": {"color": "brown"},
|
||||
},
|
||||
"do_predict": {
|
||||
"do_predict": {"color": "brown"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
process_only_new_candles = True
|
||||
stoploss = -0.05
|
||||
use_exit_signal = True
|
||||
startup_candle_count: int = 30
|
||||
can_short = False
|
||||
|
||||
def informative_pairs(self):
|
||||
whitelist_pairs = self.dp.current_whitelist()
|
||||
corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"]
|
||||
informative_pairs = []
|
||||
for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]:
|
||||
for pair in whitelist_pairs:
|
||||
informative_pairs.append((pair, tf))
|
||||
for pair in corr_pairs:
|
||||
if pair in whitelist_pairs:
|
||||
continue # avoid duplication
|
||||
informative_pairs.append((pair, tf))
|
||||
return informative_pairs
|
||||
|
||||
def populate_any_indicators(
|
||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||
):
|
||||
|
||||
coin = pair.split('/')[0]
|
||||
|
||||
if informative is None:
|
||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||
|
||||
# first loop is automatically duplicating indicators for time periods
|
||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||
|
||||
t = int(t)
|
||||
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
|
||||
|
||||
# FIXME: add these outside the user strategy?
|
||||
# The following columns are necessary for RL models.
|
||||
informative[f"%-{coin}raw_close"] = informative["close"]
|
||||
informative[f"%-{coin}raw_open"] = informative["open"]
|
||||
informative[f"%-{coin}raw_high"] = informative["high"]
|
||||
informative[f"%-{coin}raw_low"] = informative["low"]
|
||||
|
||||
indicators = [col for col in informative if col.startswith("%")]
|
||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||
if n == 0:
|
||||
continue
|
||||
informative_shift = informative[indicators].shift(n)
|
||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||
informative = pd.concat((informative, informative_shift), axis=1)
|
||||
|
||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||
skip_columns = [
|
||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||
]
|
||||
df = df.drop(columns=skip_columns)
|
||||
|
||||
# Add generalized indicators here (because in live, it will call this
|
||||
# function to populate indicators during training). Notice how we ensure not to
|
||||
# add them multiple times
|
||||
if set_generalized_indicators:
|
||||
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||
|
||||
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||
# until the agent sends an action.
|
||||
df["&-action"] = 0
|
||||
|
||||
return df
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
enter_long_conditions = [df["do_predict"] == 1, df["&-action"] == 1]
|
||||
|
||||
if enter_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
|
||||
] = (1, "long")
|
||||
|
||||
enter_short_conditions = [df["do_predict"] == 1, df["&-action"] == 3]
|
||||
|
||||
if enter_short_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]
|
||||
] = (1, "short")
|
||||
|
||||
return df
|
||||
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
exit_long_conditions = [df["do_predict"] == 1, df["&-action"] == 2]
|
||||
if exit_long_conditions:
|
||||
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
|
||||
|
||||
exit_short_conditions = [df["do_predict"] == 1, df["&-action"] == 4]
|
||||
if exit_short_conditions:
|
||||
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
|
||||
|
||||
return df
|
@@ -5,8 +5,29 @@ import pytest
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
from freqtrade.strategy import merge_informative_pair, stoploss_from_absolute, stoploss_from_open
|
||||
from tests.conftest import generate_test_data, get_patched_exchange
|
||||
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
|
||||
timeframe_to_minutes)
|
||||
from tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||
np.random.seed(42)
|
||||
tf_mins = timeframe_to_minutes(timeframe)
|
||||
|
||||
base = np.random.normal(20, 2, size=size)
|
||||
|
||||
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
|
||||
df = pd.DataFrame({
|
||||
'date': date,
|
||||
'open': base,
|
||||
'high': base + np.random.normal(2, 1, size=size),
|
||||
'low': base - np.random.normal(2, 1, size=size),
|
||||
'close': base + np.random.normal(0, 1, size=size),
|
||||
'volume': np.random.normal(200, size=size)
|
||||
}
|
||||
)
|
||||
df = df.dropna()
|
||||
return df
|
||||
|
||||
|
||||
def test_merge_informative_pair():
|
||||
|
@@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed():
|
||||
directory = Path(__file__).parent / "strats"
|
||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
||||
assert isinstance(strategies, list)
|
||||
assert len(strategies) == 9
|
||||
assert len(strategies) == 10
|
||||
assert isinstance(strategies[0], dict)
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed():
|
||||
directory = Path(__file__).parent / "strats"
|
||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
||||
assert isinstance(strategies, list)
|
||||
assert len(strategies) == 10
|
||||
assert len(strategies) == 11
|
||||
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
||||
# and 1 which fails to load
|
||||
assert len([x for x in strategies if x['class'] is not None]) == 9
|
||||
assert len([x for x in strategies if x['class'] is not None]) == 10
|
||||
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||
|
||||
directory = Path(__file__).parent / "strats_nonexistingdir"
|
||||
|
Reference in New Issue
Block a user